feat: K线增加分钟/小时周期切换,拆分独立KLineCard组件

- 后端:腾讯 mkline 新增 /api/stock/history-minute(m1/m5/m15/m30/m60)
- 新组件 kline-card.tsx:自包含周期/折线蜡烛/指标开关与K线数据加载,
  切周期只重拉K线,不刷新页面其他模块
- 详情页瘦身为独立模块:K线图 / 今开最高最低昨收 / 每日行情明细互不耦合
- 时间统一按 UTC 解析传 Unix 秒,修复日线 invalid date/N/A 与分钟线时区问题
- 资金流向失败降级为非致命,不再导致整页报错
- vite 构建拆分 recharts/lightweight-charts/router 独立 chunk
This commit is contained in:
Sakurasan
2026-08-28 17:43:11 +08:00
parent c4ed3346ab
commit b1f216b2a4
8 changed files with 396 additions and 253 deletions
+17
View File
@@ -84,6 +84,23 @@ async def stock_history(
raise HTTPException(status_code=404, detail="未获取到K线数据")
@router.get("/history-minute", summary="分钟/小时级K线")
async def stock_history_minute(
code: str = Query(..., description="6位股票代码"),
period: str = Query("60", description="周期:m1/m5/m15/m30/m60(60=小时线)"),
count: int = Query(320, description="K线数量"),
):
if not re.match(r"^\d{6}$", code):
raise HTTPException(status_code=400, detail="股票代码格式错误,需为6位数字")
if period not in ("m1", "m5", "m15", "m30", "m60"):
raise HTTPException(status_code=400, detail="period 仅支持 m1/m5/m15/m30/m60")
klines = await tencent.fetch_history_minute(code, period, count)
if not klines:
raise HTTPException(status_code=404, detail="未获取到分钟K线数据")
return {"data": klines, "count": len(klines), "source": "tencent-minute"}
@router.get("/profile", response_model=dict, summary="公司概况")
async def company_profile(code: str = Query(..., description="6位股票代码")):
"""获取东方财富F10公司概况数据"""
+58
View File
@@ -257,6 +257,64 @@ async def fetch_history(code: str, days: int = 90) -> List[dict]:
return []
async def fetch_history_minute(code: str, period: str = "60", count: int = 320) -> List[dict]:
"""获取分钟级 K 线(腾讯 mkline 接口)
period: m1/m5/m15/m30/m60(60=小时线)
返回格式与日K一致,date 为 'YYYY-MM-DD HH:MM'(北京时间)。
"""
market = get_market_prefix(code)
stock_code = f"{market}{code}"
# 腾讯 mkline:param=代码,周期,,数量
url = f"https://ifzq.gtimg.cn/appstock/app/kline/mkline?param={stock_code},{period},,{count}"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Referer": "https://stockapp.finance.qq.com/",
}
async with httpx.AsyncClient() as client:
try:
resp = await client.get(url, headers=headers, timeout=10)
if resp.status_code != 200:
return []
data = resp.json()
stock_data = data.get("data", {}).get(stock_code, {})
raw = stock_data.get(period) or []
if not isinstance(raw, list):
return []
result = []
prev_close = 0
for record in raw:
if not isinstance(record, list) or len(record) < 6:
continue
# record[0] = 'YYYYMMDDHHMM',转成 'YYYY-MM-DD HH:MM'
raw_dt = str(record[0])
try:
dt_str = f"{raw_dt[0:4]}-{raw_dt[4:6]}-{raw_dt[6:8]} {raw_dt[8:10]}:{raw_dt[10:12]}"
except Exception:
continue
close = float(record[2]) if record[2] else 0
change_pct = 0
if prev_close > 0:
change_pct = (close - prev_close) / prev_close * 100
result.append({
"date": dt_str,
"open": float(record[1]) if record[1] else 0,
"close": close,
"high": float(record[3]) if record[3] else 0,
"low": float(record[4]) if record[4] else 0,
"volume": int(float(record[5])) if record[5] else 0,
"changePercent": round(change_pct, 2),
})
prev_close = close
# 丢弃最旧1条(prev_close=0 涨跌幅失真)
if len(result) > 1:
result = result[1:]
return result
except Exception:
return []
async def fetch_kline_map(code: str, days: int = 30) -> dict:
"""获取K线数据并返回 { date: { close, changePercent, turnover } } 映射"""
market = get_market_prefix(code)