Files
auv/backend/routes/themes.py
T
SakurasanandClaude 548ebee47f perf: 优化热点穿透加载速度 + Canvas 渲染 + 触控板/手机交互
后端加载优化(盘中原本每次访问都重新聚合,耗时 10-30 秒):
- 图聚合结果与题材股票子层盘中加 60 秒缓存
- 缓存过期时返回旧数据并后台幂等重建(stale-while-revalidate),打开即秒开
- 缓存基础支持秒级 TTL(set_cache 新增 ttl_seconds)
- 响应体瘦身:移除 edges(前端从 stocks[].themeCodes 重建)、
  themes 精简字段、stocks 按 limit 裁剪,JSON 从数 MB 降至数百 KB

前端 Canvas 渲染重构:
- d3-force 布局保留,SVG 渲染层替换为 Canvas 双缓冲(静止态离屏层 drawImage)
- tick 由每帧 setState 改为 rAF 合帧,拖拽/缩放期间零 React 重渲染,800 节点流畅

交互优化:
- 手机:新增 +/−/适应 缩放按钮、命中半径放大至 22px、tap/drag 6px 阈值区分、
  双指捏合缩放、触屏禁用 hover 避免与选中冲突
- Mac 触控板:双指滚动=平移、捏合(ctrlKey)=缩放、滚轮缩放保留、点空白取消选中

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 13:21:08 +08:00

67 lines
2.4 KiB
Python

"""题材数据路由:题材列表、题材详情、题材相关股票"""
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,
)