本项目站内源代码下载地址https://download.csdn.net/download/s060403072/93356585
文章目录
一、概览
KLine Studio 是一个全栈金融图表应用,核心能力是将用户在前端配置的图表参数(图表类型、主题、技术指标组合等)实时传递给 Python 端的 mplfinance 渲染引擎,生成专业级 K 线图 PNG 图片后回传前端展示。整个链路涉及 TypeScript 前端 → Node.js API → Python 子进程 → matplotlib 渲染 → Base64 图片回流 的跨语言协作。

🎯 核心特性
| 特性 | 说明 |
|---|---|
| 多图表类型 | 蜡烛图(Candlestick)、OHLC 线形图、折线图三种渲染模式 |
| 技术指标叠加 | MA 均线、布林带(BOLL)、MACD、RSI、成交量,支持独立开关与参数调节 |
| 双配色体系 | A 股红涨绿跌(CN)与国际绿涨红跌(INTL)两种配色方案 |
| 明暗双主题 | 深色(Dark)与浅色(Light)双主题切换 |
| PNG 导出 | 一键下载渲染后的高清 K 线图 |
| 模拟行情 | 基于几何布朗运动的确定性种子数据生成,5 只覆盖消费/新能源/金融/科技/交运的模拟标的 |
| 防抖渲染 | 450ms 防抖 + AbortController 竞态取消,避免参数频繁变更时的无效计算 |
二、整体架构设计
2.1 架构总览
系统采用 三层分离 架构:前端展示层(Next.js App Router)、API 桥接层(Node.js Route Handler)、渲染引擎层(Python mplfinance)。数据从用户交互出发,经 API 层桥接后由 Python 进程完成最终绘图,再以 Base64 PNG 的形式回传前端。
#mermaid-svg-EjYUzLpCgIOWW7EV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EjYUzLpCgIOWW7EV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EjYUzLpCgIOWW7EV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EjYUzLpCgIOWW7EV .error-icon{fill:#552222;}#mermaid-svg-EjYUzLpCgIOWW7EV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EjYUzLpCgIOWW7EV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EjYUzLpCgIOWW7EV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EjYUzLpCgIOWW7EV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EjYUzLpCgIOWW7EV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EjYUzLpCgIOWW7EV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EjYUzLpCgIOWW7EV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EjYUzLpCgIOWW7EV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EjYUzLpCgIOWW7EV .marker.cross{stroke:#333333;}#mermaid-svg-EjYUzLpCgIOWW7EV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EjYUzLpCgIOWW7EV p{margin:0;}#mermaid-svg-EjYUzLpCgIOWW7EV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-EjYUzLpCgIOWW7EV .cluster-label text{fill:#333;}#mermaid-svg-EjYUzLpCgIOWW7EV .cluster-label span{color:#333;}#mermaid-svg-EjYUzLpCgIOWW7EV .cluster-label span p{background-color:transparent;}#mermaid-svg-EjYUzLpCgIOWW7EV .label text,#mermaid-svg-EjYUzLpCgIOWW7EV span{fill:#333;color:#333;}#mermaid-svg-EjYUzLpCgIOWW7EV .node rect,#mermaid-svg-EjYUzLpCgIOWW7EV .node circle,#mermaid-svg-EjYUzLpCgIOWW7EV .node ellipse,#mermaid-svg-EjYUzLpCgIOWW7EV .node polygon,#mermaid-svg-EjYUzLpCgIOWW7EV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-EjYUzLpCgIOWW7EV .rough-node .label text,#mermaid-svg-EjYUzLpCgIOWW7EV .node .label text,#mermaid-svg-EjYUzLpCgIOWW7EV .image-shape .label,#mermaid-svg-EjYUzLpCgIOWW7EV .icon-shape .label{text-anchor:middle;}#mermaid-svg-EjYUzLpCgIOWW7EV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-EjYUzLpCgIOWW7EV .rough-node .label,#mermaid-svg-EjYUzLpCgIOWW7EV .node .label,#mermaid-svg-EjYUzLpCgIOWW7EV .image-shape .label,#mermaid-svg-EjYUzLpCgIOWW7EV .icon-shape .label{text-align:center;}#mermaid-svg-EjYUzLpCgIOWW7EV .node.clickable{cursor:pointer;}#mermaid-svg-EjYUzLpCgIOWW7EV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-EjYUzLpCgIOWW7EV .arrowheadPath{fill:#333333;}#mermaid-svg-EjYUzLpCgIOWW7EV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-EjYUzLpCgIOWW7EV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-EjYUzLpCgIOWW7EV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EjYUzLpCgIOWW7EV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-EjYUzLpCgIOWW7EV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EjYUzLpCgIOWW7EV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-EjYUzLpCgIOWW7EV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-EjYUzLpCgIOWW7EV .cluster text{fill:#333;}#mermaid-svg-EjYUzLpCgIOWW7EV .cluster span{color:#333;}#mermaid-svg-EjYUzLpCgIOWW7EV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-EjYUzLpCgIOWW7EV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-EjYUzLpCgIOWW7EV rect.text{fill:none;stroke-width:0;}#mermaid-svg-EjYUzLpCgIOWW7EV .icon-shape,#mermaid-svg-EjYUzLpCgIOWW7EV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EjYUzLpCgIOWW7EV .icon-shape p,#mermaid-svg-EjYUzLpCgIOWW7EV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-EjYUzLpCgIOWW7EV .icon-shape .label rect,#mermaid-svg-EjYUzLpCgIOWW7EV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EjYUzLpCgIOWW7EV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-EjYUzLpCgIOWW7EV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-EjYUzLpCgIOWW7EV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 💾 数据层
🐍 Python 渲染层
⚙️ Node.js 服务层
🌐 浏览器端
POST /api/stock-data
POST /api/chart
stdin JSON
stdout Base64 JSON
JSON response
可选
React 组件树
ControlPanel / StatsBar / ChartPanel
状态管理
useState + useCallback + useRef
防抖调度器
450ms + AbortController
/api/stock-data
模拟行情生成 + 内存缓存
/api/chart
spawn Python 子进程
kline_chart.py
指标计算 + mplfinance 绘图
matplotlib Agg 后端
PNG → Base64
stockData.ts
mulberry32 + 几何布朗运动
SQLite + Prisma
User / Post 模型
2.2 技术栈选型
| 层级 | 技术 | 版本 | 选型理由 |
|---|---|---|---|
| 前端框架 | Next.js | 16.1+ | App Router + standalone 输出,适配容器化部署 |
| UI 库 | React | 19.0 | 并发特性 + use() hook 支持 |
| 样式方案 | Tailwind CSS | 4.x | 原子化 CSS + CSS 变量主题系统 |
| 组件库 | shadcn/ui | New York 风格 | 可复制源码、无运行时依赖、Radix UI 底层 |
| 图标库 | Lucide React | 0.525+ | 轻量、tree-shaking 友好 |
| 运行时 | Bun | --- | 替代 Node.js 执行 standalone server.js |
| ORM | Prisma | 6.11 | 类型安全的数据库访问 + SQLite 适配 |
| 图表引擎 | mplfinance | --- | matplotlib 金融图表扩展,内置蜡烛图/OHLC/技术指标 |
| 数学计算 | NumPy + Pandas | --- | 矢量化指标计算 |
| 反向代理 | Caddy | --- | 自动 HTTPS、简洁配置、XTransformPort 路由 |
2.3 数据流时序
从用户修改一个配置项(例如切换图表类型)到画面更新,完整链路如下:
Python 子进程 /api/chart 前端 React 用户 Python 子进程 /api/chart 前端 React 用户 #mermaid-svg-vh5X20d9x9LzV979{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vh5X20d9x9LzV979 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vh5X20d9x9LzV979 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vh5X20d9x9LzV979 .error-icon{fill:#552222;}#mermaid-svg-vh5X20d9x9LzV979 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vh5X20d9x9LzV979 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vh5X20d9x9LzV979 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vh5X20d9x9LzV979 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vh5X20d9x9LzV979 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vh5X20d9x9LzV979 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vh5X20d9x9LzV979 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vh5X20d9x9LzV979 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vh5X20d9x9LzV979 .marker.cross{stroke:#333333;}#mermaid-svg-vh5X20d9x9LzV979 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vh5X20d9x9LzV979 p{margin:0;}#mermaid-svg-vh5X20d9x9LzV979 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vh5X20d9x9LzV979 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-vh5X20d9x9LzV979 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-vh5X20d9x9LzV979 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-vh5X20d9x9LzV979 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-vh5X20d9x9LzV979 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-vh5X20d9x9LzV979 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-vh5X20d9x9LzV979 .sequenceNumber{fill:white;}#mermaid-svg-vh5X20d9x9LzV979 #sequencenumber{fill:#333;}#mermaid-svg-vh5X20d9x9LzV979 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-vh5X20d9x9LzV979 .messageText{fill:#333;stroke:none;}#mermaid-svg-vh5X20d9x9LzV979 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vh5X20d9x9LzV979 .labelText,#mermaid-svg-vh5X20d9x9LzV979 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-vh5X20d9x9LzV979 .loopText,#mermaid-svg-vh5X20d9x9LzV979 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-vh5X20d9x9LzV979 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-vh5X20d9x9LzV979 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-vh5X20d9x9LzV979 .noteText,#mermaid-svg-vh5X20d9x9LzV979 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-vh5X20d9x9LzV979 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vh5X20d9x9LzV979 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vh5X20d9x9LzV979 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vh5X20d9x9LzV979 .actorPopupMenu{position:absolute;}#mermaid-svg-vh5X20d9x9LzV979 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-vh5X20d9x9LzV979 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vh5X20d9x9LzV979 .actor-man circle,#mermaid-svg-vh5X20d9x9LzV979 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-vh5X20d9x9LzV979 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 若 450ms 内再次修改 则取消前一个定时器 切换"蜡烛图 → OHLC" patchConfig({chartType:'ohlc'}) 触发 450ms 防抖定时器 定时器到期,执行 renderChart() abortRef.abort() 取消上一请求 POST /api/chart {data, config} 校验 data.length ≥ 5 spawn python kline_chart.py stdin.write(JSON payload) 构建 DataFrame + 计算指标 mplfinance.plot() → PNG buffer base64 编码 + 组装 stats stdout 输出 JSON {ok,image,stats} JSON response 检查 reqId 是否过期 渲染图片 + 更新指标读数
2.4 请求竞态防护
前端使用三重机制确保用户快速操作时不会出现"旧请求覆盖新请求"的画面闪烁:
| 机制 | 实现位置 | 作用 |
|---|---|---|
| 防抖定时器 | debounceRef (450ms) |
配置变更后延迟 450ms 才发起渲染请求,连续操作只保留最后一次 |
| AbortController | abortRef |
新请求发起前 abort 上一请求,中断已发出但未完成的 fetch |
| 请求序号 | reqIdRef (自增整数) |
每次请求分配递增 ID,响应回来后比对,若不是最新请求则丢弃结果 |
typescript
// 三重防护核心逻辑
abortRef.current?.abort() // 1. 取消上一次请求
const controller = new AbortController()
abortRef.current = controller
const myId = ++reqIdRef.current // 2. 分配请求序号
// ... await fetch ...
if (myId !== reqIdRef.current) return // 3. 序号过期则丢弃结果
三、前端详细设计
3.1 页面布局
整体采用 左配置右展示 的双栏布局,大屏使用 lg:grid-cols-[320px_minmax(0,1fr)],移动端自动堆叠。
#mermaid-svg-OZGScQuNfhvOLw93{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-OZGScQuNfhvOLw93 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OZGScQuNfhvOLw93 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OZGScQuNfhvOLw93 .error-icon{fill:#552222;}#mermaid-svg-OZGScQuNfhvOLw93 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OZGScQuNfhvOLw93 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OZGScQuNfhvOLw93 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OZGScQuNfhvOLw93 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OZGScQuNfhvOLw93 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OZGScQuNfhvOLw93 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OZGScQuNfhvOLw93 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OZGScQuNfhvOLw93 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OZGScQuNfhvOLw93 .marker.cross{stroke:#333333;}#mermaid-svg-OZGScQuNfhvOLw93 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OZGScQuNfhvOLw93 p{margin:0;}#mermaid-svg-OZGScQuNfhvOLw93 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-OZGScQuNfhvOLw93 .cluster-label text{fill:#333;}#mermaid-svg-OZGScQuNfhvOLw93 .cluster-label span{color:#333;}#mermaid-svg-OZGScQuNfhvOLw93 .cluster-label span p{background-color:transparent;}#mermaid-svg-OZGScQuNfhvOLw93 .label text,#mermaid-svg-OZGScQuNfhvOLw93 span{fill:#333;color:#333;}#mermaid-svg-OZGScQuNfhvOLw93 .node rect,#mermaid-svg-OZGScQuNfhvOLw93 .node circle,#mermaid-svg-OZGScQuNfhvOLw93 .node ellipse,#mermaid-svg-OZGScQuNfhvOLw93 .node polygon,#mermaid-svg-OZGScQuNfhvOLw93 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OZGScQuNfhvOLw93 .rough-node .label text,#mermaid-svg-OZGScQuNfhvOLw93 .node .label text,#mermaid-svg-OZGScQuNfhvOLw93 .image-shape .label,#mermaid-svg-OZGScQuNfhvOLw93 .icon-shape .label{text-anchor:middle;}#mermaid-svg-OZGScQuNfhvOLw93 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-OZGScQuNfhvOLw93 .rough-node .label,#mermaid-svg-OZGScQuNfhvOLw93 .node .label,#mermaid-svg-OZGScQuNfhvOLw93 .image-shape .label,#mermaid-svg-OZGScQuNfhvOLw93 .icon-shape .label{text-align:center;}#mermaid-svg-OZGScQuNfhvOLw93 .node.clickable{cursor:pointer;}#mermaid-svg-OZGScQuNfhvOLw93 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-OZGScQuNfhvOLw93 .arrowheadPath{fill:#333333;}#mermaid-svg-OZGScQuNfhvOLw93 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-OZGScQuNfhvOLw93 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-OZGScQuNfhvOLw93 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OZGScQuNfhvOLw93 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-OZGScQuNfhvOLw93 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OZGScQuNfhvOLw93 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-OZGScQuNfhvOLw93 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-OZGScQuNfhvOLw93 .cluster text{fill:#333;}#mermaid-svg-OZGScQuNfhvOLw93 .cluster span{color:#333;}#mermaid-svg-OZGScQuNfhvOLw93 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-OZGScQuNfhvOLw93 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OZGScQuNfhvOLw93 rect.text{fill:none;stroke-width:0;}#mermaid-svg-OZGScQuNfhvOLw93 .icon-shape,#mermaid-svg-OZGScQuNfhvOLw93 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OZGScQuNfhvOLw93 .icon-shape p,#mermaid-svg-OZGScQuNfhvOLw93 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-OZGScQuNfhvOLw93 .icon-shape .label rect,#mermaid-svg-OZGScQuNfhvOLw93 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OZGScQuNfhvOLw93 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-OZGScQuNfhvOLw93 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-OZGScQuNfhvOLw93 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Main内容
页面结构
Sticky Header
Logo + 标题 + Badge
主内容区 max-w-1400
Sticky Footer
版权声明
左侧 320px
ControlPanel
sticky top-4.5rem
右侧自适应
StatsBar + ChartPanel
- IndicatorReadouts
3.2 组件架构
核心业务组件仅三个,全部位于 src/components/kline/ 下,职责清晰、无耦合:
#mermaid-svg-p0SFuxoijhR5tHla{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-p0SFuxoijhR5tHla .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-p0SFuxoijhR5tHla .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-p0SFuxoijhR5tHla .error-icon{fill:#552222;}#mermaid-svg-p0SFuxoijhR5tHla .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-p0SFuxoijhR5tHla .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-p0SFuxoijhR5tHla .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-p0SFuxoijhR5tHla .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-p0SFuxoijhR5tHla .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-p0SFuxoijhR5tHla .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-p0SFuxoijhR5tHla .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-p0SFuxoijhR5tHla .marker{fill:#333333;stroke:#333333;}#mermaid-svg-p0SFuxoijhR5tHla .marker.cross{stroke:#333333;}#mermaid-svg-p0SFuxoijhR5tHla svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-p0SFuxoijhR5tHla p{margin:0;}#mermaid-svg-p0SFuxoijhR5tHla .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-p0SFuxoijhR5tHla .cluster-label text{fill:#333;}#mermaid-svg-p0SFuxoijhR5tHla .cluster-label span{color:#333;}#mermaid-svg-p0SFuxoijhR5tHla .cluster-label span p{background-color:transparent;}#mermaid-svg-p0SFuxoijhR5tHla .label text,#mermaid-svg-p0SFuxoijhR5tHla span{fill:#333;color:#333;}#mermaid-svg-p0SFuxoijhR5tHla .node rect,#mermaid-svg-p0SFuxoijhR5tHla .node circle,#mermaid-svg-p0SFuxoijhR5tHla .node ellipse,#mermaid-svg-p0SFuxoijhR5tHla .node polygon,#mermaid-svg-p0SFuxoijhR5tHla .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-p0SFuxoijhR5tHla .rough-node .label text,#mermaid-svg-p0SFuxoijhR5tHla .node .label text,#mermaid-svg-p0SFuxoijhR5tHla .image-shape .label,#mermaid-svg-p0SFuxoijhR5tHla .icon-shape .label{text-anchor:middle;}#mermaid-svg-p0SFuxoijhR5tHla .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-p0SFuxoijhR5tHla .rough-node .label,#mermaid-svg-p0SFuxoijhR5tHla .node .label,#mermaid-svg-p0SFuxoijhR5tHla .image-shape .label,#mermaid-svg-p0SFuxoijhR5tHla .icon-shape .label{text-align:center;}#mermaid-svg-p0SFuxoijhR5tHla .node.clickable{cursor:pointer;}#mermaid-svg-p0SFuxoijhR5tHla .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-p0SFuxoijhR5tHla .arrowheadPath{fill:#333333;}#mermaid-svg-p0SFuxoijhR5tHla .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-p0SFuxoijhR5tHla .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-p0SFuxoijhR5tHla .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-p0SFuxoijhR5tHla .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-p0SFuxoijhR5tHla .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-p0SFuxoijhR5tHla .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-p0SFuxoijhR5tHla .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-p0SFuxoijhR5tHla .cluster text{fill:#333;}#mermaid-svg-p0SFuxoijhR5tHla .cluster span{color:#333;}#mermaid-svg-p0SFuxoijhR5tHla div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-p0SFuxoijhR5tHla .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-p0SFuxoijhR5tHla rect.text{fill:none;stroke-width:0;}#mermaid-svg-p0SFuxoijhR5tHla .icon-shape,#mermaid-svg-p0SFuxoijhR5tHla .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-p0SFuxoijhR5tHla .icon-shape p,#mermaid-svg-p0SFuxoijhR5tHla .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-p0SFuxoijhR5tHla .icon-shape .label rect,#mermaid-svg-p0SFuxoijhR5tHla .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-p0SFuxoijhR5tHla .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-p0SFuxoijhR5tHla .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-p0SFuxoijhR5tHla :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} page.tsx
页面容器 + 状态中枢
ControlPanel
左侧配置面板
StatsBar
行情概览
ChartPanel
图表展示与下载
IndicatorReadouts
指标读数 Badge 组
标的选择 Select
图表类型 Tabs
涨跌配色 ToggleGroup
显示范围 Slider
指标开关 Switch × 5
参数输入 Input × N
重置按钮
最新价大卡
涨跌箭头 + 百分比
六格行情
今开/最高/最低/量/区间高低
工具栏
刷新 + PNG 下载
图片主体
loading 半透明遮罩
错误兜底
ImageOff + 重试
骨架屏
Skeleton 占位
3.3 组件职责矩阵
| 组件 | 输入 Props | 输出/回调 | 内部状态 | 说明 |
|---|---|---|---|---|
ControlPanel |
config, onChange, symbol, stocks, range |
onChange(patch) 回调 |
无(纯受控) | 全部表单交互通过 onChange 上抛,组件自身不持有状态 |
StatsBar |
meta, stats |
--- | 无 | 纯展示组件,行情概览大卡 + 六格数据 |
ChartPanel |
imageUrl, loading, error, stats, onRetry, fileName |
handleDownload() 内部处理 |
无 | 图片渲染 + 下载 + 错误重试,四种显示状态切换 |
IndicatorReadouts |
stats |
--- | 无 | 以 Badge 形式展示 MA / BOLL / MACD / RSI 最新值 |
3.4 前端状态结构
页面级状态全部集中在 page.tsx 的 useState 中,没有引入 Zustand/Redux 等全局状态库。这是因为整个应用只有一个页面、一个数据域,集中式状态足以胜任且更易追踪:
| 状态变量 | 类型 | 初值 | 用途 |
|---|---|---|---|
stocks |
StockListItem[] |
[] |
股票列表(供下拉选择) |
symbol |
string |
'DEMO.600519' |
当前选中标的代码 |
meta |
`StockMeta | null` | null |
series |
OhlcvRow[] |
[] |
完整 OHLCV 日线序列 |
range |
number |
120 |
显示范围(最近 N 个交易日) |
config |
ChartConfig |
DEFAULT_CONFIG |
图表配置对象 |
imageUrl |
`string | null` | null |
stats |
`ChartStats | null` | null |
chartLoading |
boolean |
true |
渲染中状态 |
chartError |
`string | null` | null |
renderMs |
`number | null` | null |
3.5 可见数据切片
前端通过 useMemo 对完整序列做切片,只截取最近 range 根 K 线传给后端渲染。这一设计将数据传输量和 Python 端计算量都控制在用户期望的范围内:
typescript
const visibleData = useMemo(
() => (series.length > 0 ? series.slice(-range) : []),
[series, range]
)
range 通过 Slider 控制(30 ~ maxBars),并提供 60/120/250 三个快捷预设按钮。maxBars 取 Math.max(60, series.length),确保即使标的数据不足 250 根也不会出现无效区间。