Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1550291d6b | ||
|
|
a1736c8a17 | ||
|
|
548ebee47f | ||
|
|
e28d1a2fd0 | ||
|
|
45836f4a30 | ||
|
|
b107a12798 | ||
|
|
28ecc4d138 | ||
|
|
2a0960c251 | ||
|
|
b67d07e977 | ||
|
|
ea3da4e138 | ||
|
|
bd1df35456 | ||
|
|
22692be45e | ||
|
|
eea2ce86d0 | ||
|
|
94d84a368f | ||
|
|
3bfeb81c90 | ||
|
|
ef795c46b2 | ||
|
|
677482639d | ||
|
|
65013dad3e | ||
|
|
2db6bb8519 | ||
|
|
fd00ce11d6 | ||
|
|
1da4f8f479 | ||
|
|
20b2afb0fc | ||
|
|
aaa618b15d | ||
|
|
60eff4a955 | ||
|
|
86535f5de2 | ||
|
|
015aeb989a | ||
|
|
1a50f7680a |
+2
-1
@@ -6,7 +6,7 @@ from contextlib import asynccontextmanager
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
from database import init_db
|
from database import init_db
|
||||||
from routes import stock, collections, shares, sectors
|
from routes import stock, collections, shares, sectors, themes
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
@@ -31,6 +31,7 @@ app.include_router(stock.router, prefix="/api/stock")
|
|||||||
app.include_router(collections.router, prefix="/api/collections")
|
app.include_router(collections.router, prefix="/api/collections")
|
||||||
app.include_router(shares.router, prefix="/api/share")
|
app.include_router(shares.router, prefix="/api/share")
|
||||||
app.include_router(sectors.router, prefix="/api/sectors")
|
app.include_router(sectors.router, prefix="/api/sectors")
|
||||||
|
app.include_router(themes.router, prefix="/api/themes")
|
||||||
|
|
||||||
# 生产模式:后端同时托管前端静态文件
|
# 生产模式:后端同时托管前端静态文件
|
||||||
# catch-all 路由在 API 路由之后注册,所以 API 优先级更高
|
# catch-all 路由在 API 路由之后注册,所以 API 优先级更高
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ fastapi==0.115.0
|
|||||||
uvicorn==0.30.0
|
uvicorn==0.30.0
|
||||||
httpx==0.27.0
|
httpx==0.27.0
|
||||||
python-dotenv==1.0.1
|
python-dotenv==1.0.1
|
||||||
akshare==1.18.64
|
akshare==1.18.64
|
||||||
|
mootdx
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
"""板块数据路由:行业板块、概念板块"""
|
"""板块数据路由:行业板块、概念板块"""
|
||||||
|
|
||||||
from fastapi import APIRouter, Query, HTTPException
|
from fastapi import APIRouter, Query, HTTPException
|
||||||
from services import eastmoney
|
from fastapi.responses import JSONResponse
|
||||||
|
from services import eastmoney, mootdx
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
# 行业/概念通过 query 参数区分,但上游反代/CDN 可能按 path 缓存而忽略 query,
|
||||||
|
# 导致两个 tab 返回相同数据。显式禁止缓存,保证按 query 区分。
|
||||||
|
_NO_CACHE_HEADERS = {"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"}
|
||||||
|
|
||||||
|
|
||||||
@router.get("", summary="板块列表")
|
@router.get("", summary="板块列表")
|
||||||
async def sector_list(
|
async def sector_list(
|
||||||
@@ -14,4 +19,21 @@ async def sector_list(
|
|||||||
raise HTTPException(status_code=400, detail="板块类型错误,仅支持 industry/concept")
|
raise HTTPException(status_code=400, detail="板块类型错误,仅支持 industry/concept")
|
||||||
|
|
||||||
data = await eastmoney.fetch_sector_list(type)
|
data = await eastmoney.fetch_sector_list(type)
|
||||||
return {"data": data, "count": len(data), "type": type}
|
if data:
|
||||||
|
return JSONResponse(
|
||||||
|
{"data": data, "count": len(data), "type": type},
|
||||||
|
headers=_NO_CACHE_HEADERS,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 降级:通达信 mootdx(不含实时资金流数据)
|
||||||
|
md_data = await mootdx.fetch_sector_list(type)
|
||||||
|
if md_data:
|
||||||
|
return JSONResponse(
|
||||||
|
{"data": md_data, "count": len(md_data), "type": type, "source": "mootdx"},
|
||||||
|
headers=_NO_CACHE_HEADERS,
|
||||||
|
)
|
||||||
|
|
||||||
|
return JSONResponse(
|
||||||
|
{"data": [], "count": 0, "type": type},
|
||||||
|
headers=_NO_CACHE_HEADERS,
|
||||||
|
)
|
||||||
|
|||||||
+37
-6
@@ -3,7 +3,7 @@
|
|||||||
from fastapi import APIRouter, Query, HTTPException
|
from fastapi import APIRouter, Query, HTTPException
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from services import tencent, sina, eastmoney
|
from services import tencent, sina, eastmoney, mootdx
|
||||||
from models import StockSearchResult, StockQuote, KLineData, FundFlowData, FundFlowSummary, CompanyProfile, FinancialReportItem, FinancialDataResponse
|
from models import StockSearchResult, StockQuote, KLineData, FundFlowData, FundFlowSummary, CompanyProfile, FinancialReportItem, FinancialDataResponse
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -54,21 +54,28 @@ async def stock_history(
|
|||||||
if not re.match(r"^\d{6}$", code):
|
if not re.match(r"^\d{6}$", code):
|
||||||
raise HTTPException(status_code=400, detail="股票代码格式错误,需为6位数字")
|
raise HTTPException(status_code=400, detail="股票代码格式错误,需为6位数字")
|
||||||
|
|
||||||
# 主数据源:东方财富 push2his(含成交额/涨跌幅/振幅/换手率,可能被限流)
|
# 主数据源:通达信 mootdx(TCP直连,不被限流,稳定可靠)
|
||||||
|
md_klines = await mootdx.fetch_kline_history(code, days)
|
||||||
|
if md_klines and len(md_klines) >= 2:
|
||||||
|
return {"data": md_klines, "count": len(md_klines), "source": "mootdx"}
|
||||||
|
|
||||||
|
# 降级1:东方财富 push2his(含成交额/涨跌幅/振幅/换手率)
|
||||||
em_klines = await eastmoney.fetch_kline_history(code, days)
|
em_klines = await eastmoney.fetch_kline_history(code, days)
|
||||||
if em_klines and len(em_klines) >= 2:
|
if em_klines and len(em_klines) >= 2:
|
||||||
return {"data": em_klines, "count": len(em_klines), "source": "eastmoney"}
|
return {"data": em_klines, "count": len(em_klines), "source": "eastmoney"}
|
||||||
|
|
||||||
# 降级1:腾讯(含涨跌幅)
|
# 降级2:腾讯(含涨跌幅)
|
||||||
tencent_klines = await tencent.fetch_history(code, days)
|
tencent_klines = await tencent.fetch_history(code, days)
|
||||||
if tencent_klines and len(tencent_klines) >= 2:
|
if tencent_klines and len(tencent_klines) >= 2:
|
||||||
return {"data": tencent_klines, "count": len(tencent_klines), "source": "tencent"}
|
return {"data": tencent_klines, "count": len(tencent_klines), "source": "tencent"}
|
||||||
|
|
||||||
# 降级2:新浪
|
# 降级3:新浪
|
||||||
sina_klines = await sina.fetch_history(code, days)
|
sina_klines = await sina.fetch_history(code, days)
|
||||||
if sina_klines:
|
if sina_klines:
|
||||||
return {"data": sina_klines, "count": len(sina_klines), "source": "sina"}
|
return {"data": sina_klines, "count": len(sina_klines), "source": "sina"}
|
||||||
|
|
||||||
|
if md_klines:
|
||||||
|
return {"data": md_klines, "count": len(md_klines), "source": "mootdx"}
|
||||||
if em_klines:
|
if em_klines:
|
||||||
return {"data": em_klines, "count": len(em_klines), "source": "eastmoney"}
|
return {"data": em_klines, "count": len(em_klines), "source": "eastmoney"}
|
||||||
if tencent_klines:
|
if tencent_klines:
|
||||||
@@ -194,12 +201,20 @@ async def stock_fund_flow(
|
|||||||
if not data:
|
if not data:
|
||||||
raise HTTPException(status_code=404, detail="未获取到资金流向数据")
|
raise HTTPException(status_code=404, detail="未获取到资金流向数据")
|
||||||
|
|
||||||
# 从腾讯 K 线补充成交额/收盘价/涨跌幅
|
# 从腾讯 K 线补充收盘价/涨跌幅
|
||||||
kline_map = await tencent.fetch_kline_map(code, days)
|
kline_map = await tencent.fetch_kline_map(code, days)
|
||||||
|
# 从东方财富 K 线补充成交额(腾讯 fqkline 不含成交额字段)
|
||||||
|
em_klines = await eastmoney.fetch_kline_history(code, days)
|
||||||
|
em_kline_map = {}
|
||||||
|
if em_klines:
|
||||||
|
for k in em_klines:
|
||||||
|
if k.get("turnover"):
|
||||||
|
em_kline_map[k["date"]] = k["turnover"]
|
||||||
for d in data:
|
for d in data:
|
||||||
ki = kline_map.get(d["date"], {})
|
ki = kline_map.get(d["date"], {})
|
||||||
if d.get("turnover", 0) == 0:
|
if d.get("turnover", 0) == 0:
|
||||||
d["turnover"] = ki.get("turnover", 0)
|
# 优先从东方财富 kline 拿成交额,其次腾讯
|
||||||
|
d["turnover"] = em_kline_map.get(d["date"], ki.get("turnover", 0))
|
||||||
if d.get("closePrice", 0) == 0:
|
if d.get("closePrice", 0) == 0:
|
||||||
d["closePrice"] = ki.get("close", 0)
|
d["closePrice"] = ki.get("close", 0)
|
||||||
if d.get("changePercent", 0) == 0:
|
if d.get("changePercent", 0) == 0:
|
||||||
@@ -238,3 +253,19 @@ async def stock_fund_flow(
|
|||||||
"negativeDays": negative,
|
"negativeDays": negative,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/mx-tool", summary="MX 通用金融数据查询")
|
||||||
|
async def mx_tool(
|
||||||
|
query: str = Query(..., description="自然语言问句,如:「格力电器2024年净利润」「沪深300最新收盘价」「市盈率最低的50只股票」"),
|
||||||
|
):
|
||||||
|
"""通过 MX 妙想 API 查询任意金融数据(A股/港股/美股/基金/债券/指数/板块/宏观/新闻/公告/选股等)
|
||||||
|
|
||||||
|
单次查询最多支持 20 只证券。返回原始结构化表格(sheetName + columns + items)。
|
||||||
|
"""
|
||||||
|
if not query or not query.strip():
|
||||||
|
raise HTTPException(status_code=400, detail="查询内容不能为空")
|
||||||
|
data = await eastmoney.fetch_mx_tool(query.strip())
|
||||||
|
if not data:
|
||||||
|
raise HTTPException(status_code=404, detail="MX 查询无结果或所有 API Key 已耗尽")
|
||||||
|
return {"data": data}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
"""题材数据路由:题材列表、题材详情、题材相关股票"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Query, HTTPException
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from services import themes
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
# 上游反代/CDN 可能按 path 缓存,显式禁止缓存
|
||||||
|
_NO_CACHE_HEADERS = {"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", summary="题材列表")
|
||||||
|
async def theme_list(
|
||||||
|
sort_field: int = Query(1, ge=1, le=5, description="排序字段:1=涨幅 3=强度 4=热度排名 5=成交额"),
|
||||||
|
asc: bool = Query(False, description="True=升序, False=降序"),
|
||||||
|
):
|
||||||
|
if sort_field not in (1, 3, 4, 5):
|
||||||
|
raise HTTPException(status_code=400, detail="排序字段仅支持 1/3/4/5")
|
||||||
|
|
||||||
|
data = await themes.fetch_theme_list(sort_field, asc)
|
||||||
|
return JSONResponse(
|
||||||
|
{"data": data, "count": len(data), "sort_field": sort_field, "asc": asc},
|
||||||
|
headers=_NO_CACHE_HEADERS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/graph", summary="热点穿透:题材-股票网状关系图")
|
||||||
|
async def theme_graph(
|
||||||
|
sort_field: int = Query(1, description="题材排序:1=涨幅 4=热度"),
|
||||||
|
top: int = Query(30, ge=1, le=60, description="题材数量"),
|
||||||
|
limit: int = Query(1000, ge=100, le=2000, description="下发的股票节点上限(按穿透度取前 N 只)"),
|
||||||
|
):
|
||||||
|
if sort_field not in (1, 4):
|
||||||
|
raise HTTPException(status_code=400, detail="排序字段仅支持 1(涨幅)/4(热度)")
|
||||||
|
|
||||||
|
result = await themes.fetch_theme_graph(sort_field, top, limit)
|
||||||
|
return JSONResponse(result, headers=_NO_CACHE_HEADERS)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{theme_code}/detail", summary="题材详情")
|
||||||
|
async def theme_detail(theme_code: str):
|
||||||
|
data = await themes.fetch_theme_detail(theme_code)
|
||||||
|
if not data:
|
||||||
|
return JSONResponse(
|
||||||
|
{"data": None, "theme_code": theme_code},
|
||||||
|
headers=_NO_CACHE_HEADERS,
|
||||||
|
)
|
||||||
|
return JSONResponse(
|
||||||
|
{"data": data, "theme_code": theme_code},
|
||||||
|
headers=_NO_CACHE_HEADERS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{theme_code}/stocks", summary="题材相关股票(全部)")
|
||||||
|
async def theme_stocks(theme_code: str):
|
||||||
|
result = await themes.fetch_theme_stocks(theme_code)
|
||||||
|
return JSONResponse(
|
||||||
|
{
|
||||||
|
"data": result.get("stockList", []),
|
||||||
|
"statistic": result.get("statistic", {}),
|
||||||
|
"total": result.get("total", 0),
|
||||||
|
"theme_code": theme_code,
|
||||||
|
},
|
||||||
|
headers=_NO_CACHE_HEADERS,
|
||||||
|
)
|
||||||
@@ -23,9 +23,9 @@ def get_cache(key: str) -> Optional[str]:
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def set_cache(key: str, value: str, ttl_hours: int = 6):
|
def set_cache(key: str, value: str, ttl_hours: int = 6, ttl_seconds: int = 0):
|
||||||
"""写入缓存,过期时间 = now + ttl_hours"""
|
"""写入缓存,过期时间 = now + ttl_hours + ttl_seconds(支持秒级短 TTL)"""
|
||||||
expires_at = (datetime.now() + timedelta(hours=ttl_hours)).isoformat()
|
expires_at = (datetime.now() + timedelta(hours=ttl_hours, seconds=ttl_seconds)).isoformat()
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
try:
|
try:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
|
|||||||
+150
-35
@@ -206,6 +206,75 @@ async def fetch_mx_api(name: str, days: int) -> Optional[List[dict]]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_mx_tool(tool_query: str) -> Optional[dict]:
|
||||||
|
"""通用 MX 妙想 API 查询(自然语言 → 结构化数据,缓存 6 小时)
|
||||||
|
|
||||||
|
支持东方财富数据库的全品类查询,包括但不限于:
|
||||||
|
- A 股/港股/美股行情、财务、估值、股本、事件
|
||||||
|
- 基金净值、收益、持仓、排名
|
||||||
|
- 债券基本信息、久期凸性、信用评级
|
||||||
|
- 指数与板块行情、技术指标
|
||||||
|
- 宏观经济/行业经济/大宗商品数据
|
||||||
|
- 新闻研报、公告搜索
|
||||||
|
- 多条件选股/选基/选债
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tool_query: 自然语言问句,如 "格力电器2024年营业收入和净利润"
|
||||||
|
"沪深300最新收盘价" "市盈率最低的50只股票"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MX API 原始 JSON(status=0 时成功,data 中含 sheetName/columns/items)
|
||||||
|
失败返回 None
|
||||||
|
"""
|
||||||
|
cache_key = f"mx_tool:{tool_query}"
|
||||||
|
cached = get_cache(cache_key)
|
||||||
|
if cached is not None:
|
||||||
|
return json.loads(cached)
|
||||||
|
|
||||||
|
if not _ensure_keys():
|
||||||
|
return None
|
||||||
|
|
||||||
|
url = "https://mkapi2.dfcfs.com/finskillshub/api/claw/query"
|
||||||
|
payload = {"toolQuery": tool_query}
|
||||||
|
|
||||||
|
for attempt in range(len(_api_keys)):
|
||||||
|
key = _get_key()
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
try:
|
||||||
|
resp = await client.post(
|
||||||
|
url,
|
||||||
|
json=payload,
|
||||||
|
headers={"Content-Type": "application/json", "apikey": key},
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
_rotate_key()
|
||||||
|
continue
|
||||||
|
result = resp.json()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[eastmoney] MX tool error: {e}")
|
||||||
|
_rotate_key()
|
||||||
|
continue
|
||||||
|
|
||||||
|
status = result.get("status", -1)
|
||||||
|
if status == 113:
|
||||||
|
print(f"[eastmoney] key 已达每日上限,切换到下一个")
|
||||||
|
_rotate_key()
|
||||||
|
continue
|
||||||
|
if status == 114:
|
||||||
|
_rotate_key()
|
||||||
|
continue
|
||||||
|
if status != 0:
|
||||||
|
_rotate_key()
|
||||||
|
continue
|
||||||
|
|
||||||
|
set_cache(cache_key, json.dumps(result, ensure_ascii=False), ttl_hours=6)
|
||||||
|
return result
|
||||||
|
|
||||||
|
print(f"[eastmoney] 所有 MX API key 均已耗尽")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_eastmoney_market(code: str) -> str:
|
def get_eastmoney_market(code: str) -> str:
|
||||||
"""获取东方财富格式的市场标识"""
|
"""获取东方财富格式的市场标识"""
|
||||||
if code.startswith("688"):
|
if code.startswith("688"):
|
||||||
@@ -318,14 +387,14 @@ def _get_sector_session() -> AsyncSession:
|
|||||||
global _sector_session
|
global _sector_session
|
||||||
if _sector_session is None:
|
if _sector_session is None:
|
||||||
_sector_session = AsyncSession(
|
_sector_session = AsyncSession(
|
||||||
impersonate="chrome120",
|
impersonate="chrome131",
|
||||||
headers={
|
headers={
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||||
"Referer": "https://data.eastmoney.com/bkzj/hy.html",
|
"Referer": "https://data.eastmoney.com/bkzj/hy.html",
|
||||||
"Accept": "*/*",
|
"Accept": "*/*",
|
||||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||||
},
|
},
|
||||||
timeout=10,
|
timeout=5,
|
||||||
)
|
)
|
||||||
return _sector_session
|
return _sector_session
|
||||||
|
|
||||||
@@ -336,7 +405,7 @@ _SECTOR_MEM_TTL = 60
|
|||||||
|
|
||||||
|
|
||||||
async def fetch_sector_list(sector_type: str) -> list[dict]:
|
async def fetch_sector_list(sector_type: str) -> list[dict]:
|
||||||
# 1. 内存缓存(仅交易时段有效,60s 避免重复请求)
|
# 1. 内存缓存
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if sector_type in _sector_cache:
|
if sector_type in _sector_cache:
|
||||||
data, ts = _sector_cache[sector_type]
|
data, ts = _sector_cache[sector_type]
|
||||||
@@ -345,7 +414,7 @@ async def fetch_sector_list(sector_type: str) -> list[dict]:
|
|||||||
|
|
||||||
cache_key = f"sector_list:{sector_type}"
|
cache_key = f"sector_list:{sector_type}"
|
||||||
|
|
||||||
# 2. 非交易时段:走磁盘持久缓存,不请求 API
|
# 2. 非交易时段:走磁盘持久缓存
|
||||||
if not _is_trading_time():
|
if not _is_trading_time():
|
||||||
cached = get_cache(cache_key)
|
cached = get_cache(cache_key)
|
||||||
if cached is not None:
|
if cached is not None:
|
||||||
@@ -353,21 +422,28 @@ async def fetch_sector_list(sector_type: str) -> list[dict]:
|
|||||||
_sector_cache[sector_type] = (data, now)
|
_sector_cache[sector_type] = (data, now)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
# 3. 请求 API
|
# 3. 并发请求 push2 和 akshare,优先使用 push2
|
||||||
data = await _fetch_push2(sector_type)
|
push2_task = asyncio.create_task(_fetch_push2(sector_type))
|
||||||
if data:
|
akshare_task = asyncio.create_task(_fetch_akshare(sector_type))
|
||||||
_sector_cache[sector_type] = (data, now)
|
|
||||||
ttl = _sector_ttl_hours()
|
|
||||||
# 交易时段 ttl=0 → 不写磁盘(内存缓存已够)
|
|
||||||
if ttl > 0:
|
|
||||||
set_cache(cache_key, json.dumps(data, ensure_ascii=False), ttl_hours=ttl)
|
|
||||||
return data
|
|
||||||
|
|
||||||
data = await _fetch_akshare(sector_type)
|
push2_data = await push2_task
|
||||||
if data:
|
if push2_data:
|
||||||
_sector_cache[sector_type] = (data, now)
|
akshare_task.cancel()
|
||||||
# akshare 数据源更新不确定,不写入磁盘缓存
|
try:
|
||||||
return data
|
await akshare_task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
_sector_cache[sector_type] = (push2_data, now)
|
||||||
|
ttl = _sector_ttl_hours()
|
||||||
|
if ttl > 0:
|
||||||
|
set_cache(cache_key, json.dumps(push2_data, ensure_ascii=False), ttl_hours=ttl)
|
||||||
|
return push2_data
|
||||||
|
|
||||||
|
# push2 失败,用 akshare(不写磁盘缓存)
|
||||||
|
akshare_data = await akshare_task
|
||||||
|
if akshare_data:
|
||||||
|
_sector_cache[sector_type] = (akshare_data, now)
|
||||||
|
return akshare_data
|
||||||
|
|
||||||
|
|
||||||
async def _fetch_push2(sector_type: str) -> list[dict]:
|
async def _fetch_push2(sector_type: str) -> list[dict]:
|
||||||
@@ -378,14 +454,14 @@ async def _fetch_push2(sector_type: str) -> list[dict]:
|
|||||||
|
|
||||||
session = _get_sector_session()
|
session = _get_sector_session()
|
||||||
ut = await get_em_ut()
|
ut = await get_em_ut()
|
||||||
url = (
|
|
||||||
f"https://push2.eastmoney.com/api/qt/clist/get"
|
|
||||||
f"?fs={fs}&fields={SECTOR_FIELDS}"
|
|
||||||
f"&fid=f62&po=1&pz=500&pn=1&np=1&fltt=2"
|
|
||||||
f"&invt=2&ut={ut}"
|
|
||||||
)
|
|
||||||
|
|
||||||
for attempt in range(2):
|
for attempt in range(2):
|
||||||
|
url = (
|
||||||
|
f"https://push2.eastmoney.com/api/qt/clist/get"
|
||||||
|
f"?fs={fs}&fields={SECTOR_FIELDS}"
|
||||||
|
f"&fid=f62&po=1&pz=500&pn=1&np=1&fltt=2"
|
||||||
|
f"&invt=2&ut={ut}"
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
resp = await session.get(url)
|
resp = await session.get(url)
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
@@ -425,6 +501,11 @@ async def _fetch_push2(sector_type: str) -> list[dict]:
|
|||||||
if "disconnect" in err.lower() or "refused" in err.lower() or attempt == 1:
|
if "disconnect" in err.lower() or "refused" in err.lower() or attempt == 1:
|
||||||
await get_em_ut(force_refresh=True)
|
await get_em_ut(force_refresh=True)
|
||||||
ut = _em_ut
|
ut = _em_ut
|
||||||
|
# 先尝试更新现有会话的 headers
|
||||||
|
try:
|
||||||
|
session.headers.update({"Referer": "https://data.eastmoney.com/bkzj/hy.html"})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
# 重建会话(TLS 指纹可能会被缓存)
|
# 重建会话(TLS 指纹可能会被缓存)
|
||||||
global _sector_session
|
global _sector_session
|
||||||
_sector_session = None
|
_sector_session = None
|
||||||
@@ -438,10 +519,12 @@ import akshare as ak
|
|||||||
|
|
||||||
|
|
||||||
async def _fetch_akshare(sector_type: str) -> list[dict]:
|
async def _fetch_akshare(sector_type: str) -> list[dict]:
|
||||||
"""akshare 降级方案(同花顺数据源)"""
|
"""akshare 降级方案(东方财富数据源)"""
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
code_map_key = f"board_codes:{sector_type}"
|
||||||
|
|
||||||
def _get():
|
def _build_code_map():
|
||||||
|
"""获取板块代码映射(HTTP 较慢,结果单独缓存 24h)"""
|
||||||
code_map = {}
|
code_map = {}
|
||||||
try:
|
try:
|
||||||
if sector_type == "industry":
|
if sector_type == "industry":
|
||||||
@@ -462,16 +545,34 @@ async def _fetch_akshare(sector_type: str) -> list[dict]:
|
|||||||
code_map[str(r.get("name", ""))] = str(r.get("code", ""))
|
code_map[str(r.get("name", ""))] = str(r.get("code", ""))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
return code_map
|
||||||
|
|
||||||
|
def _get_fund_flow():
|
||||||
if sector_type == "industry":
|
if sector_type == "industry":
|
||||||
df = ak.stock_fund_flow_industry()
|
return ak.stock_fund_flow_industry()
|
||||||
else:
|
else:
|
||||||
df = ak.stock_fund_flow_concept()
|
return ak.stock_fund_flow_concept()
|
||||||
|
|
||||||
return code_map, df
|
# 1. 尝试从缓存读取 code_map
|
||||||
|
code_map = {}
|
||||||
|
cached_map = get_cache(code_map_key)
|
||||||
|
if cached_map is not None:
|
||||||
|
code_map = json.loads(cached_map)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
code_map, df = await loop.run_in_executor(None, _get)
|
if code_map:
|
||||||
|
# 已有缓存,只需获取资金流
|
||||||
|
df = await loop.run_in_executor(None, _get_fund_flow)
|
||||||
|
else:
|
||||||
|
# 首次:code_map + 资金流并发获取
|
||||||
|
map_data, df = await asyncio.gather(
|
||||||
|
loop.run_in_executor(None, _build_code_map),
|
||||||
|
loop.run_in_executor(None, _get_fund_flow),
|
||||||
|
)
|
||||||
|
if map_data:
|
||||||
|
code_map = map_data
|
||||||
|
set_cache(code_map_key, json.dumps(code_map, ensure_ascii=False), ttl_hours=24)
|
||||||
|
|
||||||
if df is None or df.empty:
|
if df is None or df.empty:
|
||||||
return []
|
return []
|
||||||
df = df.sort_values("净额", ascending=False)
|
df = df.sort_values("净额", ascending=False)
|
||||||
@@ -934,7 +1035,6 @@ async def fetch_kline_history(code: str, days: int = 90) -> Optional[list[dict]]
|
|||||||
"turnoverRate": _f(10),
|
"turnoverRate": _f(10),
|
||||||
})
|
})
|
||||||
|
|
||||||
parsed.reverse()
|
|
||||||
return parsed
|
return parsed
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if attempt == 0:
|
if attempt == 0:
|
||||||
@@ -963,16 +1063,31 @@ async def fetch_fund_flow_daykline(code: str, days: int) -> Optional[list[dict]]
|
|||||||
"Referer": "https://quote.eastmoney.com/",
|
"Referer": "https://quote.eastmoney.com/",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
from curl_cffi.requests import AsyncSession
|
||||||
|
|
||||||
for attempt in range(2):
|
for attempt in range(2):
|
||||||
async with httpx.AsyncClient() as client:
|
async with AsyncSession(impersonate="chrome120") as session:
|
||||||
try:
|
try:
|
||||||
resp = await client.get(url, headers=headers, timeout=10)
|
resp = await session.get(url, headers=headers, timeout=10)
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
if attempt == 0:
|
if attempt == 0:
|
||||||
continue
|
continue
|
||||||
return None
|
return None
|
||||||
result = resp.json()
|
result = resp.json()
|
||||||
klines = result.get("data", {}).get("klines", [])
|
if not isinstance(result, dict):
|
||||||
|
if attempt == 0:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
if result.get("rc", -1) != 0:
|
||||||
|
if attempt == 0:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
data = result.get("data")
|
||||||
|
if not data or not isinstance(data, dict):
|
||||||
|
if attempt == 0:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
klines = data.get("klines", [])
|
||||||
if not klines:
|
if not klines:
|
||||||
if attempt == 0:
|
if attempt == 0:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
"""通达信数据源(mootdx TCP直连通达信服务器)
|
||||||
|
|
||||||
|
通过 TCP 协议直连通达信行情服务器,不走 HTTP,不会被限流。
|
||||||
|
主要用途:
|
||||||
|
- K线数据:主数据源(稳定可靠)
|
||||||
|
- 板块数据:东方财富 push2 的降级方案
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
|
||||||
|
def _get_market(code: str) -> str:
|
||||||
|
return "sh" if code.startswith("6") else "sz"
|
||||||
|
|
||||||
|
|
||||||
|
def _create_client():
|
||||||
|
from mootdx.quotes import Quotes
|
||||||
|
|
||||||
|
return Quotes.factory(market="std", multithread=True, heartbeat=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_fetch_kline(code: str, days: int) -> Optional[list]:
|
||||||
|
client = _create_client()
|
||||||
|
|
||||||
|
klines = client.bars(symbol=code, frequency=9, offset=min(days, 800))
|
||||||
|
if klines is None or len(klines) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
result = []
|
||||||
|
prev_close = 0.0
|
||||||
|
for bar in reversed(klines):
|
||||||
|
close = float(bar.close)
|
||||||
|
change_pct = 0
|
||||||
|
if prev_close > 0:
|
||||||
|
change_pct = (close - prev_close) / prev_close * 100
|
||||||
|
|
||||||
|
result.append(
|
||||||
|
{
|
||||||
|
"date": (
|
||||||
|
bar.datetime.strftime("%Y-%m-%d")
|
||||||
|
if hasattr(bar.datetime, "strftime")
|
||||||
|
else str(bar.datetime)[:10]
|
||||||
|
),
|
||||||
|
"open": float(bar.open),
|
||||||
|
"close": close,
|
||||||
|
"high": float(bar.high),
|
||||||
|
"low": float(bar.low),
|
||||||
|
"volume": int(bar.vol) if hasattr(bar, "vol") else 0,
|
||||||
|
"turnover": (
|
||||||
|
float(bar.amount) if hasattr(bar, "amount") and bar.amount else 0
|
||||||
|
),
|
||||||
|
"changePercent": round(change_pct, 2),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
prev_close = close
|
||||||
|
|
||||||
|
result.sort(key=lambda x: x["date"])
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_kline_history(code: str, days: int = 90) -> Optional[List[dict]]:
|
||||||
|
"""获取日K线(TCP直连通达信,主数据源)"""
|
||||||
|
try:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
return await loop.run_in_executor(None, _sync_fetch_kline, code, days)
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[mootdx] fetch_kline error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 板块数据(东方财富降级方案)----
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_fetch_sectors(sector_type: str) -> Optional[list]:
|
||||||
|
from mootdx.consts import MARKET_SH, MARKET_SZ
|
||||||
|
|
||||||
|
client = _create_client()
|
||||||
|
|
||||||
|
# block() 返回 DataFrame,列:code, name 等
|
||||||
|
# 按板块类型过滤
|
||||||
|
block_df = client.block()
|
||||||
|
if block_df is None or block_df.empty:
|
||||||
|
return None
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for _, row in block_df.iterrows():
|
||||||
|
name = str(row.get("name", "") or row.get("blockname", ""))
|
||||||
|
code = str(row.get("code", "") or row.get("blockcode", ""))
|
||||||
|
if not code or not name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
items.append(
|
||||||
|
{
|
||||||
|
"code": code,
|
||||||
|
"name": name,
|
||||||
|
"level": None,
|
||||||
|
"changePercent": None,
|
||||||
|
"changeAmount": None,
|
||||||
|
"mainNetInflow": 0,
|
||||||
|
"mainNetInflowPercent": None,
|
||||||
|
"superLargeInflow": None,
|
||||||
|
"superLargeInflowPercent": None,
|
||||||
|
"largeInflow": None,
|
||||||
|
"largeInflowPercent": None,
|
||||||
|
"mediumInflow": None,
|
||||||
|
"mediumInflowPercent": None,
|
||||||
|
"smallInflow": None,
|
||||||
|
"smallInflowPercent": None,
|
||||||
|
"turnover": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return items if items else None
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_sector_list(sector_type: str) -> Optional[List[dict]]:
|
||||||
|
"""获取板块列表(东方财富的降级方案,仅含代码和名称)"""
|
||||||
|
try:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
return await loop.run_in_executor(None, _sync_fetch_sectors, sector_type)
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[mootdx] fetch_sectors error: {e}")
|
||||||
|
return None
|
||||||
@@ -142,10 +142,11 @@ async def fetch_quote(code: str) -> Optional[dict]:
|
|||||||
return None
|
return None
|
||||||
text = resp.content.decode("gbk", errors="replace")
|
text = resp.content.decode("gbk", errors="replace")
|
||||||
parts = parse_tencent_data(text)
|
parts = parse_tencent_data(text)
|
||||||
if not parts or len(parts) < 38:
|
if not parts or len(parts) < 47:
|
||||||
return None
|
return None
|
||||||
# 字段索引(1-based):1=名称, 3=当前价, 4=昨收, 5=今开, 6=成交量(手)
|
# 字段索引:1=名称, 3=当前价, 4=昨收, 5=今开, 6=成交量(手)
|
||||||
# 7=外盘, 8=内盘, 31=涨跌额, 32=涨跌幅%, 33=最高, 34=最低, 37=成交额(万)
|
# 7=外盘, 8=内盘, 31=涨跌额, 32=涨跌幅%, 33=最高, 34=最低, 37=成交额(万)
|
||||||
|
# 38=换手率%, 39=市盈率, 43=振幅%, 44=流通市值(亿), 45=总市值(亿), 46=市净率
|
||||||
name = parts[1] or ""
|
name = parts[1] or ""
|
||||||
current_price = float(parts[3]) if parts[3] else 0
|
current_price = float(parts[3]) if parts[3] else 0
|
||||||
yesterday_close = float(parts[4]) if parts[4] else 0
|
yesterday_close = float(parts[4]) if parts[4] else 0
|
||||||
@@ -158,6 +159,12 @@ async def fetch_quote(code: str) -> Optional[dict]:
|
|||||||
change_pct = float(parts[32]) if parts[32] else 0
|
change_pct = float(parts[32]) if parts[32] else 0
|
||||||
outer_disk = float(parts[7]) if parts[7] else 0
|
outer_disk = float(parts[7]) if parts[7] else 0
|
||||||
inner_disk = float(parts[8]) if parts[8] else 0
|
inner_disk = float(parts[8]) if parts[8] else 0
|
||||||
|
turnover_rate = float(parts[38]) if parts[38] else 0 # 换手率%
|
||||||
|
pe = float(parts[39]) if parts[39] else 0 # 市盈率
|
||||||
|
# 腾讯API返回的市值单位是亿, formatMoney期望元, 需×1e8
|
||||||
|
total_market_cap = float(parts[45]) * 1e8 if parts[45] else 0 # 总市值(亿→元)
|
||||||
|
circulating_market_cap = float(parts[44]) * 1e8 if parts[44] else 0 # 流通市值(亿→元)
|
||||||
|
pb = float(parts[46]) if parts[46] else 0 # 市净率
|
||||||
|
|
||||||
if not name or current_price == 0:
|
if not name or current_price == 0:
|
||||||
return None
|
return None
|
||||||
@@ -186,6 +193,11 @@ async def fetch_quote(code: str) -> Optional[dict]:
|
|||||||
"time": now.strftime("%H:%M:%S"),
|
"time": now.strftime("%H:%M:%S"),
|
||||||
"change": round(change, 2),
|
"change": round(change, 2),
|
||||||
"changePercent": round(change_percent, 2),
|
"changePercent": round(change_percent, 2),
|
||||||
|
"turnoverRate": round(turnover_rate, 2),
|
||||||
|
"pe": round(pe, 2) if pe else 0,
|
||||||
|
"pb": round(pb, 2) if pb else 0,
|
||||||
|
"totalMarketCap": round(total_market_cap, 2),
|
||||||
|
"circulatingMarketCap": round(circulating_market_cap, 2),
|
||||||
}
|
}
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -0,0 +1,432 @@
|
|||||||
|
"""东方财富题材数据服务:题材列表、题材详情、题材相关股票
|
||||||
|
|
||||||
|
逆向自 emrnweb.eastmoney.com/investment 的 H5 接口:
|
||||||
|
- 题材列表: POST https://emcfgdata.eastmoney.com/api/themeInvest/getThemeList
|
||||||
|
- 题材详情: GET https://emcfgdata.securities.eastmoney.com/api/themeInvest/getDetail/{themeCode}
|
||||||
|
- 相关股票: POST https://emcfgdata.eastmoney.com/api/themeInvest/getStockList
|
||||||
|
|
||||||
|
两个 POST 接口需要「移动端包装结构」:
|
||||||
|
{args:{...业务参数}, appKey, client, clientVersion, clientType, randomCode, timestamp}
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import time
|
||||||
|
from datetime import datetime, time as dtime, timedelta, timezone
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from services.cache import get_cache, set_cache
|
||||||
|
|
||||||
|
# ---- 东方财富移动端配置中心域名 ----
|
||||||
|
|
||||||
|
_PZ_URL = "https://emcfgdata.eastmoney.com"
|
||||||
|
_PZ_CDN_URL = "https://emcfgdata.securities.eastmoney.com"
|
||||||
|
|
||||||
|
# appKey 与页面场景对应:题材列表索引页 / 题材详情页
|
||||||
|
_APP_KEY_INDEX = "rn-themeIndex"
|
||||||
|
_APP_KEY_DETAIL = "rn-themeDetail"
|
||||||
|
|
||||||
|
_HEADERS = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||||
|
"Origin": "https://emrnweb.eastmoney.com",
|
||||||
|
"Referer": "https://emrnweb.eastmoney.com/",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- 交易时段感知缓存 ----
|
||||||
|
|
||||||
|
_CST = timezone(timedelta(hours=8)) # 北京时间
|
||||||
|
_TRADING_MORNING = (dtime(9, 30), dtime(11, 30))
|
||||||
|
_TRADING_AFTERNOON = (dtime(13, 0), dtime(15, 0))
|
||||||
|
|
||||||
|
|
||||||
|
def _is_trading_time() -> bool:
|
||||||
|
"""判断当前是否为 A 股交易时段(周一至周五 9:30-11:30 / 13:00-15:00)"""
|
||||||
|
now = datetime.now(_CST)
|
||||||
|
if now.weekday() >= 5:
|
||||||
|
return False
|
||||||
|
t = now.time()
|
||||||
|
return (_TRADING_MORNING[0] <= t <= _TRADING_MORNING[1]
|
||||||
|
or _TRADING_AFTERNOON[0] <= t <= _TRADING_AFTERNOON[1])
|
||||||
|
|
||||||
|
|
||||||
|
def _next_open_delta_seconds() -> int:
|
||||||
|
"""非交易时段写入的缓存距下次开盘的秒数。
|
||||||
|
|
||||||
|
缓存只允许存活到下一次开盘(早盘 9:30 / 午休后 13:00)前一刻,
|
||||||
|
保证交易日开盘后缓存必然过期并实时拉取,不会读到上个交易日写入的旧数据。
|
||||||
|
"""
|
||||||
|
now = datetime.now(_CST)
|
||||||
|
# 午休 11:30-13:00 → 截止今天 13:00
|
||||||
|
if _TRADING_MORNING[1] < now.time() < _TRADING_AFTERNOON[0]:
|
||||||
|
open_dt = now.replace(hour=13, minute=0, second=0, microsecond=0)
|
||||||
|
return max(0, int((open_dt - now).total_seconds()))
|
||||||
|
# 其余非交易时段(早盘前 / 收盘后 / 周末 / 节假日)→ 下一个工作日 9:30
|
||||||
|
for days in range(0, 8):
|
||||||
|
d = (now + timedelta(days=days)).date()
|
||||||
|
if d.weekday() >= 5: # 跳过周末
|
||||||
|
continue
|
||||||
|
open_dt = datetime(d.year, d.month, d.day, 9, 30, tzinfo=_CST)
|
||||||
|
if open_dt > now:
|
||||||
|
return max(0, int((open_dt - now).total_seconds()))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _list_ttl_seconds() -> int:
|
||||||
|
"""题材列表缓存秒数:交易时段 0(不缓存、实时拉取);非交易时段缓存到下次开盘前失效"""
|
||||||
|
return 0 if _is_trading_time() else _next_open_delta_seconds()
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 请求封装 ----
|
||||||
|
|
||||||
|
def _build_payload(args: Optional[dict] = None, app_key: str = _APP_KEY_INDEX) -> dict:
|
||||||
|
"""构建东方财富移动端请求包装结构"""
|
||||||
|
return {
|
||||||
|
"args": args or {},
|
||||||
|
"appKey": app_key,
|
||||||
|
"client": "iOS",
|
||||||
|
"clientVersion": "8.3",
|
||||||
|
"clientType": "cfw",
|
||||||
|
"randomCode": "".join(random.choices(string.ascii_uppercase + string.ascii_lowercase + string.digits, k=16)),
|
||||||
|
"timestamp": int(time.time() * 1000),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _post(path: str, args: dict, app_key: str = _APP_KEY_INDEX) -> Optional[dict]:
|
||||||
|
"""POST 到配置中心接口,返回 data 层 JSON"""
|
||||||
|
payload = _build_payload(args, app_key)
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=15) as client:
|
||||||
|
resp = await client.post(_PZ_URL + path, json=payload, headers=_HEADERS)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
return None
|
||||||
|
body = resp.json()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[themes] POST {path} 失败: {e}")
|
||||||
|
return None
|
||||||
|
if body.get("code") != 0:
|
||||||
|
print(f"[themes] POST {path} 返回错误: {body.get('message')}")
|
||||||
|
return None
|
||||||
|
return body.get("data")
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_cdn(path: str, app_key: str = _APP_KEY_DETAIL) -> Optional[dict]:
|
||||||
|
"""GET 到配置中心 CDN 接口(题材详情),data 包装结构放 query 参数"""
|
||||||
|
payload = _build_payload({}, app_key)
|
||||||
|
params = {"data": json.dumps(payload, ensure_ascii=False)}
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=15) as client:
|
||||||
|
resp = await client.get(_PZ_CDN_URL + path, params=params, headers=_HEADERS)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
return None
|
||||||
|
body = resp.json()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[themes] GET {path} 失败: {e}")
|
||||||
|
return None
|
||||||
|
if body.get("code") != 0:
|
||||||
|
print(f"[themes] GET {path} 返回错误: {body.get('message')}")
|
||||||
|
return None
|
||||||
|
return body.get("data")
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 题材列表 ----
|
||||||
|
|
||||||
|
# sortField 映射(题材列表页):1=涨幅(bf3) 3=强度(strengthValue) 4=热度排名(hotRank) 5=成交额(fex5)
|
||||||
|
_LIST_PAGE_SIZE = 500
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_theme_list(sort_field: int = 1, asc: bool = False) -> list[dict]:
|
||||||
|
"""获取全部题材列表(内部循环分页拉全,约 623 个,最多 2 页)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sort_field: 排序字段 1/3/4/5
|
||||||
|
asc: True=升序, False=降序
|
||||||
|
"""
|
||||||
|
cache_key = f"theme_list:{sort_field}:{asc}"
|
||||||
|
# 交易时段强制实时:跳过缓存读取,避免命中非交易时段写入的上个交易日旧数据
|
||||||
|
if not _is_trading_time():
|
||||||
|
cached = get_cache(cache_key)
|
||||||
|
if cached is not None:
|
||||||
|
return json.loads(cached)
|
||||||
|
|
||||||
|
sort = 1 if asc else -1
|
||||||
|
# hotRank 数值越小越热,"热度降序(最热在前)" 需反转为接口升序
|
||||||
|
if sort_field == 4:
|
||||||
|
sort = -sort
|
||||||
|
items: list[dict] = []
|
||||||
|
page = 1
|
||||||
|
total = None
|
||||||
|
|
||||||
|
for _ in range(5): # 安全上限
|
||||||
|
data = await _post(
|
||||||
|
"/api/themeInvest/getThemeList",
|
||||||
|
{"pageSize": _LIST_PAGE_SIZE, "pageNum": page, "sort": sort, "sortField": sort_field},
|
||||||
|
)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
if total is None:
|
||||||
|
total = data.get("total", 0)
|
||||||
|
page_items = data.get("list", [])
|
||||||
|
if not page_items:
|
||||||
|
break
|
||||||
|
items.extend(page_items)
|
||||||
|
if len(items) >= total:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
if items:
|
||||||
|
ttl_s = _list_ttl_seconds()
|
||||||
|
if ttl_s > 0:
|
||||||
|
set_cache(cache_key, json.dumps(items, ensure_ascii=False), ttl_seconds=ttl_s)
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 题材详情 ----
|
||||||
|
|
||||||
|
async def fetch_theme_detail(theme_code: str) -> Optional[dict]:
|
||||||
|
"""获取题材详情(简介 + 热点事件 + 相关新闻),缓存 1 小时"""
|
||||||
|
cache_key = f"theme_detail:{theme_code}"
|
||||||
|
cached = get_cache(cache_key)
|
||||||
|
if cached is not None:
|
||||||
|
return json.loads(cached)
|
||||||
|
|
||||||
|
data = await _get_cdn(f"/api/themeInvest/getDetail/{theme_code}", app_key=_APP_KEY_DETAIL)
|
||||||
|
if not data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
set_cache(cache_key, json.dumps(data, ensure_ascii=False), ttl_hours=1)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 题材相关股票 ----
|
||||||
|
|
||||||
|
_STOCK_PAGE_SIZE = 100
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_theme_stocks(theme_code: str) -> dict:
|
||||||
|
"""获取题材下全部相关股票(分页拉全),返回 {stockList, statistic, total}"""
|
||||||
|
cache_key = f"theme_stocks:{theme_code}"
|
||||||
|
cached = get_cache(cache_key)
|
||||||
|
if cached is not None:
|
||||||
|
return json.loads(cached)
|
||||||
|
|
||||||
|
stock_list: list[dict] = []
|
||||||
|
statistic = {}
|
||||||
|
total = 0
|
||||||
|
page = 1
|
||||||
|
|
||||||
|
for _ in range(20): # 安全上限
|
||||||
|
data = await _post(
|
||||||
|
"/api/themeInvest/getStockList",
|
||||||
|
{"themeCode": theme_code, "pageSize": _STOCK_PAGE_SIZE, "pageNum": page, "sort": -1, "sortField": "f3"},
|
||||||
|
app_key=_APP_KEY_DETAIL,
|
||||||
|
)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
if not statistic and data.get("statistic"):
|
||||||
|
statistic = data["statistic"]
|
||||||
|
page_items = data.get("stockList", [])
|
||||||
|
if not page_items:
|
||||||
|
break
|
||||||
|
stock_list.extend(page_items)
|
||||||
|
total = data.get("total", 0)
|
||||||
|
if len(stock_list) >= total:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
result = {"stockList": stock_list, "statistic": statistic, "total": total}
|
||||||
|
if stock_list:
|
||||||
|
ttl_s = _graph_ttl_seconds()
|
||||||
|
if ttl_s > 0:
|
||||||
|
set_cache(cache_key, json.dumps(result, ensure_ascii=False), ttl_seconds=ttl_s)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 热点穿透:题材-股票 网状关系图数据 ----
|
||||||
|
|
||||||
|
# 合并采样:涨幅榜 Top N + 热度榜 Top N 去重合并,
|
||||||
|
# 避免单一榜单导致股票覆盖题材数被低估(如有研新材覆盖 10+ 题材,仅涨幅榜只能采到 2 个)。
|
||||||
|
|
||||||
|
# 盘中图聚合结果/题材股票子层的缓存秒数。交易时段数据波动快,用 60s 短缓存;
|
||||||
|
# 非交易时段缓存 18 小时(覆盖到下一交易日)。
|
||||||
|
_GRAPH_CACHE_SECONDS = 60
|
||||||
|
|
||||||
|
|
||||||
|
def _graph_ttl_seconds() -> int:
|
||||||
|
"""图聚合结果与题材股票子层的缓存秒数:盘中 60 秒;非盘中缓存到下次开盘前失效"""
|
||||||
|
return _GRAPH_CACHE_SECONDS if _is_trading_time() else _next_open_delta_seconds()
|
||||||
|
|
||||||
|
|
||||||
|
# 后台重建锁:cache_key -> asyncio.Lock,幂等去重,防止并发重复聚合
|
||||||
|
_REBUILD_LOCKS: dict[str, asyncio.Lock] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _set_graph_cache(cache_key: str, data: dict) -> None:
|
||||||
|
"""写入图聚合缓存(存 data + built_at + expires_at,epoch 秒)"""
|
||||||
|
ttl_s = _graph_ttl_seconds()
|
||||||
|
if ttl_s <= 0:
|
||||||
|
return
|
||||||
|
now = time.time()
|
||||||
|
entry = {"data": data, "built_at": now, "expires_at": now + ttl_s}
|
||||||
|
set_cache(cache_key, json.dumps(entry, ensure_ascii=False), ttl_seconds=ttl_s)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_graph_cache(cache_key: str) -> tuple[Optional[dict], Optional[float]]:
|
||||||
|
"""读取图聚合缓存,返回 (data, expires_at);无缓存/损坏返回 (None, None)"""
|
||||||
|
cached = get_cache(cache_key)
|
||||||
|
if cached is None:
|
||||||
|
return None, None
|
||||||
|
try:
|
||||||
|
entry = json.loads(cached)
|
||||||
|
return entry.get("data"), entry.get("expires_at")
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
async def _build_theme_graph(sort_field: int, top_n: int) -> dict:
|
||||||
|
"""构建完整图数据(全量统计,边由前端从 stocks[].themeCodes 重建)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sort_field: 题材排序 1=涨幅, 4=热度(当前榜在前,另一榜合并补充)
|
||||||
|
top_n: 每个榜单的题材数量(1-60)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"themes": [{themeCode, themeName, stockCount}],
|
||||||
|
"stocks": [{securityCode, securityName, coverCount, f3, f2, f62, f100, themeCodes[]}],
|
||||||
|
"stats": {themeCount, stockCount, coreCount, maxCover}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
# 1. 合并采样涨幅榜 + 热度榜(当前榜优先在前,另一榜补充),按 themeCode 去重
|
||||||
|
primary_list = await fetch_theme_list(sort_field, asc=False)
|
||||||
|
other_field = 4 if sort_field == 1 else 1
|
||||||
|
other_list = await fetch_theme_list(other_field, asc=False)
|
||||||
|
merged: dict[str, dict] = {}
|
||||||
|
for t in primary_list[:top_n]:
|
||||||
|
merged.setdefault(t["themeCode"], t)
|
||||||
|
for t in other_list[:top_n]:
|
||||||
|
merged.setdefault(t["themeCode"], t)
|
||||||
|
themes = list(merged.values())
|
||||||
|
if not themes:
|
||||||
|
return {"themes": [], "stocks": [], "stats": {}}
|
||||||
|
|
||||||
|
# 2. 并发拉取每题材股票(限流保护)
|
||||||
|
sem = asyncio.Semaphore(5)
|
||||||
|
|
||||||
|
async def _fetch_with_limit(code: str):
|
||||||
|
async with sem:
|
||||||
|
return await fetch_theme_stocks(code)
|
||||||
|
|
||||||
|
results = await asyncio.gather(*[_fetch_with_limit(t["themeCode"]) for t in themes])
|
||||||
|
|
||||||
|
# 3. 构建 M:N 关系:统计每股覆盖的题材数
|
||||||
|
theme_map = {t["themeCode"]: t for t in themes}
|
||||||
|
stock_map: dict[str, dict] = {} # securityCode -> stock dict
|
||||||
|
|
||||||
|
for t, res in zip(themes, results):
|
||||||
|
stock_list = res.get("stockList", [])
|
||||||
|
theme_map[t["themeCode"]]["stockCount"] = len(stock_list)
|
||||||
|
for s in stock_list:
|
||||||
|
code = s["securityCode"]
|
||||||
|
if code not in stock_map:
|
||||||
|
stock_map[code] = {
|
||||||
|
"securityCode": code,
|
||||||
|
"securityName": s.get("securityName", ""),
|
||||||
|
"coverCount": 0,
|
||||||
|
"f3": s.get("f3"),
|
||||||
|
"f2": s.get("f2"),
|
||||||
|
"f62": s.get("f62"),
|
||||||
|
"f100": s.get("f100", ""),
|
||||||
|
"themeCodes": [],
|
||||||
|
}
|
||||||
|
stock_map[code]["coverCount"] += 1
|
||||||
|
stock_map[code]["themeCodes"].append(t["themeCode"])
|
||||||
|
|
||||||
|
# 4. 排序:覆盖题材数越多(穿透越强)排越前
|
||||||
|
stocks = sorted(stock_map.values(), key=lambda x: (-x["coverCount"], -(x.get("f3") or 0)))
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
"themeCount": len(themes),
|
||||||
|
"stockCount": len(stocks),
|
||||||
|
"coreCount": sum(1 for s in stocks if s["coverCount"] >= 2),
|
||||||
|
"maxCover": max((s["coverCount"] for s in stocks), default=1),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"themes": [
|
||||||
|
{"themeCode": t["themeCode"], "themeName": t["themeName"], "stockCount": t["stockCount"]}
|
||||||
|
for t in theme_map.values()
|
||||||
|
],
|
||||||
|
"stocks": stocks,
|
||||||
|
"stats": stats,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _trim_graph_result(result: dict, limit: int) -> dict:
|
||||||
|
"""按 limit 裁剪 stocks(保留穿透度最高的 N 只),仅影响下发体积,不影响 coverCount 统计"""
|
||||||
|
return {
|
||||||
|
"themes": result.get("themes", []),
|
||||||
|
"stocks": result.get("stocks", [])[:limit],
|
||||||
|
"stats": result.get("stats", {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _spawn_rebuild(cache_key: str, sort_field: int, top_n: int) -> None:
|
||||||
|
"""幂等触发后台重建:已有重建任务在跑则跳过"""
|
||||||
|
lock = _REBUILD_LOCKS.setdefault(cache_key, asyncio.Lock())
|
||||||
|
if lock.locked():
|
||||||
|
return
|
||||||
|
asyncio.create_task(_rebuild_task(cache_key, sort_field, top_n, lock))
|
||||||
|
|
||||||
|
|
||||||
|
async def _rebuild_task(cache_key: str, sort_field: int, top_n: int, lock: asyncio.Lock) -> None:
|
||||||
|
"""后台重建:拿锁后 double-check 缓存是否已被刷新,避免重复聚合"""
|
||||||
|
async with lock:
|
||||||
|
try:
|
||||||
|
data, expires_at = _get_graph_cache(cache_key)
|
||||||
|
if data is not None and expires_at and expires_at > time.time():
|
||||||
|
return # 已被其他任务刷新
|
||||||
|
data = await _build_theme_graph(sort_field, top_n)
|
||||||
|
_set_graph_cache(cache_key, data)
|
||||||
|
print(f"[themes] graph 后台重建完成: {cache_key}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[themes] graph 后台重建失败: {cache_key} {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_theme_graph(sort_field: int = 1, top_n: int = 30, limit: int = 1000) -> dict:
|
||||||
|
"""获取热点穿透图数据(盘中 60s 缓存 + stale-while-revalidate)
|
||||||
|
|
||||||
|
缓存命中且未过期 → 直接返回;已过期 → 返回旧数据并后台异步重建(秒开);
|
||||||
|
无缓存 → 同步构建(并发下加锁去重)。返回前按 limit 裁剪 stocks。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sort_field: 题材排序 1=涨幅, 4=热度
|
||||||
|
top_n: 每个榜单的题材数量(1-60)
|
||||||
|
limit: 下发 stocks 上限(穿透度最高的 N 只)
|
||||||
|
"""
|
||||||
|
cache_key = f"theme_graph:{sort_field}:{top_n}"
|
||||||
|
|
||||||
|
data, expires_at = _get_graph_cache(cache_key)
|
||||||
|
if data is not None:
|
||||||
|
# 有缓存:新鲜直接返回;过期返回旧数据并后台刷新
|
||||||
|
if not (expires_at and expires_at > time.time()):
|
||||||
|
_spawn_rebuild(cache_key, sort_field, top_n)
|
||||||
|
return _trim_graph_result(data, limit)
|
||||||
|
|
||||||
|
# 无缓存:同步构建(并发下加锁去重)
|
||||||
|
lock = _REBUILD_LOCKS.setdefault(cache_key, asyncio.Lock())
|
||||||
|
async with lock:
|
||||||
|
data, expires_at = _get_graph_cache(cache_key)
|
||||||
|
if data is not None:
|
||||||
|
# 等待锁期间已被其他请求写入
|
||||||
|
if not (expires_at and expires_at > time.time()):
|
||||||
|
_spawn_rebuild(cache_key, sort_field, top_n)
|
||||||
|
return _trim_graph_result(data, limit)
|
||||||
|
data = await _build_theme_graph(sort_field, top_n)
|
||||||
|
_set_graph_cache(cache_key, data)
|
||||||
|
return _trim_graph_result(data, limit)
|
||||||
@@ -44,6 +44,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
"d3-force": "^3.0.0",
|
||||||
"date-fns": "^4.4.0",
|
"date-fns": "^4.4.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"framer-motion": "^11.18.2",
|
"framer-motion": "^11.18.2",
|
||||||
@@ -64,6 +65,7 @@
|
|||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/d3-force": "^3.0.10",
|
||||||
"@types/node": "^22.20.0",
|
"@types/node": "^22.20.0",
|
||||||
"@types/react": "^19.2.17",
|
"@types/react": "^19.2.17",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
|||||||
Generated
+231
-198
@@ -13,82 +13,82 @@ importers:
|
|||||||
version: 5.4.0(react-hook-form@7.81.0(react@19.2.7))
|
version: 5.4.0(react-hook-form@7.81.0(react@19.2.7))
|
||||||
'@radix-ui/react-accordion':
|
'@radix-ui/react-accordion':
|
||||||
specifier: ^1.2.15
|
specifier: ^1.2.15
|
||||||
version: 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.2.15(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-alert-dialog':
|
'@radix-ui/react-alert-dialog':
|
||||||
specifier: ^1.1.18
|
specifier: ^1.1.18
|
||||||
version: 1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.1.18(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-aspect-ratio':
|
'@radix-ui/react-aspect-ratio':
|
||||||
specifier: ^1.1.11
|
specifier: ^1.1.11
|
||||||
version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-avatar':
|
'@radix-ui/react-avatar':
|
||||||
specifier: ^1.2.1
|
specifier: ^1.2.1
|
||||||
version: 1.2.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.2.1(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-checkbox':
|
'@radix-ui/react-checkbox':
|
||||||
specifier: ^1.3.6
|
specifier: ^1.3.6
|
||||||
version: 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.3.6(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-collapsible':
|
'@radix-ui/react-collapsible':
|
||||||
specifier: ^1.1.15
|
specifier: ^1.1.15
|
||||||
version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-context-menu':
|
'@radix-ui/react-context-menu':
|
||||||
specifier: ^2.3.2
|
specifier: ^2.3.2
|
||||||
version: 2.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 2.3.2(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-dialog':
|
'@radix-ui/react-dialog':
|
||||||
specifier: ^1.1.18
|
specifier: ^1.1.18
|
||||||
version: 1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.1.18(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-dropdown-menu':
|
'@radix-ui/react-dropdown-menu':
|
||||||
specifier: ^2.1.19
|
specifier: ^2.1.19
|
||||||
version: 2.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 2.1.19(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-hover-card':
|
'@radix-ui/react-hover-card':
|
||||||
specifier: ^1.1.18
|
specifier: ^1.1.18
|
||||||
version: 1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.1.18(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-label':
|
'@radix-ui/react-label':
|
||||||
specifier: ^2.1.11
|
specifier: ^2.1.11
|
||||||
version: 2.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 2.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-menubar':
|
'@radix-ui/react-menubar':
|
||||||
specifier: ^1.1.19
|
specifier: ^1.1.19
|
||||||
version: 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-navigation-menu':
|
'@radix-ui/react-navigation-menu':
|
||||||
specifier: ^1.2.17
|
specifier: ^1.2.17
|
||||||
version: 1.2.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.2.17(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-popover':
|
'@radix-ui/react-popover':
|
||||||
specifier: ^1.1.18
|
specifier: ^1.1.18
|
||||||
version: 1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.1.18(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-progress':
|
'@radix-ui/react-progress':
|
||||||
specifier: ^1.1.11
|
specifier: ^1.1.11
|
||||||
version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-radio-group':
|
'@radix-ui/react-radio-group':
|
||||||
specifier: ^1.4.2
|
specifier: ^1.4.2
|
||||||
version: 1.4.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.4.2(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-scroll-area':
|
'@radix-ui/react-scroll-area':
|
||||||
specifier: ^1.2.13
|
specifier: ^1.2.13
|
||||||
version: 1.2.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.2.13(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-select':
|
'@radix-ui/react-select':
|
||||||
specifier: ^2.3.2
|
specifier: ^2.3.2
|
||||||
version: 2.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 2.3.2(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-separator':
|
'@radix-ui/react-separator':
|
||||||
specifier: ^1.1.11
|
specifier: ^1.1.11
|
||||||
version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-slider':
|
'@radix-ui/react-slider':
|
||||||
specifier: ^1.4.2
|
specifier: ^1.4.2
|
||||||
version: 1.4.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.4.2(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-slot':
|
'@radix-ui/react-slot':
|
||||||
specifier: ^1.3.0
|
specifier: ^1.3.0
|
||||||
version: 1.3.0(@types/react@19.2.17)(react@19.2.7)
|
version: 1.3.0(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-switch':
|
'@radix-ui/react-switch':
|
||||||
specifier: ^1.3.2
|
specifier: ^1.3.2
|
||||||
version: 1.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.3.2(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-tabs':
|
'@radix-ui/react-tabs':
|
||||||
specifier: ^1.1.16
|
specifier: ^1.1.16
|
||||||
version: 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.1.16(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-toggle':
|
'@radix-ui/react-toggle':
|
||||||
specifier: ^1.1.13
|
specifier: ^1.1.13
|
||||||
version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.1.13(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-toggle-group':
|
'@radix-ui/react-toggle-group':
|
||||||
specifier: ^1.1.14
|
specifier: ^1.1.14
|
||||||
version: 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.1.14(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-tooltip':
|
'@radix-ui/react-tooltip':
|
||||||
specifier: ^1.2.11
|
specifier: ^1.2.11
|
||||||
version: 1.2.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.2.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.3.2
|
specifier: ^4.3.2
|
||||||
version: 4.3.2(vite@7.3.6(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5))
|
version: 4.3.2(vite@7.3.6(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5))
|
||||||
@@ -109,7 +109,10 @@ importers:
|
|||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
cmdk:
|
cmdk:
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.1.1(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
|
d3-force:
|
||||||
|
specifier: ^3.0.0
|
||||||
|
version: 3.0.0
|
||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^4.4.0
|
specifier: ^4.4.0
|
||||||
version: 4.4.0
|
version: 4.4.0
|
||||||
@@ -157,7 +160,7 @@ importers:
|
|||||||
version: 1.4.0
|
version: 1.4.0
|
||||||
vaul:
|
vaul:
|
||||||
specifier: ^1.1.2
|
specifier: ^1.1.2
|
||||||
version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.1.2(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
vite-tsconfig-paths:
|
vite-tsconfig-paths:
|
||||||
specifier: ^6.1.1
|
specifier: ^6.1.1
|
||||||
version: 6.1.1(typescript@5.9.3)(vite@7.3.6(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5))
|
version: 6.1.1(typescript@5.9.3)(vite@7.3.6(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5))
|
||||||
@@ -165,6 +168,9 @@ importers:
|
|||||||
specifier: ^3.25.76
|
specifier: ^3.25.76
|
||||||
version: 3.25.76
|
version: 3.25.76
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/d3-force':
|
||||||
|
specifier: ^3.0.10
|
||||||
|
version: 3.0.10
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.20.0
|
specifier: ^22.20.0
|
||||||
version: 22.20.0
|
version: 22.20.0
|
||||||
@@ -173,7 +179,7 @@ importers:
|
|||||||
version: 19.2.17
|
version: 19.2.17
|
||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
specifier: ^19.2.3
|
specifier: ^19.2.3
|
||||||
version: 19.2.3(@types/react@19.2.17)
|
version: 19.2.4(@types/react@19.2.17)
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^5.2.0
|
specifier: ^5.2.0
|
||||||
version: 5.2.0(vite@7.3.6(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5))
|
version: 5.2.0(vite@7.3.6(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5))
|
||||||
@@ -1399,6 +1405,9 @@ packages:
|
|||||||
'@types/d3-ease@3.0.2':
|
'@types/d3-ease@3.0.2':
|
||||||
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
|
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
|
||||||
|
|
||||||
|
'@types/d3-force@3.0.10':
|
||||||
|
resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==}
|
||||||
|
|
||||||
'@types/d3-interpolate@3.0.4':
|
'@types/d3-interpolate@3.0.4':
|
||||||
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
|
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
|
||||||
|
|
||||||
@@ -1423,8 +1432,8 @@ packages:
|
|||||||
'@types/node@22.20.0':
|
'@types/node@22.20.0':
|
||||||
resolution: {integrity: sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==}
|
resolution: {integrity: sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==}
|
||||||
|
|
||||||
'@types/react-dom@19.2.3':
|
'@types/react-dom@19.2.4':
|
||||||
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
|
resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': ^19.2.0
|
'@types/react': ^19.2.0
|
||||||
|
|
||||||
@@ -1495,10 +1504,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
|
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-dispatch@3.0.1:
|
||||||
|
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
d3-ease@3.0.1:
|
d3-ease@3.0.1:
|
||||||
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
|
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-force@3.0.0:
|
||||||
|
resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
d3-format@3.1.2:
|
d3-format@3.1.2:
|
||||||
resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==}
|
resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -1511,6 +1528,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
|
resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-quadtree@3.0.1:
|
||||||
|
resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
d3-scale@4.0.2:
|
d3-scale@4.0.2:
|
||||||
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
|
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -2350,58 +2371,58 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/primitive@1.1.4': {}
|
'@radix-ui/primitive@1.1.4': {}
|
||||||
|
|
||||||
'@radix-ui/react-accordion@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-accordion@1.2.15(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-collapsible': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-collapsible': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-alert-dialog@1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-alert-dialog@1.1.18(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-dialog': 1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-dialog': 1.1.18(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-arrow@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-arrow@1.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-aspect-ratio@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-aspect-ratio@1.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-avatar@1.2.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-avatar@1.2.1(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
@@ -2409,15 +2430,15 @@ snapshots:
|
|||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-checkbox@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-checkbox@1.3.6(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
@@ -2425,35 +2446,35 @@ snapshots:
|
|||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-collapsible@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-collapsible@1.1.15(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-collection@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-collection@1.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-compose-refs@1.1.3(@types/react@19.2.17)(react@19.2.7)':
|
'@radix-ui/react-compose-refs@1.1.3(@types/react@19.2.17)(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2461,18 +2482,18 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
|
|
||||||
'@radix-ui/react-context-menu@2.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-context-menu@2.3.2(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-menu': 2.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-menu': 2.1.19(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-context@1.1.4(@types/react@19.2.17)(react@19.2.7)':
|
'@radix-ui/react-context@1.1.4(@types/react@19.2.17)(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2480,18 +2501,18 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
|
|
||||||
'@radix-ui/react-dialog@1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-dialog@1.1.18(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-focus-scope': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-focus-scope': 1.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
aria-hidden: 1.2.6
|
aria-hidden: 1.2.6
|
||||||
@@ -2500,7 +2521,7 @@ snapshots:
|
|||||||
react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7)
|
react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-direction@1.1.2(@types/react@19.2.17)(react@19.2.7)':
|
'@radix-ui/react-direction@1.1.2(@types/react@19.2.17)(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2508,33 +2529,33 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
|
|
||||||
'@radix-ui/react-dismissable-layer@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-dismissable-layer@1.1.14(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-dropdown-menu@2.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-dropdown-menu@2.1.19(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-menu': 2.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-menu': 2.1.19(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-focus-guards@1.1.4(@types/react@19.2.17)(react@19.2.7)':
|
'@radix-ui/react-focus-guards@1.1.4(@types/react@19.2.17)(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2542,33 +2563,33 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
|
|
||||||
'@radix-ui/react-focus-scope@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-focus-scope@1.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-hover-card@1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-hover-card@1.1.18(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-popper': 1.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-popper': 1.3.2(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-id@1.1.2(@types/react@19.2.17)(react@19.2.7)':
|
'@radix-ui/react-id@1.1.2(@types/react@19.2.17)(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2577,31 +2598,31 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
|
|
||||||
'@radix-ui/react-label@2.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-label@2.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-menu@2.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-menu@2.1.19(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-focus-scope': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-focus-scope': 1.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-popper': 1.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-popper': 1.3.2(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-roving-focus': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-roving-focus': 1.1.14(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
aria-hidden: 1.2.6
|
aria-hidden: 1.2.6
|
||||||
@@ -2610,61 +2631,61 @@ snapshots:
|
|||||||
react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7)
|
react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-menubar@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-menubar@1.1.19(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-menu': 2.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-menu': 2.1.19(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-roving-focus': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-roving-focus': 1.1.14(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-navigation-menu@1.2.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-navigation-menu@1.2.17(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-visually-hidden': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-visually-hidden': 1.2.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-popover@1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-popover@1.1.18(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-focus-scope': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-focus-scope': 1.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-popper': 1.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-popper': 1.3.2(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
aria-hidden: 1.2.6
|
aria-hidden: 1.2.6
|
||||||
@@ -2673,15 +2694,15 @@ snapshots:
|
|||||||
react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7)
|
react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-popper@1.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-popper@1.3.2(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@floating-ui/react-dom': 2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@floating-ui/react-dom': 2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-arrow': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-arrow': 1.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-rect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-rect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
@@ -2691,55 +2712,55 @@ snapshots:
|
|||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-portal@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-portal@1.1.13(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-presence@1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-presence@1.1.6(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-primitive@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-primitive@2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-progress@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-progress@1.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-radio-group@1.4.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-radio-group@1.4.2(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-roving-focus': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-roving-focus': 1.1.14(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
@@ -2747,90 +2768,90 @@ snapshots:
|
|||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-roving-focus@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-roving-focus@1.1.14(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-scroll-area@1.2.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-scroll-area@1.2.13(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/number': 1.1.2
|
'@radix-ui/number': 1.1.2
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-select@2.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-select@2.3.2(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/number': 1.1.2
|
'@radix-ui/number': 1.1.2
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-focus-scope': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-focus-scope': 1.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-popper': 1.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-popper': 1.3.2(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-visually-hidden': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-visually-hidden': 1.2.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
aria-hidden: 1.2.6
|
aria-hidden: 1.2.6
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7)
|
react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-separator@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-separator@1.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-slider@1.4.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-slider@1.4.2(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/number': 1.1.2
|
'@radix-ui/number': 1.1.2
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
@@ -2839,7 +2860,7 @@ snapshots:
|
|||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-slot@1.3.0(@types/react@19.2.17)(react@19.2.7)':
|
'@radix-ui/react-slot@1.3.0(@types/react@19.2.17)(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2848,12 +2869,12 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
|
|
||||||
'@radix-ui/react-switch@1.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-switch@1.3.2(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
@@ -2861,69 +2882,69 @@ snapshots:
|
|||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-tabs@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-tabs@1.1.16(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-roving-focus': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-roving-focus': 1.1.14(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-toggle-group@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-toggle-group@1.1.14(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-roving-focus': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-roving-focus': 1.1.14(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-toggle': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-toggle': 1.1.13(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-toggle@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-toggle@1.1.13(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-tooltip@1.2.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-tooltip@1.2.11(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.4
|
'@radix-ui/primitive': 1.1.4
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-popper': 1.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-popper': 1.3.2(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-visually-hidden': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-visually-hidden': 1.2.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-use-callback-ref@1.1.2(@types/react@19.2.17)(react@19.2.7)':
|
'@radix-ui/react-use-callback-ref@1.1.2(@types/react@19.2.17)(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2978,14 +2999,14 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
|
|
||||||
'@radix-ui/react-visually-hidden@1.2.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@radix-ui/react-visually-hidden@1.2.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.4(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/rect@1.1.2': {}
|
'@radix-ui/rect@1.1.2': {}
|
||||||
|
|
||||||
@@ -3251,6 +3272,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/d3-ease@3.0.2': {}
|
'@types/d3-ease@3.0.2': {}
|
||||||
|
|
||||||
|
'@types/d3-force@3.0.10': {}
|
||||||
|
|
||||||
'@types/d3-interpolate@3.0.4':
|
'@types/d3-interpolate@3.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/d3-color': 3.1.3
|
'@types/d3-color': 3.1.3
|
||||||
@@ -3275,7 +3298,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
'@types/react-dom@19.2.3(@types/react@19.2.17)':
|
'@types/react-dom@19.2.4(@types/react@19.2.17)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
|
|
||||||
@@ -3332,12 +3355,12 @@ snapshots:
|
|||||||
|
|
||||||
clsx@2.1.1: {}
|
clsx@2.1.1: {}
|
||||||
|
|
||||||
cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
cmdk@1.1.1(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-dialog': 1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-dialog': 1.1.18(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
'@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
|
||||||
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -3356,8 +3379,16 @@ snapshots:
|
|||||||
|
|
||||||
d3-color@3.1.0: {}
|
d3-color@3.1.0: {}
|
||||||
|
|
||||||
|
d3-dispatch@3.0.1: {}
|
||||||
|
|
||||||
d3-ease@3.0.1: {}
|
d3-ease@3.0.1: {}
|
||||||
|
|
||||||
|
d3-force@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
d3-dispatch: 3.0.1
|
||||||
|
d3-quadtree: 3.0.1
|
||||||
|
d3-timer: 3.0.1
|
||||||
|
|
||||||
d3-format@3.1.2: {}
|
d3-format@3.1.2: {}
|
||||||
|
|
||||||
d3-interpolate@3.0.1:
|
d3-interpolate@3.0.1:
|
||||||
@@ -3366,6 +3397,8 @@ snapshots:
|
|||||||
|
|
||||||
d3-path@3.1.0: {}
|
d3-path@3.1.0: {}
|
||||||
|
|
||||||
|
d3-quadtree@3.0.1: {}
|
||||||
|
|
||||||
d3-scale@4.0.2:
|
d3-scale@4.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
d3-array: 3.2.4
|
d3-array: 3.2.4
|
||||||
@@ -3813,9 +3846,9 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
|
|
||||||
vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
vaul@1.1.2(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-dialog': 1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@radix-ui/react-dialog': 1.1.18(@types/react-dom@19.2.4(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ export interface StockQuote {
|
|||||||
time: string;
|
time: string;
|
||||||
change: number;
|
change: number;
|
||||||
changePercent: number;
|
changePercent: number;
|
||||||
|
turnoverRate: number; // 换手率%
|
||||||
|
pe: number; // 市盈率
|
||||||
|
pb: number; // 市净率
|
||||||
|
totalMarketCap: number; // 总市值
|
||||||
|
circulatingMarketCap: number; // 流通市值
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KLineData {
|
export interface KLineData {
|
||||||
@@ -458,12 +463,12 @@ export interface SectorResponse {
|
|||||||
* 获取东方财富板块列表(按主力净流入排序)
|
* 获取东方财富板块列表(按主力净流入排序)
|
||||||
* @param type industry=行业板块, concept=概念板块
|
* @param type industry=行业板块, concept=概念板块
|
||||||
*/
|
*/
|
||||||
export async function fetchSectors(type: SectorType): Promise<SectorItem[]> {
|
export async function fetchSectors(type: SectorType, signal?: AbortSignal): Promise<SectorItem[]> {
|
||||||
const baseUrl = getApiBaseUrl();
|
const baseUrl = getApiBaseUrl();
|
||||||
const url = `${baseUrl}/api/sectors?type=${type}`;
|
const url = `${baseUrl}/api/sectors?type=${type}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(url, { method: "GET" });
|
const resp = await fetch(url, { method: "GET", signal, cache: "no-store" });
|
||||||
if (!resp.ok) return [];
|
if (!resp.ok) return [];
|
||||||
const result: SectorResponse = await resp.json();
|
const result: SectorResponse = await resp.json();
|
||||||
return result.data || [];
|
return result.data || [];
|
||||||
|
|||||||
@@ -0,0 +1,229 @@
|
|||||||
|
// 题材数据获取工具:通过 Python 后端代理调用东方财富题材接口
|
||||||
|
import { getApiBaseUrl } from "@/lib/api-client";
|
||||||
|
|
||||||
|
/* ── 题材列表 ── */
|
||||||
|
|
||||||
|
export interface ThemeItem {
|
||||||
|
themeCode: string;
|
||||||
|
themeName: string;
|
||||||
|
securityName: string; // 领涨股名称
|
||||||
|
securityCode: string; // 领涨股代码
|
||||||
|
codeWithSuffix: string;
|
||||||
|
hotRank: number; // 热度排名
|
||||||
|
f3: number | null; // 领涨股涨幅
|
||||||
|
bf3: number | null; // 题材涨幅
|
||||||
|
hotValue: number; // 热度值
|
||||||
|
hotValueUpLimit: number; // 热度上限
|
||||||
|
strengthValue: number | null; // 强度值
|
||||||
|
fex5: number | null; // 成交额
|
||||||
|
fex3: number | null;
|
||||||
|
label: string | null; // 标签(如"超级爆点")
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ThemeSortField = 1 | 3 | 4 | 5; // 1=涨幅 3=强度 4=热度排名 5=成交额
|
||||||
|
|
||||||
|
export interface ThemeListResponse {
|
||||||
|
data: ThemeItem[];
|
||||||
|
count: number;
|
||||||
|
sort_field: number;
|
||||||
|
asc: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取全部题材列表
|
||||||
|
* @param sortField 1=涨幅 3=强度 4=热度排名 5=成交额
|
||||||
|
* @param asc true=升序
|
||||||
|
*/
|
||||||
|
export async function fetchThemes(sortField: ThemeSortField = 1, asc: boolean = false): Promise<ThemeItem[]> {
|
||||||
|
const baseUrl = getApiBaseUrl();
|
||||||
|
const url = `${baseUrl}/api/themes?sort_field=${sortField}&asc=${asc}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(url, { method: "GET", cache: "no-store" });
|
||||||
|
if (!resp.ok) return [];
|
||||||
|
const result: ThemeListResponse = await resp.json();
|
||||||
|
return result.data || [];
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[theme-api] 获取题材列表失败:", err);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 题材详情 ── */
|
||||||
|
|
||||||
|
export interface ThemeHotEvent {
|
||||||
|
newsTitle: string | null;
|
||||||
|
newsSummary: string | null;
|
||||||
|
newsMediaName: string | null;
|
||||||
|
newsPublishTimeFormat: string | null;
|
||||||
|
newsCode: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThemeNews {
|
||||||
|
newsCode: string;
|
||||||
|
newsTitle: string;
|
||||||
|
newsMediaName: string;
|
||||||
|
newsPublishTime: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThemeBaseInfo {
|
||||||
|
themeCode: string;
|
||||||
|
themeName: string;
|
||||||
|
introduction: string;
|
||||||
|
explainImgUrl: string | null;
|
||||||
|
themeLevel: number;
|
||||||
|
isShowRank: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThemeDetail {
|
||||||
|
baseInfo: ThemeBaseInfo;
|
||||||
|
hotEvent: ThemeHotEvent | null;
|
||||||
|
eventHistory: ThemeNews[];
|
||||||
|
topicId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取题材详情(简介 + 热点事件 + 相关新闻)
|
||||||
|
*/
|
||||||
|
export async function fetchThemeDetail(themeCode: string): Promise<ThemeDetail | null> {
|
||||||
|
const baseUrl = getApiBaseUrl();
|
||||||
|
const url = `${baseUrl}/api/themes/${themeCode}/detail`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(url, { method: "GET", cache: "no-store" });
|
||||||
|
if (!resp.ok) return null;
|
||||||
|
const result = await resp.json();
|
||||||
|
return result.data || null;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[theme-api] 获取题材详情失败:", err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 题材相关股票 ── */
|
||||||
|
|
||||||
|
export interface ThemeStockKeyword {
|
||||||
|
keywordCode: string;
|
||||||
|
keyword: string;
|
||||||
|
introduction: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThemeStock {
|
||||||
|
securityName: string;
|
||||||
|
securityCode: string;
|
||||||
|
codeSuffix: string;
|
||||||
|
f2: number; // 现价
|
||||||
|
f3: number; // 涨跌幅%
|
||||||
|
f5: number; // 成交量
|
||||||
|
f6: number; // 成交额
|
||||||
|
f8: number; // 换手率%
|
||||||
|
f20: number; // 总市值
|
||||||
|
f21: number; // 流通市值
|
||||||
|
f62: number; // 主力净流入
|
||||||
|
f100: string; // 所属行业
|
||||||
|
f265: string; // 板块代码
|
||||||
|
label: string | null; // 涨停标签
|
||||||
|
rank: number;
|
||||||
|
dragonStockLabel: number;
|
||||||
|
keywordList: ThemeStockKeyword[]; // 入选理由
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThemeStatistic {
|
||||||
|
f3: number | null; // 板块涨幅
|
||||||
|
f104: number | null; // 上涨家数
|
||||||
|
f105: number | null; // 下跌家数
|
||||||
|
f106: number | null; // 平盘家数
|
||||||
|
fex5: number | null; // 板块成交额
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThemeStocksResponse {
|
||||||
|
data: ThemeStock[];
|
||||||
|
statistic: ThemeStatistic;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取题材下全部相关股票
|
||||||
|
*/
|
||||||
|
export async function fetchThemeStocks(themeCode: string): Promise<ThemeStocksResponse | null> {
|
||||||
|
const baseUrl = getApiBaseUrl();
|
||||||
|
const url = `${baseUrl}/api/themes/${themeCode}/stocks`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(url, { method: "GET", cache: "no-store" });
|
||||||
|
if (!resp.ok) return null;
|
||||||
|
return await resp.json();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[theme-api] 获取题材股票失败:", err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 热点穿透:题材-股票 网状关系图 ── */
|
||||||
|
|
||||||
|
export interface GraphTheme {
|
||||||
|
themeCode: string;
|
||||||
|
themeName: string;
|
||||||
|
stockCount: number; // 题材内股票数
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphStock {
|
||||||
|
securityCode: string;
|
||||||
|
securityName: string;
|
||||||
|
coverCount: number; // 覆盖题材数(穿透强度)
|
||||||
|
f3: number | null; // 涨幅
|
||||||
|
f2: number | null; // 现价
|
||||||
|
f62: number | null; // 主力净流入
|
||||||
|
f100: string; // 行业
|
||||||
|
themeCodes: string[]; // 所属题材代码
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphStats {
|
||||||
|
themeCount: number;
|
||||||
|
stockCount: number;
|
||||||
|
coreCount: number; // 覆盖≥2 的核心股数
|
||||||
|
maxCover: number; // 最大覆盖题材数
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThemeGraph {
|
||||||
|
themes: GraphTheme[];
|
||||||
|
stocks: GraphStock[];
|
||||||
|
stats: GraphStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从股票覆盖题材重建关系边(后端不再下发 edges,体积减半以上)
|
||||||
|
* 返回 d3-force 可直接使用的 source/target 节点 id("t:题材code" / "s:股票code")
|
||||||
|
*/
|
||||||
|
export function buildEdgesFromStocks(stocks: GraphStock[]): { source: string; target: string }[] {
|
||||||
|
const edges: { source: string; target: string }[] = [];
|
||||||
|
for (const s of stocks) {
|
||||||
|
const target = `s:${s.securityCode}`;
|
||||||
|
for (const tc of s.themeCodes) edges.push({ source: `t:${tc}`, target });
|
||||||
|
}
|
||||||
|
return edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取热点穿透图数据(题材-股票 M:N 网状关系)
|
||||||
|
* @param sortField 1=涨幅 4=热度
|
||||||
|
* @param top 题材数量
|
||||||
|
* @param limit 下发的股票节点上限(按穿透度取前 N 只)
|
||||||
|
*/
|
||||||
|
export async function fetchThemeGraph(
|
||||||
|
sortField: 1 | 4 = 1,
|
||||||
|
top: number = 30,
|
||||||
|
limit: number = 1000,
|
||||||
|
): Promise<ThemeGraph | null> {
|
||||||
|
const baseUrl = getApiBaseUrl();
|
||||||
|
const url = `${baseUrl}/api/themes/graph?sort_field=${sortField}&top=${top}&limit=${limit}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(url, { method: "GET", cache: "no-store" });
|
||||||
|
if (!resp.ok) return null;
|
||||||
|
return await resp.json();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[theme-api] 获取热点穿透图数据失败:", err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-2
@@ -1,14 +1,17 @@
|
|||||||
// 应用入口:样式在 ./styles.css(Tailwind v4 + design token),路由见 ./router.tsx
|
// 应用入口:样式在 ./styles.css(Tailwind v4 + design token),路由见 ./router.tsx
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { RouterProvider } from "@tanstack/react-router";
|
import { RouterProvider } from "@tanstack/react-router";
|
||||||
import { getRouter } from "./router";
|
import { getRouter } from "./router";
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
|
|
||||||
const router = getRouter();
|
const { router, queryClient } = getRouter();
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<RouterProvider router={router} />
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</QueryClientProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
+79
-3
@@ -9,21 +9,39 @@
|
|||||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||||
|
|
||||||
import { Route as rootRouteImport } from './routes/__root'
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
|
import { Route as ThemesRouteImport } from './routes/themes'
|
||||||
import { Route as SectorsRouteImport } from './routes/sectors'
|
import { Route as SectorsRouteImport } from './routes/sectors'
|
||||||
|
import { Route as HotMapRouteImport } from './routes/hot-map'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
|
import { Route as ThemeCodeRouteImport } from './routes/theme.$code'
|
||||||
import { Route as StockCodeRouteImport } from './routes/stock.$code'
|
import { Route as StockCodeRouteImport } from './routes/stock.$code'
|
||||||
import { Route as ShareCodeRouteImport } from './routes/share.$code'
|
import { Route as ShareCodeRouteImport } from './routes/share.$code'
|
||||||
|
|
||||||
|
const ThemesRoute = ThemesRouteImport.update({
|
||||||
|
id: '/themes',
|
||||||
|
path: '/themes',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const SectorsRoute = SectorsRouteImport.update({
|
const SectorsRoute = SectorsRouteImport.update({
|
||||||
id: '/sectors',
|
id: '/sectors',
|
||||||
path: '/sectors',
|
path: '/sectors',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const HotMapRoute = HotMapRouteImport.update({
|
||||||
|
id: '/hot-map',
|
||||||
|
path: '/hot-map',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const IndexRoute = IndexRouteImport.update({
|
const IndexRoute = IndexRouteImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const ThemeCodeRoute = ThemeCodeRouteImport.update({
|
||||||
|
id: '/theme/$code',
|
||||||
|
path: '/theme/$code',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const StockCodeRoute = StockCodeRouteImport.update({
|
const StockCodeRoute = StockCodeRouteImport.update({
|
||||||
id: '/stock/$code',
|
id: '/stock/$code',
|
||||||
path: '/stock/$code',
|
path: '/stock/$code',
|
||||||
@@ -37,40 +55,81 @@ const ShareCodeRoute = ShareCodeRouteImport.update({
|
|||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/hot-map': typeof HotMapRoute
|
||||||
'/sectors': typeof SectorsRoute
|
'/sectors': typeof SectorsRoute
|
||||||
|
'/themes': typeof ThemesRoute
|
||||||
'/share/$code': typeof ShareCodeRoute
|
'/share/$code': typeof ShareCodeRoute
|
||||||
'/stock/$code': typeof StockCodeRoute
|
'/stock/$code': typeof StockCodeRoute
|
||||||
|
'/theme/$code': typeof ThemeCodeRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/hot-map': typeof HotMapRoute
|
||||||
'/sectors': typeof SectorsRoute
|
'/sectors': typeof SectorsRoute
|
||||||
|
'/themes': typeof ThemesRoute
|
||||||
'/share/$code': typeof ShareCodeRoute
|
'/share/$code': typeof ShareCodeRoute
|
||||||
'/stock/$code': typeof StockCodeRoute
|
'/stock/$code': typeof StockCodeRoute
|
||||||
|
'/theme/$code': typeof ThemeCodeRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/hot-map': typeof HotMapRoute
|
||||||
'/sectors': typeof SectorsRoute
|
'/sectors': typeof SectorsRoute
|
||||||
|
'/themes': typeof ThemesRoute
|
||||||
'/share/$code': typeof ShareCodeRoute
|
'/share/$code': typeof ShareCodeRoute
|
||||||
'/stock/$code': typeof StockCodeRoute
|
'/stock/$code': typeof StockCodeRoute
|
||||||
|
'/theme/$code': typeof ThemeCodeRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths: '/' | '/sectors' | '/share/$code' | '/stock/$code'
|
fullPaths:
|
||||||
|
| '/'
|
||||||
|
| '/hot-map'
|
||||||
|
| '/sectors'
|
||||||
|
| '/themes'
|
||||||
|
| '/share/$code'
|
||||||
|
| '/stock/$code'
|
||||||
|
| '/theme/$code'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/' | '/sectors' | '/share/$code' | '/stock/$code'
|
to:
|
||||||
id: '__root__' | '/' | '/sectors' | '/share/$code' | '/stock/$code'
|
| '/'
|
||||||
|
| '/hot-map'
|
||||||
|
| '/sectors'
|
||||||
|
| '/themes'
|
||||||
|
| '/share/$code'
|
||||||
|
| '/stock/$code'
|
||||||
|
| '/theme/$code'
|
||||||
|
id:
|
||||||
|
| '__root__'
|
||||||
|
| '/'
|
||||||
|
| '/hot-map'
|
||||||
|
| '/sectors'
|
||||||
|
| '/themes'
|
||||||
|
| '/share/$code'
|
||||||
|
| '/stock/$code'
|
||||||
|
| '/theme/$code'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
|
HotMapRoute: typeof HotMapRoute
|
||||||
SectorsRoute: typeof SectorsRoute
|
SectorsRoute: typeof SectorsRoute
|
||||||
|
ThemesRoute: typeof ThemesRoute
|
||||||
ShareCodeRoute: typeof ShareCodeRoute
|
ShareCodeRoute: typeof ShareCodeRoute
|
||||||
StockCodeRoute: typeof StockCodeRoute
|
StockCodeRoute: typeof StockCodeRoute
|
||||||
|
ThemeCodeRoute: typeof ThemeCodeRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
interface FileRoutesByPath {
|
interface FileRoutesByPath {
|
||||||
|
'/themes': {
|
||||||
|
id: '/themes'
|
||||||
|
path: '/themes'
|
||||||
|
fullPath: '/themes'
|
||||||
|
preLoaderRoute: typeof ThemesRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/sectors': {
|
'/sectors': {
|
||||||
id: '/sectors'
|
id: '/sectors'
|
||||||
path: '/sectors'
|
path: '/sectors'
|
||||||
@@ -78,6 +137,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof SectorsRouteImport
|
preLoaderRoute: typeof SectorsRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/hot-map': {
|
||||||
|
id: '/hot-map'
|
||||||
|
path: '/hot-map'
|
||||||
|
fullPath: '/hot-map'
|
||||||
|
preLoaderRoute: typeof HotMapRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/': {
|
'/': {
|
||||||
id: '/'
|
id: '/'
|
||||||
path: '/'
|
path: '/'
|
||||||
@@ -85,6 +151,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IndexRouteImport
|
preLoaderRoute: typeof IndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/theme/$code': {
|
||||||
|
id: '/theme/$code'
|
||||||
|
path: '/theme/$code'
|
||||||
|
fullPath: '/theme/$code'
|
||||||
|
preLoaderRoute: typeof ThemeCodeRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/stock/$code': {
|
'/stock/$code': {
|
||||||
id: '/stock/$code'
|
id: '/stock/$code'
|
||||||
path: '/stock/$code'
|
path: '/stock/$code'
|
||||||
@@ -104,9 +177,12 @@ declare module '@tanstack/react-router' {
|
|||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
|
HotMapRoute: HotMapRoute,
|
||||||
SectorsRoute: SectorsRoute,
|
SectorsRoute: SectorsRoute,
|
||||||
|
ThemesRoute: ThemesRoute,
|
||||||
ShareCodeRoute: ShareCodeRoute,
|
ShareCodeRoute: ShareCodeRoute,
|
||||||
StockCodeRoute: StockCodeRoute,
|
StockCodeRoute: StockCodeRoute,
|
||||||
|
ThemeCodeRoute: ThemeCodeRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
|
|||||||
+1
-1
@@ -20,5 +20,5 @@ export const getRouter = () => {
|
|||||||
defaultPreloadStaleTime: 0,
|
defaultPreloadStaleTime: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
return router;
|
return { router, queryClient };
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+24
-10
@@ -10,7 +10,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from
|
|||||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Search, Plus, Share2, Trash2, TrendingUp, Loader2 } from "lucide-react";
|
import { Search, Plus, Share2, Trash2, TrendingUp, Loader2, Flame, Network } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
@@ -211,12 +211,26 @@ function Index() {
|
|||||||
A股走势追踪
|
A股走势追踪
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm md:text-base text-muted-foreground">创建股票集合,分享历史走势</p>
|
<p className="text-sm md:text-base text-muted-foreground">创建股票集合,分享历史走势</p>
|
||||||
<Link to="/sectors">
|
<div className="mt-3 flex items-center justify-center gap-2">
|
||||||
<Button variant="outline" size="sm" className="mt-3 gap-1.5 text-xs">
|
<Link to="/sectors">
|
||||||
<TrendingUp className="h-3.5 w-3.5" />
|
<Button variant="outline" size="sm" className="gap-1.5 text-xs">
|
||||||
板块资金流向
|
<TrendingUp className="h-3.5 w-3.5" />
|
||||||
</Button>
|
板块资金流向
|
||||||
</Link>
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link to="/themes">
|
||||||
|
<Button variant="outline" size="sm" className="gap-1.5 text-xs">
|
||||||
|
<Flame className="h-3.5 w-3.5" />
|
||||||
|
题材热点
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link to="/hot-map">
|
||||||
|
<Button variant="outline" size="sm" className="gap-1.5 text-xs">
|
||||||
|
<Network className="h-3.5 w-3.5" />
|
||||||
|
热点穿透
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="mb-6 md:mb-8 shadow-lg">
|
<Card className="mb-6 md:mb-8 shadow-lg">
|
||||||
@@ -517,7 +531,7 @@ function CollectionCard({
|
|||||||
<p className="text-sm text-muted-foreground text-center py-4">暂无股票</p>
|
<p className="text-sm text-muted-foreground text-center py-4">暂无股票</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{stocks.slice(0, 10).map((stock) => (
|
{stocks.slice(0, 20).map((stock) => (
|
||||||
<StockRowItem
|
<StockRowItem
|
||||||
key={stock.id}
|
key={stock.id}
|
||||||
stock={stock}
|
stock={stock}
|
||||||
@@ -526,8 +540,8 @@ function CollectionCard({
|
|||||||
swipeResetKey={swipeResetKey}
|
swipeResetKey={swipeResetKey}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{stocks.length > 10 && (
|
{stocks.length > 20 && (
|
||||||
<p className="text-xs text-muted-foreground text-center pt-1">还有 {stocks.length - 10} 只股票...</p>
|
<p className="text-xs text-muted-foreground text-center pt-1">还有 {stocks.length - 20} 只股票...</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+255
-120
@@ -1,49 +1,114 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||||
import { useState, useEffect, useMemo } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { fetchSectors, type SectorItem, type SectorType } from "@/lib/stock-api";
|
import { fetchSectors, type SectorItem, type SectorType } from "@/lib/stock-api";
|
||||||
|
import { formatMoney } from "@/lib/utils";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { ArrowLeft, TrendingUp, TrendingDown, RefreshCw } from "lucide-react";
|
import {
|
||||||
import { Link } from "@tanstack/react-router";
|
ArrowLeft,
|
||||||
|
RefreshCw,
|
||||||
|
ArrowDown,
|
||||||
|
ArrowUp,
|
||||||
|
TrendingUp,
|
||||||
|
TrendingDown,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
export const Route = createFileRoute("/sectors")({
|
export const Route = createFileRoute("/sectors")({
|
||||||
component: SectorsPage,
|
component: SectorsPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Tab 定义:行业 / 概念
|
||||||
|
============================================================ */
|
||||||
const TABS: { key: SectorType; label: string }[] = [
|
const TABS: { key: SectorType; label: string }[] = [
|
||||||
{ key: "industry", label: "行业板块" },
|
{ key: "industry", label: "行业" },
|
||||||
{ key: "concept", label: "概念板块" },
|
{ key: "concept", label: "概念" },
|
||||||
];
|
];
|
||||||
|
|
||||||
type SortMode = "mainNetInflow" | "changePercent";
|
/* ============================================================
|
||||||
|
排序维度
|
||||||
|
============================================================ */
|
||||||
|
type SortKey = "mainNetInflow" | "mainNetInflowPercent";
|
||||||
|
|
||||||
|
const SORT_LABEL: Record<SortKey, string> = {
|
||||||
|
mainNetInflow: "资金",
|
||||||
|
mainNetInflowPercent: "涨幅",
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
页面组件
|
||||||
|
============================================================ */
|
||||||
function SectorsPage() {
|
function SectorsPage() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const [tab, setTab] = useState<SectorType>("industry");
|
const [tab, setTab] = useState<SectorType>("industry");
|
||||||
const [data, setData] = useState<SectorItem[]>([]);
|
const [sortKey, setSortKey] = useState<SortKey>("mainNetInflow");
|
||||||
const [loading, setLoading] = useState(true);
|
const [asc, setAsc] = useState(true); // 默认升序
|
||||||
const [sort, setSort] = useState<SortMode>("mainNetInflow");
|
|
||||||
|
|
||||||
const loadData = (t: SectorType) => {
|
// ── 行业 / 概念各自独立 Query,缓存完全隔离 ──
|
||||||
setLoading(true);
|
const industryQ = useQuery({
|
||||||
fetchSectors(t).then((items) => {
|
queryKey: ["sectors", "industry"],
|
||||||
setData(items);
|
queryFn: ({ signal }) => fetchSectors("industry", signal),
|
||||||
setLoading(false);
|
staleTime: 30_000,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
const conceptQ = useQuery({
|
||||||
|
queryKey: ["sectors", "concept"],
|
||||||
|
queryFn: ({ signal }) => fetchSectors("concept", signal),
|
||||||
|
staleTime: 30_000,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当前激活的 tab 查询
|
||||||
|
const activeQuery = tab === "industry" ? industryQ : conceptQ;
|
||||||
|
const { isLoading, isFetching, isError, refetch } = activeQuery;
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
三层数据分离:缓存 → 排序 → 展示
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
// ① 缓存数据 — React Query 从后端拿到的原始数据
|
||||||
|
const cachedData: SectorItem[] = activeQuery.data ?? [];
|
||||||
|
|
||||||
|
// ② 排序数据 — 按当前排序规则在内存中重排
|
||||||
|
const sortedData = useMemo<SectorItem[]>(() => {
|
||||||
|
const dir = asc ? 1 : -1;
|
||||||
|
return [...cachedData].sort((a, b) => {
|
||||||
|
const av =
|
||||||
|
sortKey === "mainNetInflow"
|
||||||
|
? a.mainNetInflow
|
||||||
|
: (a.mainNetInflowPercent ?? -Infinity);
|
||||||
|
const bv =
|
||||||
|
sortKey === "mainNetInflow"
|
||||||
|
? b.mainNetInflow
|
||||||
|
: (b.mainNetInflowPercent ?? -Infinity);
|
||||||
|
return (bv - av) * dir;
|
||||||
});
|
});
|
||||||
|
}, [cachedData, sortKey, asc]);
|
||||||
|
|
||||||
|
// ③ 展示数据 — 最终渲染的数据集(当前即排序数据,后续可加分页截断)
|
||||||
|
const displayData = sortedData;
|
||||||
|
|
||||||
|
// ── 切换板块:清空全部缓存 + 重新获取 ──
|
||||||
|
const handleTab = (t: SectorType) => {
|
||||||
|
if (t === tab) return;
|
||||||
|
setTab(t);
|
||||||
|
// 移除所有板块缓存,切换后对应的 useQuery 会自动 refetch
|
||||||
|
queryClient.removeQueries({ queryKey: ["sectors"] });
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
// ── 切换排序 ──
|
||||||
loadData(tab);
|
const toggleSort = (key: SortKey) => {
|
||||||
}, [tab]);
|
if (key === sortKey) {
|
||||||
|
setAsc((v) => !v);
|
||||||
const sorted = useMemo(() => {
|
} else {
|
||||||
return [...data].sort((a, b) => {
|
setSortKey(key);
|
||||||
if (sort === "mainNetInflow") return b.mainNetInflow - a.mainNetInflow;
|
setAsc(true); // 切新维度默认升序
|
||||||
return (b.changePercent ?? 0) - (a.changePercent ?? 0);
|
}
|
||||||
});
|
};
|
||||||
}, [data, sort]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
{/* 顶栏 */}
|
{/* ── 顶栏 ── */}
|
||||||
<header className="sticky top-0 z-10 bg-background/95 backdrop-blur border-b">
|
<header className="sticky top-0 z-10 bg-background/95 backdrop-blur border-b">
|
||||||
<div className="max-w-5xl mx-auto px-4 h-12 flex items-center justify-between">
|
<div className="max-w-5xl mx-auto px-4 h-12 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -53,68 +118,95 @@ function SectorsPage() {
|
|||||||
<h1 className="text-base font-semibold">板块资金流向</h1>
|
<h1 className="text-base font-semibold">板块资金流向</h1>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => loadData(tab)}
|
onClick={() => refetch()}
|
||||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
title="刷新"
|
||||||
>
|
>
|
||||||
<RefreshCw className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />
|
<RefreshCw
|
||||||
|
className={`h-4 w-4 ${isFetching ? "animate-spin" : ""}`}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Tab + Sorting 切换 */}
|
{/* ── Tab 切换 ── */}
|
||||||
<div className="max-w-5xl mx-auto px-4 mt-4 flex items-center gap-2">
|
<div className="max-w-5xl mx-auto px-4 mt-4">
|
||||||
<div className="flex gap-1 bg-muted rounded-lg p-1 flex-1">
|
<div className="flex gap-1 bg-muted rounded-lg p-1">
|
||||||
{TABS.map((t) => (
|
{TABS.map((t) => (
|
||||||
<button
|
<button
|
||||||
key={t.key}
|
key={t.key}
|
||||||
onClick={() => setTab(t.key)}
|
onClick={() => handleTab(t.key)}
|
||||||
className={`flex-1 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
className={`flex-1 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
||||||
tab === t.key
|
tab === t.key
|
||||||
? "bg-background text-foreground shadow-sm"
|
? "bg-background text-foreground shadow-sm"
|
||||||
: "text-muted-foreground hover:text-foreground"
|
: "text-muted-foreground hover:text-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{t.label}
|
{t.label}板块
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-0.5 text-xs border rounded overflow-hidden shrink-0">
|
</div>
|
||||||
<button onClick={() => setSort("mainNetInflow")}
|
|
||||||
className={`px-2 py-1 transition-colors ${
|
{/* ── 排序切换 + 统计 ── */}
|
||||||
sort === "mainNetInflow"
|
<div className="max-w-5xl mx-auto px-4 mt-3 flex items-center justify-between">
|
||||||
? "bg-primary text-primary-foreground"
|
<p className="text-[10px] text-muted-foreground">
|
||||||
: "text-muted-foreground hover:text-foreground"
|
共 {cachedData.length} 个板块
|
||||||
}`}
|
{isFetching && (
|
||||||
>主力</button>
|
<span className="ml-1 text-[10px] text-muted-foreground/60">
|
||||||
<button onClick={() => setSort("changePercent")}
|
· 刷新中…
|
||||||
className={`px-2 py-1 transition-colors ${
|
</span>
|
||||||
sort === "changePercent"
|
)}
|
||||||
? "bg-primary text-primary-foreground"
|
</p>
|
||||||
: "text-muted-foreground hover:text-foreground"
|
<div className="flex gap-0.5 text-xs border rounded-md overflow-hidden">
|
||||||
}`}
|
{(Object.keys(SORT_LABEL) as SortKey[]).map((key) => (
|
||||||
>涨幅</button>
|
<button
|
||||||
|
key={key}
|
||||||
|
onClick={() => toggleSort(key)}
|
||||||
|
className={`px-2.5 py-1 flex items-center gap-0.5 transition-colors ${
|
||||||
|
sortKey === key
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "text-muted-foreground hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{SORT_LABEL[key]}
|
||||||
|
{sortKey === key &&
|
||||||
|
(asc ? <ArrowUp className="h-3 w-3" /> : <ArrowDown className="h-3 w-3" />)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 数据信息 */}
|
{/* ── 内容区 ── */}
|
||||||
<div className="max-w-5xl mx-auto px-4 mt-2">
|
|
||||||
<p className="text-[10px] text-muted-foreground">
|
|
||||||
含 {sorted.length} 个板块 · 按{sort === "mainNetInflow" ? "主力净流入" : "涨跌幅"}降序
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 卡片网格 */}
|
|
||||||
<div className="max-w-5xl mx-auto px-4 mt-3 pb-8">
|
<div className="max-w-5xl mx-auto px-4 mt-3 pb-8">
|
||||||
{loading ? (
|
{/* 加载骨架 */}
|
||||||
|
{isLoading ? (
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
|
||||||
{Array.from({ length: 20 }).map((_, i) => (
|
{Array.from({ length: 20 }).map((_, i) => (
|
||||||
<div key={i} className="animate-pulse rounded-xl bg-muted h-32" />
|
<div key={i} className="animate-pulse rounded-xl bg-muted h-40" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
) : isError ? (
|
||||||
|
/* 请求失败 */
|
||||||
|
<div className="flex flex-col items-center gap-3 py-20">
|
||||||
|
<p className="text-sm text-muted-foreground">数据加载失败</p>
|
||||||
|
<button
|
||||||
|
onClick={() => refetch()}
|
||||||
|
className="text-xs text-primary hover:underline"
|
||||||
|
>
|
||||||
|
点击重试
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : cachedData.length === 0 ? (
|
||||||
|
/* 数据为空 */
|
||||||
|
<div className="text-center py-20 text-sm text-muted-foreground">
|
||||||
|
暂无{tab === "industry" ? "行业" : "概念"}板块数据
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
/* 板块卡片网格 */
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
|
||||||
{sorted.map((item) => (
|
{displayData.map((item) => (
|
||||||
<SectorBlock key={item.code} item={item} />
|
<SectorCard key={item.code} item={item} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -123,54 +215,27 @@ function SectorsPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatInflow(val: number | null | undefined): string {
|
/* ============================================================
|
||||||
|
数值格式化
|
||||||
|
============================================================ */
|
||||||
|
function fmt(val: number | null | undefined, digits = 2): string {
|
||||||
if (val == null) return "--";
|
if (val == null) return "--";
|
||||||
const abs = Math.abs(val);
|
return val.toFixed(digits);
|
||||||
if (abs >= 1e8) return (val / 1e8).toFixed(2) + "亿";
|
|
||||||
if (abs >= 1e4) return (val / 1e4).toFixed(0) + "万";
|
|
||||||
return val.toFixed(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function FundFlowBar({ value, maxAbs }: { value: number | null | undefined; maxAbs: number }) {
|
/* ============================================================
|
||||||
if (value == null) return null;
|
板块卡片
|
||||||
const pct = maxAbs > 0 ? (value / maxAbs) * 100 : 0;
|
============================================================ */
|
||||||
const isPos = value >= 0;
|
function SectorCard({ item }: { item: SectorItem }) {
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<div className="flex-1 h-1.5 rounded-full bg-muted overflow-hidden">
|
|
||||||
<div
|
|
||||||
className={`h-full rounded-full transition-all ${
|
|
||||||
isPos ? "bg-red-500/60" : "bg-green-500/60"
|
|
||||||
}`}
|
|
||||||
style={{ width: `${Math.min(Math.abs(pct), 100)}%`, marginLeft: isPos ? "50%" : undefined }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className={`text-[10px] font-medium tabular-nums w-14 text-right ${
|
|
||||||
isPos ? "text-red-500" : "text-green-500"
|
|
||||||
}`}>
|
|
||||||
{formatInflow(value)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SectorBlock({ item }: { item: SectorItem }) {
|
|
||||||
const inflow = item.mainNetInflow;
|
|
||||||
const isPositive = inflow >= 0;
|
|
||||||
const change = item.changePercent;
|
const change = item.changePercent;
|
||||||
const maxAbs = Math.max(
|
const inflow = item.mainNetInflow;
|
||||||
Math.abs(item.mainNetInflow),
|
const inflowIsPos = inflow >= 0;
|
||||||
Math.abs(item.superLargeInflow ?? 0),
|
const inflowPct = item.mainNetInflowPercent;
|
||||||
Math.abs(item.largeInflow ?? 0),
|
|
||||||
Math.abs(item.mediumInflow ?? 0),
|
|
||||||
Math.abs(item.smallInflow ?? 0),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="rounded-xl hover:shadow-md transition-shadow">
|
<Card className="rounded-xl hover:shadow-md transition-shadow">
|
||||||
<CardContent className="p-3 space-y-2">
|
<CardContent className="p-3 space-y-1.5">
|
||||||
{/* 板块名称 */}
|
{/* 板块名称 + 代码 */}
|
||||||
<div className="flex items-center justify-between gap-1">
|
<div className="flex items-center justify-between gap-1">
|
||||||
<p className="text-sm font-medium truncate" title={item.name}>
|
<p className="text-sm font-medium truncate" title={item.name}>
|
||||||
{item.name}
|
{item.name}
|
||||||
@@ -182,7 +247,7 @@ function SectorBlock({ item }: { item: SectorItem }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 涨跌幅 */}
|
{/* 涨跌幅 + 成交额 */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{change != null ? (
|
{change != null ? (
|
||||||
<span
|
<span
|
||||||
@@ -196,36 +261,106 @@ function SectorBlock({ item }: { item: SectorItem }) {
|
|||||||
<TrendingDown className="h-3 w-3" />
|
<TrendingDown className="h-3 w-3" />
|
||||||
)}
|
)}
|
||||||
{change >= 0 ? "+" : ""}
|
{change >= 0 ? "+" : ""}
|
||||||
{change.toFixed(2)}%
|
{fmt(change)}%
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-xs text-muted-foreground">--</span>
|
<span className="text-xs text-muted-foreground">--</span>
|
||||||
)}
|
)}
|
||||||
<span className="text-[10px] text-muted-foreground">
|
<span className="text-[10px] text-muted-foreground">
|
||||||
{formatInflow(item.turnover)}
|
{formatMoney(item.turnover)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 主力净流入 */}
|
{/* 分割线 */}
|
||||||
<div className="pt-1.5 border-t border-border/40">
|
<hr className="border-border/40" />
|
||||||
<div className="flex items-center justify-between mb-1">
|
|
||||||
<span className="text-[10px] text-muted-foreground">主力净流入</span>
|
|
||||||
<span className={`text-xs font-bold tabular-nums ${
|
|
||||||
isPositive ? "text-red-500" : "text-green-500"
|
|
||||||
}`}>
|
|
||||||
{isPositive ? "+" : ""}{formatInflow(inflow)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 资金流向明细条 */}
|
{/* 主力净流入金额 + 占比 */}
|
||||||
<div className="space-y-0.5">
|
<div className="flex items-center justify-between">
|
||||||
<FundFlowBar value={item.superLargeInflow} maxAbs={maxAbs} />
|
<span className="text-[10px] text-muted-foreground">主力净流入</span>
|
||||||
<FundFlowBar value={item.largeInflow} maxAbs={maxAbs} />
|
<div className="flex items-center gap-2">
|
||||||
<FundFlowBar value={item.mediumInflow} maxAbs={maxAbs} />
|
<span
|
||||||
<FundFlowBar value={item.smallInflow} maxAbs={maxAbs} />
|
className={`text-xs font-bold tabular-nums ${
|
||||||
|
inflowIsPos ? "text-red-500" : "text-green-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{inflow >= 0 ? "+" : ""}
|
||||||
|
{formatMoney(inflow)}
|
||||||
|
</span>
|
||||||
|
{inflowPct != null && (
|
||||||
|
<span
|
||||||
|
className={`text-[10px] tabular-nums ${
|
||||||
|
inflowIsPos ? "text-red-500/70" : "text-green-500/70"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{inflow >= 0 ? "+" : ""}
|
||||||
|
{fmt(inflowPct)}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 资金流向明细条 */}
|
||||||
|
<FundFlowBreakdown item={item} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
资金流向明细 — 超大单 / 大单 / 中单 / 小单
|
||||||
|
============================================================ */
|
||||||
|
const FLOW_LABELS = [
|
||||||
|
{ key: "superLargeInflow" as const, label: "超大单" },
|
||||||
|
{ key: "largeInflow" as const, label: "大单" },
|
||||||
|
{ key: "mediumInflow" as const, label: "中单" },
|
||||||
|
{ key: "smallInflow" as const, label: "小单" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function FundFlowBreakdown({ item }: { item: SectorItem }) {
|
||||||
|
// 取所有流量的最大绝对值做归一化
|
||||||
|
const maxAbs = Math.max(
|
||||||
|
Math.abs(item.mainNetInflow),
|
||||||
|
Math.abs(item.superLargeInflow ?? 0),
|
||||||
|
Math.abs(item.largeInflow ?? 0),
|
||||||
|
Math.abs(item.mediumInflow ?? 0),
|
||||||
|
Math.abs(item.smallInflow ?? 0),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
{FLOW_LABELS.map((f) => {
|
||||||
|
const val = item[f.key];
|
||||||
|
if (val == null) return null;
|
||||||
|
const pct = maxAbs > 0 ? (Math.abs(val) / maxAbs) * 100 : 0;
|
||||||
|
const isPos = val >= 0;
|
||||||
|
return (
|
||||||
|
<div key={f.key} className="flex items-center gap-1.5">
|
||||||
|
<span className="text-[9px] text-muted-foreground w-6 shrink-0 text-right">
|
||||||
|
{f.label}
|
||||||
|
</span>
|
||||||
|
<div className="flex-1 h-1 rounded-full bg-muted overflow-hidden relative">
|
||||||
|
<div
|
||||||
|
className={`h-full rounded-full transition-all ${
|
||||||
|
isPos ? "bg-red-500/60 ml-1/2" : "bg-green-500/60"
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
width: `${Math.min(pct, 100)}%`,
|
||||||
|
marginLeft: isPos ? "50%" : undefined,
|
||||||
|
marginRight: isPos ? undefined : `${100 - Math.min(pct, 100)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`text-[9px] font-medium tabular-nums w-14 text-right shrink-0 ${
|
||||||
|
isPos ? "text-red-500" : "text-green-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{formatMoney(val)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,8 +63,14 @@ interface StockInfo {
|
|||||||
addedPrice: number | null;
|
addedPrice: number | null;
|
||||||
outerDisk: number; // 外盘(手)
|
outerDisk: number; // 外盘(手)
|
||||||
innerDisk: number; // 内盘(手)
|
innerDisk: number; // 内盘(手)
|
||||||
|
volume: number; // 成交量(股)
|
||||||
amount: number; // 成交额(元)
|
amount: number; // 成交额(元)
|
||||||
quoteDate: string; // 行情日期 YYYY-MM-DD
|
quoteDate: string; // 行情日期 YYYY-MM-DD
|
||||||
|
turnoverRate: number; // 换手率%
|
||||||
|
pe: number; // 市盈率
|
||||||
|
pb: number; // 市净率
|
||||||
|
totalMarketCap: number; // 总市值
|
||||||
|
circulatingMarketCap: number; // 流通市值
|
||||||
}
|
}
|
||||||
|
|
||||||
function StockDetail() {
|
function StockDetail() {
|
||||||
@@ -155,6 +161,12 @@ function StockDetail() {
|
|||||||
innerDisk: quote.innerDisk || 0,
|
innerDisk: quote.innerDisk || 0,
|
||||||
amount: (quote.amount || 0) * 10000,
|
amount: (quote.amount || 0) * 10000,
|
||||||
quoteDate: quote.date,
|
quoteDate: quote.date,
|
||||||
|
volume: quote.volume || 0,
|
||||||
|
turnoverRate: quote.turnoverRate ?? 0,
|
||||||
|
pe: quote.pe ?? 0,
|
||||||
|
pb: quote.pb ?? 0,
|
||||||
|
totalMarketCap: quote.totalMarketCap ?? 0,
|
||||||
|
circulatingMarketCap: quote.circulatingMarketCap ?? 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 基础信息已就绪,结束主loading,先渲染页面框架
|
// 基础信息已就绪,结束主loading,先渲染页面框架
|
||||||
@@ -267,6 +279,14 @@ function StockDetail() {
|
|||||||
negativeDays: negative,
|
negativeDays: negative,
|
||||||
};
|
};
|
||||||
}, [fundFlowSlice]);
|
}, [fundFlowSlice]);
|
||||||
|
|
||||||
|
// 从财务数据中提取最新ROE(加权净资产收益率)
|
||||||
|
const latestROE = useMemo(() => {
|
||||||
|
if (!financialData?.data?.length) return null;
|
||||||
|
const latest = financialData.data[0];
|
||||||
|
const roe = latest.indicators["加权净资产收益率"];
|
||||||
|
return typeof roe === "number" ? roe : null;
|
||||||
|
}, [financialData]);
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
@@ -331,12 +351,8 @@ function StockDetail() {
|
|||||||
ticks.push(added);
|
ticks.push(added);
|
||||||
}
|
}
|
||||||
ticks.push(last);
|
ticks.push(last);
|
||||||
return [...new Set(ticks)].sort((a, b) => {
|
// 保持chartData的原始顺序(已按日期升序排列),不做二次排序
|
||||||
// 按日期排序(MM/DD格式需按MM和DD比较)
|
return [...new Set(ticks)];
|
||||||
const [am, ad] = a.split('/').map(Number);
|
|
||||||
const [bm, bd] = b.split('/').map(Number);
|
|
||||||
return am - bm || ad - bd;
|
|
||||||
});
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// 东方财富市场标识:0=深圳(000/002/300), 1=上海(60), 6=科创板(688)
|
// 东方财富市场标识:0=深圳(000/002/300), 1=上海(60), 6=科创板(688)
|
||||||
@@ -447,6 +463,48 @@ function StockDetail() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 行情指标合并到顶部卡片 */}
|
||||||
|
<div className="mt-4 md:mt-5 pt-4 md:pt-5 border-t border-border/50">
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 md:gap-3">
|
||||||
|
<div className="text-center p-2 md:p-3 rounded-lg bg-muted/30">
|
||||||
|
<p className="text-[10px] md:text-xs text-muted-foreground mb-0.5">市盈率(PE)</p>
|
||||||
|
<p className="text-sm md:text-base font-semibold">{stockInfo.pe > 0 ? stockInfo.pe.toFixed(2) : '-'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-2 md:p-3 rounded-lg bg-muted/30">
|
||||||
|
<p className="text-[10px] md:text-xs text-muted-foreground mb-0.5">市净率(PB)</p>
|
||||||
|
<p className="text-sm md:text-base font-semibold">{stockInfo.pb > 0 ? stockInfo.pb.toFixed(2) : '-'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-2 md:p-3 rounded-lg bg-muted/30">
|
||||||
|
<p className="text-[10px] md:text-xs text-muted-foreground mb-0.5">ROE</p>
|
||||||
|
<p className={`text-sm md:text-base font-semibold ${latestROE !== null ? (latestROE > 0 ? 'text-red-500' : 'text-green-500') : ''}`}>
|
||||||
|
{latestROE !== null ? `${latestROE.toFixed(2)}%` : '加载中...'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-2 md:p-3 rounded-lg bg-muted/30">
|
||||||
|
<p className="text-[10px] md:text-xs text-muted-foreground mb-0.5">换手率</p>
|
||||||
|
<p className="text-sm md:text-base font-semibold">{stockInfo.turnoverRate > 0 ? `${stockInfo.turnoverRate}%` : '-'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-2 md:p-3 rounded-lg bg-muted/30">
|
||||||
|
<p className="text-[10px] md:text-xs text-muted-foreground mb-0.5">总手</p>
|
||||||
|
<p className="text-sm md:text-base font-semibold">
|
||||||
|
{stockInfo.volume > 0 ? `${(stockInfo.volume / 100).toLocaleString()}手` : '-'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-2 md:p-3 rounded-lg bg-muted/30">
|
||||||
|
<p className="text-[10px] md:text-xs text-muted-foreground mb-0.5">成交额</p>
|
||||||
|
<p className="text-sm md:text-base font-semibold">{formatMoney(stockInfo.amount)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-2 md:p-3 rounded-lg bg-muted/30">
|
||||||
|
<p className="text-[10px] md:text-xs text-muted-foreground mb-0.5">总市值</p>
|
||||||
|
<p className="text-sm md:text-base font-semibold">{stockInfo.totalMarketCap > 0 ? formatMoney(stockInfo.totalMarketCap) : '-'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-2 md:p-3 rounded-lg bg-muted/30">
|
||||||
|
<p className="text-[10px] md:text-xs text-muted-foreground mb-0.5">流通市值</p>
|
||||||
|
<p className="text-sm md:text-base font-semibold">{stockInfo.circulatingMarketCap > 0 ? formatMoney(stockInfo.circulatingMarketCap) : '-'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -653,6 +711,8 @@ function StockDetail() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Daily Data Table - Independent from fund flow */}
|
{/* Daily Data Table - Independent from fund flow */}
|
||||||
{chartData.length > 0 && (
|
{chartData.length > 0 && (
|
||||||
<Card className="mt-4 md:mt-6 shadow-lg">
|
<Card className="mt-4 md:mt-6 shadow-lg">
|
||||||
|
|||||||
@@ -0,0 +1,355 @@
|
|||||||
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { fetchThemeDetail, fetchThemeStocks, type ThemeStock } from "@/lib/theme-api";
|
||||||
|
import { getStockBoard } from "@/lib/stock-api";
|
||||||
|
import { formatMoney } from "@/lib/utils";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
RefreshCw,
|
||||||
|
TrendingUp,
|
||||||
|
TrendingDown,
|
||||||
|
Flame,
|
||||||
|
Newspaper,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
Info,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/theme/$code")({
|
||||||
|
component: ThemeDetailPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
function ThemeDetailPage() {
|
||||||
|
const { code } = Route.useParams();
|
||||||
|
|
||||||
|
const detailQ = useQuery({
|
||||||
|
queryKey: ["themeDetail", code],
|
||||||
|
queryFn: () => fetchThemeDetail(code),
|
||||||
|
staleTime: 60_000,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
const stocksQ = useQuery({
|
||||||
|
queryKey: ["themeStocks", code],
|
||||||
|
queryFn: () => fetchThemeStocks(code),
|
||||||
|
staleTime: 30_000,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isLoading = detailQ.isLoading || stocksQ.isLoading;
|
||||||
|
const isError = detailQ.isError || stocksQ.isError;
|
||||||
|
const isFetching = detailQ.isFetching || stocksQ.isFetching;
|
||||||
|
|
||||||
|
const detail = detailQ.data;
|
||||||
|
const stocks = stocksQ.data?.data ?? [];
|
||||||
|
const statistic = stocksQ.data?.statistic;
|
||||||
|
const total = stocksQ.data?.total ?? 0;
|
||||||
|
|
||||||
|
const refresh = () => {
|
||||||
|
detailQ.refetch();
|
||||||
|
stocksQ.refetch();
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseInfo = detail?.baseInfo;
|
||||||
|
const hotEvent = detail?.hotEvent;
|
||||||
|
const eventHistory = detail?.eventHistory ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
{/* ── 顶栏 ── */}
|
||||||
|
<header className="sticky top-0 z-10 bg-background/95 backdrop-blur border-b">
|
||||||
|
<div className="max-w-3xl mx-auto px-4 h-12 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
|
<Link to="/themes" className="hover:opacity-70 transition-opacity shrink-0">
|
||||||
|
<ArrowLeft className="h-5 w-5" />
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-base font-semibold truncate">{baseInfo?.themeName ?? "题材详情"}</h1>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={refresh}
|
||||||
|
className="text-muted-foreground hover:text-foreground transition-colors shrink-0"
|
||||||
|
title="刷新"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`h-4 w-4 ${isFetching ? "animate-spin" : ""}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="max-w-3xl mx-auto px-4 py-4 pb-10 space-y-4">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="animate-pulse rounded-xl bg-muted h-32" />
|
||||||
|
<div className="animate-pulse rounded-xl bg-muted h-24" />
|
||||||
|
<div className="animate-pulse rounded-xl bg-muted h-64" />
|
||||||
|
</div>
|
||||||
|
) : isError ? (
|
||||||
|
<div className="flex flex-col items-center gap-3 py-20">
|
||||||
|
<p className="text-sm text-muted-foreground">数据加载失败</p>
|
||||||
|
<button onClick={refresh} className="text-xs text-primary hover:underline">
|
||||||
|
点击重试
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* ── 题材简介 ── */}
|
||||||
|
{baseInfo?.introduction && (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-1.5 mb-2">
|
||||||
|
<Info className="h-4 w-4 text-primary" />
|
||||||
|
<h2 className="text-sm font-semibold">题材简介</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
{baseInfo.introduction}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── 热点事件 ── */}
|
||||||
|
{hotEvent?.newsTitle && (
|
||||||
|
<Card className="border-orange-500/30">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-1.5 mb-2">
|
||||||
|
<Flame className="h-4 w-4 text-orange-500" />
|
||||||
|
<h2 className="text-sm font-semibold">热点事件</h2>
|
||||||
|
{hotEvent.newsMediaName && (
|
||||||
|
<span className="text-[10px] text-muted-foreground ml-auto shrink-0">
|
||||||
|
{hotEvent.newsMediaName}
|
||||||
|
{hotEvent.newsPublishTimeFormat ? ` · ${hotEvent.newsPublishTimeFormat}` : ""}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-medium leading-snug">{hotEvent.newsTitle}</p>
|
||||||
|
{hotEvent.newsSummary && (
|
||||||
|
<p className="text-xs text-muted-foreground leading-relaxed mt-1.5 line-clamp-3">
|
||||||
|
{hotEvent.newsSummary}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── 板块统计 ── */}
|
||||||
|
<StatBar
|
||||||
|
f3={statistic?.f3}
|
||||||
|
up={statistic?.f104}
|
||||||
|
down={statistic?.f105}
|
||||||
|
flat={statistic?.f106}
|
||||||
|
fex5={statistic?.fex5}
|
||||||
|
total={total}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* ── 相关新闻(可折叠) ── */}
|
||||||
|
{eventHistory.length > 0 && <NewsList items={eventHistory} />}
|
||||||
|
|
||||||
|
{/* ── 相关股票 ── */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-2 px-1">
|
||||||
|
<h2 className="text-sm font-semibold">相关股票</h2>
|
||||||
|
<span className="text-[10px] text-muted-foreground">共 {total} 只</span>
|
||||||
|
</div>
|
||||||
|
{stocks.length === 0 ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6 text-center text-sm text-muted-foreground">
|
||||||
|
暂无相关股票
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{stocks.map((s) => (
|
||||||
|
<ThemeStockRow key={s.securityCode} stock={s} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
板块统计条
|
||||||
|
============================================================ */
|
||||||
|
function StatBar({
|
||||||
|
f3,
|
||||||
|
up,
|
||||||
|
down,
|
||||||
|
flat,
|
||||||
|
fex5,
|
||||||
|
total,
|
||||||
|
}: {
|
||||||
|
f3: number | null | undefined;
|
||||||
|
up: number | null | undefined;
|
||||||
|
down: number | null | undefined;
|
||||||
|
flat: number | null | undefined;
|
||||||
|
fex5: number | null | undefined;
|
||||||
|
total: number;
|
||||||
|
}) {
|
||||||
|
const isPos = (f3 ?? 0) >= 0;
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-3">
|
||||||
|
<div className="grid grid-cols-4 divide-x divide-border/50 text-center">
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-muted-foreground">板块涨幅</p>
|
||||||
|
<p className={`text-sm font-bold tabular-nums ${isPos ? "text-red-500" : "text-green-500"}`}>
|
||||||
|
{f3 != null ? `${isPos ? "+" : ""}${f3.toFixed(2)}%` : "--"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-muted-foreground">上涨</p>
|
||||||
|
<p className="text-sm font-bold tabular-nums text-red-500">{up ?? "--"}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-muted-foreground">下跌</p>
|
||||||
|
<p className="text-sm font-bold tabular-nums text-green-500">{down ?? "--"}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-muted-foreground">成交额</p>
|
||||||
|
<p className="text-xs font-semibold tabular-nums">{fex5 != null ? formatMoney(fex5) : "--"}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{(flat != null && flat > 0) && (
|
||||||
|
<p className="text-[10px] text-muted-foreground text-center mt-1.5">
|
||||||
|
平盘 {flat} 只
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
相关新闻(可折叠)
|
||||||
|
============================================================ */
|
||||||
|
function NewsList({ items }: { items: { newsTitle: string; newsMediaName: string; newsPublishTime: number | null }[] }) {
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const shown = expanded ? items : items.slice(0, 2);
|
||||||
|
|
||||||
|
const fmtTime = (ts: number | null) => {
|
||||||
|
if (!ts) return "";
|
||||||
|
const d = new Date(ts);
|
||||||
|
const pad = (n: number) => String(n).padStart(2, "0");
|
||||||
|
return `${d.getMonth() + 1}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-1.5 mb-2">
|
||||||
|
<Newspaper className="h-4 w-4 text-primary" />
|
||||||
|
<h2 className="text-sm font-semibold">相关新闻</h2>
|
||||||
|
<span className="text-[10px] text-muted-foreground ml-auto">{items.length} 条</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
{shown.map((n, idx) => (
|
||||||
|
<div key={idx} className="space-y-0.5">
|
||||||
|
<p className="text-sm leading-snug line-clamp-2">{n.newsTitle}</p>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
{n.newsMediaName}
|
||||||
|
{n.newsPublishTime ? ` · ${fmtTime(n.newsPublishTime)}` : ""}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{items.length > 2 && (
|
||||||
|
<button
|
||||||
|
onClick={() => setExpanded((v) => !v)}
|
||||||
|
className="mt-2 text-xs text-primary hover:underline inline-flex items-center gap-0.5"
|
||||||
|
>
|
||||||
|
{expanded ? "收起" : `展开全部 ${items.length} 条`}
|
||||||
|
{expanded ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
相关股票行
|
||||||
|
============================================================ */
|
||||||
|
function ThemeStockRow({ stock }: { stock: ThemeStock }) {
|
||||||
|
const [showReason, setShowReason] = useState(true); // 入选理由默认展开
|
||||||
|
const board = getStockBoard(stock.securityCode);
|
||||||
|
const isPos = stock.f3 >= 0;
|
||||||
|
const reasons = stock.keywordList ?? [];
|
||||||
|
|
||||||
|
// 换手率:接口返回放大 100 倍的值(如 3733 = 37.33%)
|
||||||
|
const turnoverRate = stock.f8 > 100 ? stock.f8 / 100 : stock.f8;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to="/stock/$code" params={{ code: stock.securityCode }} className="block">
|
||||||
|
<Card className="rounded-xl hover:shadow-md transition-shadow">
|
||||||
|
<CardContent className="p-3 space-y-1.5">
|
||||||
|
{/* 名称 + 现价 + 涨幅 */}
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<div className="flex items-center gap-1.5 min-w-0">
|
||||||
|
<p className="text-sm font-medium truncate">{stock.securityName}</p>
|
||||||
|
{board.label && (
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center justify-center w-3.5 h-3.5 rounded-sm text-[8px] font-bold leading-none shrink-0 ${board.className}`}
|
||||||
|
>
|
||||||
|
{board.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{stock.label && (
|
||||||
|
<span className="shrink-0 text-[9px] font-medium text-orange-500 bg-orange-500/10 border border-orange-500/30 rounded px-1 py-0.5">
|
||||||
|
{stock.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 shrink-0">
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-[10px] text-muted-foreground">现价</p>
|
||||||
|
<p className="text-sm font-semibold tabular-nums">{stock.f2.toFixed(2)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right min-w-[56px]">
|
||||||
|
<p className="text-[10px] text-muted-foreground">涨跌</p>
|
||||||
|
<p className={`text-sm font-bold tabular-nums ${isPos ? "text-red-500" : "text-green-500"}`}>
|
||||||
|
{isPos ? "+" : ""}
|
||||||
|
{stock.f3.toFixed(2)}%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 行业 + 换手 + 主力 + 成交额 */}
|
||||||
|
<div className="flex items-center gap-2 text-[11px] text-muted-foreground">
|
||||||
|
{stock.f100 && (
|
||||||
|
<span className="truncate bg-muted rounded px-1.5 py-0.5 text-[10px]">{stock.f100}</span>
|
||||||
|
)}
|
||||||
|
<span className="shrink-0 tabular-nums">换手 {turnoverRate.toFixed(2)}%</span>
|
||||||
|
<span className="shrink-0 tabular-nums">主力 {formatMoney(stock.f62)}</span>
|
||||||
|
<span className="shrink-0 tabular-nums ml-auto">成交 {formatMoney(stock.f6)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 入选理由 */}
|
||||||
|
{reasons.length > 0 && (
|
||||||
|
<div className="border-t border-border/40 pt-1.5">
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowReason((v) => !v);
|
||||||
|
}}
|
||||||
|
className="text-[10px] text-primary hover:underline inline-flex items-center gap-0.5"
|
||||||
|
>
|
||||||
|
入选理由
|
||||||
|
{showReason ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
|
||||||
|
</button>
|
||||||
|
{showReason && (
|
||||||
|
<p className="text-[11px] text-muted-foreground leading-relaxed mt-1">
|
||||||
|
{reasons.map((r) => r.introduction).filter(Boolean).join(" ")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { fetchThemes, type ThemeItem, type ThemeSortField } from "@/lib/theme-api";
|
||||||
|
import { getStockBoard } from "@/lib/stock-api";
|
||||||
|
import { formatMoney } from "@/lib/utils";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
RefreshCw,
|
||||||
|
ArrowDown,
|
||||||
|
ArrowUp,
|
||||||
|
TrendingUp,
|
||||||
|
TrendingDown,
|
||||||
|
Flame,
|
||||||
|
Network,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/themes")({
|
||||||
|
component: ThemesPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
排序维度(sortField 与后端/东方财富对齐)
|
||||||
|
============================================================ */
|
||||||
|
const SORTS: { key: ThemeSortField; label: string }[] = [
|
||||||
|
{ key: 1, label: "涨幅" },
|
||||||
|
{ key: 3, label: "强度" },
|
||||||
|
{ key: 4, label: "热度" },
|
||||||
|
{ key: 5, label: "成交额" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function ThemesPage() {
|
||||||
|
const [sortField, setSortField] = useState<ThemeSortField>(1);
|
||||||
|
const [asc, setAsc] = useState(false); // 默认降序
|
||||||
|
|
||||||
|
const { data: themes, isLoading, isFetching, isError, refetch } = useQuery({
|
||||||
|
queryKey: ["themes", sortField, asc],
|
||||||
|
queryFn: () => fetchThemes(sortField, asc),
|
||||||
|
staleTime: 30_000,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = themes ?? [];
|
||||||
|
|
||||||
|
const toggleSort = (key: ThemeSortField) => {
|
||||||
|
if (key === sortField) {
|
||||||
|
setAsc((v) => !v);
|
||||||
|
} else {
|
||||||
|
setSortField(key);
|
||||||
|
setAsc(false); // 切新维度默认降序
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
{/* ── 顶栏 ── */}
|
||||||
|
<header className="sticky top-0 z-10 bg-background/95 backdrop-blur border-b">
|
||||||
|
<div className="max-w-5xl mx-auto px-4 h-12 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Link to="/" className="hover:opacity-70 transition-opacity">
|
||||||
|
<ArrowLeft className="h-5 w-5" />
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-base font-semibold">题材热点</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Link
|
||||||
|
to="/hot-map"
|
||||||
|
className="text-xs text-primary flex items-center gap-1 hover:opacity-80 transition-opacity"
|
||||||
|
>
|
||||||
|
<Network className="h-3.5 w-3.5" />
|
||||||
|
热点穿透
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={() => refetch()}
|
||||||
|
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
title="刷新"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`h-4 w-4 ${isFetching ? "animate-spin" : ""}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* ── 排序切换 + 统计 ── */}
|
||||||
|
<div className="max-w-5xl mx-auto px-4 mt-3 flex items-center justify-between">
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
共 {data.length} 个题材
|
||||||
|
{isFetching && (
|
||||||
|
<span className="ml-1 text-[10px] text-muted-foreground/60">· 刷新中…</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-0.5 text-xs border rounded-md overflow-hidden">
|
||||||
|
{SORTS.map((s) => (
|
||||||
|
<button
|
||||||
|
key={s.key}
|
||||||
|
onClick={() => toggleSort(s.key)}
|
||||||
|
className={`px-2.5 py-1 flex items-center gap-0.5 transition-colors ${
|
||||||
|
sortField === s.key
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "text-muted-foreground hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{s.label}
|
||||||
|
{sortField === s.key &&
|
||||||
|
(asc ? <ArrowUp className="h-3 w-3" /> : <ArrowDown className="h-3 w-3" />)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── 内容区 ── */}
|
||||||
|
<div className="max-w-5xl mx-auto px-4 mt-3 pb-8">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
|
{Array.from({ length: 18 }).map((_, i) => (
|
||||||
|
<div key={i} className="animate-pulse rounded-xl bg-muted h-32" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : isError ? (
|
||||||
|
<div className="flex flex-col items-center gap-3 py-20">
|
||||||
|
<p className="text-sm text-muted-foreground">数据加载失败</p>
|
||||||
|
<button onClick={() => refetch()} className="text-xs text-primary hover:underline">
|
||||||
|
点击重试
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : data.length === 0 ? (
|
||||||
|
<div className="text-center py-20 text-sm text-muted-foreground">暂无题材数据</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
|
{data.map((item) => (
|
||||||
|
<ThemeCard key={item.themeCode} item={item} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
数值格式化
|
||||||
|
============================================================ */
|
||||||
|
function fmt(val: number | null | undefined, digits = 2): string {
|
||||||
|
if (val == null) return "--";
|
||||||
|
return val.toFixed(digits);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
题材卡片
|
||||||
|
============================================================ */
|
||||||
|
function ThemeCard({ item }: { item: ThemeItem }) {
|
||||||
|
const change = item.bf3;
|
||||||
|
const hotPct =
|
||||||
|
item.hotValueUpLimit > 0 ? Math.min((item.hotValue / item.hotValueUpLimit) * 100, 100) : 0;
|
||||||
|
const stockBoard = getStockBoard(item.securityCode);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to="/theme/$code" params={{ code: item.themeCode }} className="block">
|
||||||
|
<Card className="rounded-xl hover:shadow-md transition-shadow h-full">
|
||||||
|
<CardContent className="p-3 space-y-2">
|
||||||
|
{/* 题材名 + 领涨标签 */}
|
||||||
|
<div className="flex items-center justify-between gap-1">
|
||||||
|
<p className="text-sm font-medium truncate" title={item.themeName}>
|
||||||
|
{item.themeName}
|
||||||
|
</p>
|
||||||
|
{item.label && (
|
||||||
|
<span className="shrink-0 text-[9px] font-medium text-orange-500 bg-orange-500/10 border border-orange-500/30 rounded px-1 py-0.5">
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 热度进度条 */}
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Flame className="h-3 w-3 text-orange-500 shrink-0" />
|
||||||
|
<div className="flex-1 h-1.5 rounded-full bg-muted overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full bg-gradient-to-r from-orange-400 to-red-500"
|
||||||
|
style={{ width: `${Math.max(hotPct, 2)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] text-muted-foreground tabular-nums shrink-0">
|
||||||
|
{item.hotValue}/{item.hotValueUpLimit}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 涨幅 + 成交额 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
{change != null ? (
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center gap-0.5 text-xs font-semibold ${
|
||||||
|
change >= 0 ? "text-red-500" : "text-green-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{change >= 0 ? <TrendingUp className="h-3 w-3" /> : <TrendingDown className="h-3 w-3" />}
|
||||||
|
{change >= 0 ? "+" : ""}
|
||||||
|
{fmt(change)}%
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-muted-foreground">--</span>
|
||||||
|
)}
|
||||||
|
<span className="text-[10px] text-muted-foreground">
|
||||||
|
{item.fex5 != null ? formatMoney(item.fex5) : "--"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分割线 */}
|
||||||
|
<hr className="border-border/40" />
|
||||||
|
|
||||||
|
{/* 领涨股 + 强度 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-1 min-w-0">
|
||||||
|
<span className="text-[10px] text-muted-foreground shrink-0">领涨</span>
|
||||||
|
<span className="text-xs font-medium truncate">{item.securityName || "--"}</span>
|
||||||
|
{stockBoard.label && (
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center justify-center w-3.5 h-3.5 rounded-sm text-[8px] font-bold leading-none shrink-0 ${stockBoard.className}`}
|
||||||
|
>
|
||||||
|
{stockBoard.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
|
{item.f3 != null && (
|
||||||
|
<span
|
||||||
|
className={`text-[10px] tabular-nums ${
|
||||||
|
item.f3 >= 0 ? "text-red-500" : "text-green-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.f3 >= 0 ? "+" : ""}
|
||||||
|
{fmt(item.f3)}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="text-[10px] text-muted-foreground tabular-nums">
|
||||||
|
强度 {item.strengthValue ?? "--"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user