最近跟朋友聊天,发现一些基金购买限额,需要“开超市”在多个基金 app 定投。每天想看总盈亏都要挨个切换 App;每月定投日回头看当月投入、累计年化,更费事。参考养基宝 app 的功能,借助 Claude Code,搭建了一个投资记录的纯前端工具 fund-tracker。
这篇文章讲 4 层架构和 5 个选型为什么这么选,再讲 AI 哪些地方写代码省心、哪些地方漏了要靠业务经验补。下一篇再展开工具能做什么、怎么用。
投资有风险,入市需谨慎。本文工具仅用于投资记录与收益计算,非投资平台,不提供交易功能。
一、整体架构:4 层分法
先说这 4 层分别是什么。打个比方:页面是装修工,状态是仓管,服务是加工产线,数据是货架。一句话概括——谁渲染、谁存状态、谁做事、谁落盘,全部单向调用。
- 页面层:7 个页面(Dashboard / 基金 / 交易 / 定投 / 报表 / 设置 / 数据管理),AntD 组件 + ECharts 图表。
- 状态层:1 个 Zustand store,18 个状态分片(持仓 / 交易 / NAV 历史 / 刷新队列 / 定投计划 / 设置)。
- 服务层:东方财富 JSONP 拉取、NAV 增量合并、交易记账、XIRR / 收益日历计算。
- 数据层:localStorage 落盘 + JSON 备份导入导出。
调用方向是:页面订阅 store,store 调用 service,service 读写 localStorage。以后如果要换 JSONP 数据源或加缓存,只需动对应层。
二、5 个核心选型
选型 1:无后端(纯前端)
为什么不用服务端?
- 数据是用户级的,持仓、交易、NAV 全都属于个人隐私。
- 数据量小:一年几百条交易,加上每只基金几百到几千条 NAV,实测只有 100-500 KB。
- 浏览器一开通常就一个人用,没有并发压力。
如果引入服务端,反而要搭鉴权、同步、备份一整套流程。所以纯前端在这里是最自然的选择。
项目里有 18 个状态分片,覆盖持仓、交易、NAV 历史、刷新队列、定投计划和设置。如果按 Redux Toolkit 的套路拆 slice,每个 slice 都要配一套 reducer + selector + action,心智成本不低。Zustand 把所有状态集中在 1 个 store,actions 写成方法挂上去,组件按需 useStore(s => s.x) 订阅,反而更清楚。
calculator.ts 走纯函数 + fixture 单测,不依赖 React 渲染。
选型 3:东方财富 JSONP(跨域唯一办法)
pingzhongdata 接口不支持 CORS,前端直接 fetch 会撞上跨域失败。浏览器同源策略挡在那里,可行的办法就是 JSONP 风格:用 <script> 注入加载 JS,脚本执行后再从 window 读 fS_name、Data_netWorthTrend 等 globals。
同时配了 15000ms 超时,以及三条出口(onload / onerror / timeout)来清理 globals,避免读到上一只基金的脏数据。落地细节在第四节展开。
选型 4:GitHub Pages(不是 Vercel / 自建)
零运维成本:HTTPS、CDN、自定义域名全部免费。零运行时:整个工具就是纯静态资源(HTML / JS / CSS / 字体),没有服务端运行时。子路径对齐:vite.config.ts 里把 base 设为 /fund-tracker/,跟 GitHub Pages 的默认子路径部署保持一致。部署流程也很直接:dist/ 走 Actions 推到 gh-pages 分支。
选型 5:localStorage(不是 IndexedDB)
5MB 配额足够装下几百条交易,以及每只基金的日级 NAV 历史。key-value 结构正好适配状态分片,按 store 命名空间拆开写,刷新页面后还能自动恢复。
版本号 version: 1 写在 payload 里,将来 schema 升级时,比如新增定投分组字段,可以识别老数据走迁移,兼容已有用户的备份。
5 个选型其实互相咬合成链:选型 1(无后端)是地基,它决定了选型 4(GitHub Pages 部署)、选型 5(localStorage 落盘)和选型 2(Zustand 单 store 集中管理);选型 3(JSONP)只解决第三方数据源没有 CORS 的问题,独立于这条链。
三、AI 辅助搭建:vibe coding 节奏
整个工具一共 119 次 commit,AI 协作占比约 65-70%,集中在架构骨架、重构和测试;人工微调约 30-35%,主要补业务判断和 bug 细节。
AI 写得最顺手的是有现成约定的代码,比如 Zustand store 设计、JSONP 封装。真正需要人工盯的是业务边界判断:QDII 美股节假日规则、T+N 自动确认的严格校验。
节奏分 V1 / V2 / V3 三段:
- V1 跑通(3 天):AI 一次性把骨架出完,包括 7 个页面、store、API、utils。出代码快,但只是能过编译,边界没过,比如 QDII 净值延迟被写死成 T+1。
- V2 加功能(1 周):补上 XIRR、收益日历、定投自动生成、刷新队列。
- V3 优化(1 周):把“按基金类型推算发布日”换成
navPair(最新一对可用 NAV 配对)+ navFreshness(对照上次刷新判断 NAV 是否更新)的观测法。V1 / V2 的数据模型有结构性偏差,V3 基本重做。
四、1 个架构亮点:JSONP 三条出口清理
JSONP 落地时有 3 个非主线但必须处理的脏活。其中第 3 条 AI 默认会漏,后来靠人工跑测试、看执行时序才补上。
3 条都在 loadPingzhongScript 这一层:
- ES Module 严格模式下
var 是 non-configurable(不可删除):delete window.fS_name(window 上的全局变量)会静默失败,所以要改用 = undefined 来清理,即保留键、把值抹掉。
<script> 标签带 referrerpolicy="no-referrer":否则 referer header(请求头里的来源 URL)会触发东方财富拦截。
- 脚本注入之前先把旧的 25 个 globals 显式置
undefined:避免新一轮 <script> 还没执行完时,前一帧 globals 还留在 window 上,造成新旧数据交叉污染。
第 3 条 AI 默认忽略,是因为它默认按“onload 会清 globals”来假设。但 onload 是异步的,新旧 <script> 之间存在时序窗口,前一帧 globals 这时候还挂在 window 上。
小结
回头看这 5 个选型,其实是串成一条链的:选型 1 是地基,推出 4(GitHub Pages)、5(localStorage)、2(Zustand 单 store);选型 3(JSONP)只解决第三方数据源无 CORS,独立于这条链。AI 协作占比约 65-70%,剩下的业务边界靠人工补。
下一篇讲工具能做什么、怎么用。
工具链接
标签:#vibe coding #GitHub Pages #人人都是程序员