【应用】基于 Next.js 16 + Python mplfinance的金融K线图与技术指标可视化平台(一)

本项目站内源代码下载地址https://download.csdn.net/download/s060403072/93356585

文章目录

    • 一、概览
      • [🎯 核心特性](#🎯 核心特性)
    • 二、整体架构设计
      • [2.1 架构总览](#2.1 架构总览)
      • [2.2 技术栈选型](#2.2 技术栈选型)
      • [2.3 数据流时序](#2.3 数据流时序)
      • [2.4 请求竞态防护](#2.4 请求竞态防护)
    • 三、前端详细设计
      • [3.1 页面布局](#3.1 页面布局)
      • [3.2 组件架构](#3.2 组件架构)
      • [3.3 组件职责矩阵](#3.3 组件职责矩阵)
      • [3.4 前端状态结构](#3.4 前端状态结构)
      • [3.5 可见数据切片](#3.5 可见数据切片)

一、概览

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.tsxuseState 中,没有引入 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 三个快捷预设按钮。maxBarsMath.max(60, series.length),确保即使标的数据不足 250 根也不会出现无效区间。

相关推荐
大衛說17 分钟前
《Python 从入门到精通》系列总览与学习路线
开发语言·python·学习
小猴子爱上树31 分钟前
跨境电商翻译工具推荐:批量图片翻译、视频字幕翻译、智能抠图一站式搞定
人工智能·python·音视频
丈剑走天涯1 小时前
基于 Python 3 语法的 PyCharm 练习示例,涵盖列表操作、类型判断及数据清洗等核心知识点
windows·python·pycharm
————A2 小时前
Agent 接收用户上传文件
人工智能·笔记·python·状态模式
可乐鸡翅yeah_2 小时前
hls.js 内存泄漏实战排查,直播 M3U8 长时间播放异常定位方案
开发语言·javascript·python·django·ecmascript·m3u8·m3u8在线
可乐鸡翅yeah_2 小时前
第三方接口对接 M3U8 流媒体排坑实战,解决外部服务商流兼容难题
前端·javascript·python·django·html·m3u8·m3u8在线
金融小师妹2 小时前
多因子智能推演:黄金震荡回升,杰克逊霍尔“沃什首秀”政策如何重塑金价路径的AI预测框架
大数据·人工智能·python·线性回归
阿童木写作2 小时前
跨马翻译:AI批量图片翻译与视频字幕翻译工具推荐
人工智能·python·音视频
阿kun要赚马内2 小时前
MCP(Model Context Protocol,模型上下文协议)
人工智能·python