先把"病因"交代清楚:我看盘的时候特别容易把自己搞累------同时开好几个 APP、网页、行情工具,来回切得飞起,信息没吸收多少,焦虑倒是堆满了(浏览器标签也堆满了)。所以我干脆换个思路:只要一个页面,把常用的行情、板块、分时、K 线、资金面、筛选工具都塞进去,减少"切来切去的成本",让自己专注在该看的东西上。
于是就有了 stock-dashboard:一个 React + TypeScript + Vite 的前端看板。数据源全部走 stock-sdk。重点是:没有后端、没有定时脚本、也没有"我朋友的服务器" ------就是纯前端直接拉数据,然后在 UI 里把它们排兵布阵。

体验地址放这:stock-dashboard 👉 https://chengzuopeng.github.io/stock-dashboard/ (建议小窗挂着,摸鱼更丝滑)。
先把底层说清楚:数据是怎么接进来的?
项目里把 stock-sdk 的调用集中到 src/services/sdk.ts,让页面层尽量"只消费服务,不直连 SDK"。整体做法偏工程化,但没有太多花活:
-
SDK 单例 + 自带重试
统一用
new StockSDK({ timeout, retry })创建实例。网络抖动、偶发超时之类的事,交给 SDK 的重试机制处理(最多 3 次,指数退避那一套)。 -
内存缓存(TTL)
行业/概念列表这种数据短时间内变化不大,缓存一下能省不少请求;实时行情则给一个更短的 TTL(2~3 秒),在"新鲜度"和"别把接口当压测"之间取个平衡。
-
页面只认封装后的 service
在
src/pages/**里基本看不到new StockSDK(),页面只调用getFullQuotes / getTodayTimeline / getKlineWithIndicators ...这些方法,同时类型直接从stock-sdk导入,减少"字符串拼错、字段写错"的机会。
下面两段是核心"骨架",后面的功能基本都是围绕这套服务层扩展的:
ts
// src/services/sdk.ts
export const sdk = new StockSDK({
timeout: 30000,
retry: { maxRetries: 3, baseDelay: 1000, maxDelay: 10000, backoffMultiplier: 2 },
});
export async function getFullQuotes(codes: string[], useCache = true) {
const key = getCacheKey('getFullQuotes', codes);
if (useCache) {
return withCache(key, DEFAULT_TTL.quotes, () => sdk.getFullQuotes(codes));
}
return sdk.getFullQuotes(codes);
}
ts
// src/services/sdk.ts
export async function getAllAShareQuotes(options?: {
batchSize?: number;
concurrency?: number;
onProgress?: (completed: number, total: number) => void;
}) {
return sdk.getAllAShareQuotes(options);
}
功能拆解:各个页面分别怎么用 stock-sdk?
路由集中在 src/router/index.tsx,页面按功能放在 src/pages/*。下面按"你在页面上能点到的入口"顺一遍。
1) 顶部搜索:别让我为了查代码去翻软件
入口是 src/components/layout/Header.tsx,核心就是:
search(keyword)→sdk.search(keyword)
输入做了 300ms 防抖,支持股票/板块混搜;点结果就跳转到对应页面:
- 行业板块:
/boards/industry/:code - 概念板块:
/boards/concept/:code - 个股:
/s/:code
另外把搜索历史写到 localStorage(src/services/storage.ts)。你以为你在找标的,其实你很多时候是在找"上一次看过的那只"。
2) 总览 Dashboard:打开先扫一眼(顺便带自选快照)
页面在 src/pages/Dashboard/Dashboard.tsx,数据拿法比较直给:
- 指数行情:
getFullQuotes(MAIN_INDICES)(上证、深成指、创业板、科创 50...) - 行业/概念列表:
getIndustryList()+getConceptList() - 自选快照:从
src/services/storage.ts读自选代码,再getFullQuotes(watchlistCodes.slice(0, 50))
轮询用 usePolling(src/hooks/usePolling.ts),默认 5 秒刷新一次;页面不可见会自动暂停,避免你切到别的标签页它还在后台狂刷。
(顺带一提:Dashboard 里的"榜单"区域目前更多是板块数据的延伸;如果要做全市场个股榜,路线其实就是 getAllAShareQuotes------后面"一日持股法"已经把关键能力跑通了。)
3) 热力图 Heatmap:今天谁最"烫"一眼看明白

页面 src/pages/Heatmap/Heatmap.tsx,用 ECharts 的 treemap 做展示。不同维度对应不同数据源:
- 行业热力图:
getIndustryList()(行业里包含涨跌幅、换手、领涨股等字段) - 概念热力图:
getConceptList() - 自选热力图:
getAllWatchlistCodes()→getAllQuotesByCodes(codes.slice(0, topK))
如果要按"个股维度"做(目前代码里留了入口但还没开放),我的思路是:
getIndustryConstituents(industryCode)先拿成分股getAllQuotesByCodes(stockCodes)再拉行情- 拼好结构喂给 treemap
热力图的价值就在这:不用盯着榜单刷屏,颜色铺开以后,强弱结构基本是秒懂。
4) 榜单 Rankings:卷不动就看谁在卷

页面在 src/pages/Rankings/Rankings.tsx,实现方式属于"简单但够用":
- 先
getIndustryList()/getConceptList()拿板块数据 - 前端按
changePercent/turnoverRate做排序,取 Top 50
所以这里的榜单是"板块榜"。如果要做"全市场个股榜",技术路径跟后面的全市场扫描类似。
5) 板块列表 + 详情:这波到底谁在带节奏?
板块列表:src/pages/Boards/Boards.tsx
getIndustryList()+getConceptList()一次拿齐- tab 切换只是前端切数组
- 支持按板块名/领涨股搜索
板块详情:src/pages/Boards/BoardDetail.tsx,这页 API 用得更完整(行业/概念分别走不同分支):
- 详情信息:直接从
getIndustryList()/getConceptList()里 find(少一次请求) - 成分股:
getIndustryConstituents(code)/getConceptConstituents(code) - 板块 K 线:
getIndustryKline(code, { period })/getConceptKline(code, { period }) - 板块 Spot 指标:
getIndustrySpot(code)/getConceptSpot(code)
板块 K 线我只取最近 60 根(slice(-60)),否则拖 dataZoom 的时候浏览器会明显开始"喘气"。
6) 自选 Watchlist:我盯的不是票,是"我自己的偏好"
页面在 src/pages/Watchlist/Watchlist.tsx,自选的分组/增删改在 src/services/storage.ts 处理。
行情拉取走:
getAllQuotesByCodes(normalizedActiveCodes)
这里我把代码做了 normalizeStockCode(src/utils/format.ts),避免 SZ000001、sz000001、000001 这种"同一个人换不同马甲"造成重复或取不到数据。
7) 个股详情 StockDetail:该看的给你,不该看的也顺手给你

页面 src/pages/StockDetail/StockDetail.tsx 是整个项目里信息密度最高的一页之一,数据来源大概分几组:
- 实时行情:
getFullQuotes([code]) - 分时(1 分钟):
getTodayTimeline(code) - 分钟 K(5/15/30/60):
getMinuteKline(code, { period }) - 日/周/月 K + 技术指标:
getKlineWithIndicators(code, { period, adjust: 'qfq', indicators }) - 资金流:
getFundFlow([code]) - 盘口大单:
getPanelLargeOrder([code])
我很喜欢 getKlineWithIndicators:页面勾选 MA/MACD/BOLL/KDJ/RSI,SDK 直接把指标结果算好返回,前端专心画图就行------少写一堆计算逻辑,也少养一堆 bug。
同样配了轮询策略:
- 行情 2 秒刷新
- 分时 3 秒刷新
- 资金 10 秒刷新
8) 信号扫描 Scanner:给自己一点"量化的幻觉"
页面在 src/pages/Scanner/Scanner.tsx,流程是:
- 先选股票池
- 自选:本地读代码
- 行业/概念:
getIndustryConstituents('BK0475')/getConceptConstituents('BK0891')抽取成分股
- 对每只股票调用:
getKlineWithIndicators(code, { indicators: { ma/macd/rsi/boll } })
- 前端用最近两根 K 线判断信号(比如 MA 金叉、MACD 金叉、RSI 超买超卖...)
这个功能的"心理收益"大于"实盘收益",但它至少能把"我觉得它要动了"变成"它确实触发了某个条件"(哪怕条件是我写的)。
9) 设置 Settings:不碰行情,只负责体验

页面在 src/pages/Settings/Settings.tsx。
这页不调用 stock-sdk ,只把刷新频率、涨跌配色、指标默认参数等偏好写到 localStorage(src/services/storage.ts),保证下次打开不是"出厂状态"。
重头戏:一日持股法(尾盘选股)= 前端全市场扫描的重武器

页面在 src/pages/EndOfDayPicker/EndOfDayPicker.tsx。我做的是一个"三段式"流程,核心能力就是 getAllAShareQuotes(全市场 5000+ 一把抓)。
第一步:全量拉取 A 股行情
ts
// src/pages/EndOfDayPicker/EndOfDayPicker.tsx
const quotes = await getAllAShareQuotes({
batchSize: 500,
concurrency: 5,
onProgress: (completed, total) => setLoadingProgress({ completed, total, stage: '获取行情数据' }),
});
对应 stock-sdk 的签名是:
sdk.getAllAShareQuotes(options?: GetAllAShareQuotesOptions): Promise<FullQuote[]>GetAllAShareQuotesOptions支持:batchSize:单次请求股票数量(默认 500)concurrency:最大并发数(默认 7)onProgress:进度回调
我这里把并发设为 5,属于"别太激进,浏览器和网速都有极限"的保守设置;配合 onProgress,页面能实时显示进度条,不至于让人怀疑是不是卡死了。
第二步:用基础条件先把范围收紧
FullQuote[] 拿到手后,先用这些字段做一轮基础过滤(直接来自 FullQuote):
- 流通市值
circulatingMarketCap - 量比
volumeRatio - 涨跌幅
changePercent - 换手率
turnoverRate - 以及过滤
ST/*ST
这一步在 filterStocksBasic(),目的是先把 5000+ 压到几十/几百只,避免后面拉分时把自己拖进"请求风暴"。
第三步:对候选股拉分时,算"强度比例"
对筛出来的候选,分批去拉分时:
getTodayTimeline(fullCode)(注意这里需要拼sh/sz/bj前缀)
然后用分时里的 price 和 avgPrice 算一个简单但直观的强度指标:
price >= avgPrice的点数 / 总点数 =timelineAboveAvgRatio
分时请求在 filterWithTimeline() 里按 batchSize = 5 控制并发(这是我自己在页面里控制的批大小,不是 SDK 的参数),防止对几百只股票同时开火,最后浏览器先躺平。
最终按 timelineAboveAvgRatio 排序,列表里每只股票还带一张迷你分时图,用来做"尾盘快速过一遍候选"的效率工具。
收个尾:这套思路适合谁?
如果你想要的是一个"能看行情、能看板块、能看分时/ K 线、还能顺手管理自选"的轻量看板,同时你又不想为了它额外维护后端,那么这个项目的路线挺适合:用 stock-sdk 把数据能力带进前端,然后在 UI 里做组合、筛选和展示。
本地启动也很简单:
bash
yarn install
yarn dev
最后提醒一句:页面底部那句"仅供学习参考,不构成投资建议"不是装饰品------写代码可以自信,交易还是得保持敬畏。