前端控制面架构
前端不只是一个管理员表格集合。它承担首次设置、公开站点、用户自助、管理员控制面、支付结果恢复和 Ops 可视化,但所有权限与业务事实仍由后端决定。
技术栈与交付方式
前端位于 frontend/,核心技术是 Vue 3、TypeScript、Vite、Vue Router、Pinia、Axios、Vue I18n、Tailwind CSS 和 Vitest。
生产构建后,静态产物通过 Go embed 打入服务二进制:
Vue/Vite build
│
▼
frontend/dist
│ Go embed
▼
Sub2API 单二进制
├─ 页面与静态资源
├─ /api/v1/*
└─ /v1/*、/v1beta/*默认同源部署可以减少 CORS 和版本错配。开发环境使用 Vite dev server 时,才需要单独处理 API base URL 和代理。
启动顺序
frontend/src/main.ts 有明确顺序:
- 在 mount 前应用 dark mode,避免主题闪烁。
- 创建 Vue app 和 Pinia。
- 从后端注入的公开配置初始化 App Store。
- 设置初始站点标题。
- 异步初始化 i18n。
- 安装 Router 与 i18n。
- 等待首次路由导航完成。
- 挂载应用。
不要把需要在首屏前完成的配置放到某个页面 onMounted,否则会出现路由守卫误判、标题闪烁或设置页面短暂显示错误状态。
页面分区
| 区域 | 路由示例 | 身份 |
|---|---|---|
| 首次设置 | /setup | 仅未安装状态 |
| 公开页面 | /home、/login、OAuth callback、法律文档 | 无登录或临时授权流程 |
| 用户自助 | /dashboard、/keys、/usage、/subscriptions | 普通 JWT 用户 |
| 支付 | /purchase、/orders、支付结果/Provider 页面 | 登录、公开 resume 或 Provider SDK |
| 管理控制面 | /admin/* | 管理员 JWT + 合规要求 |
| Ops | /admin/ops | 管理员且后端 Ops 开启 |
页面使用 lazy import。发布后旧浏览器可能请求已不存在的 chunk,Router 会识别动态导入错误并做一次受控刷新,避免无限 reload。
Router 守卫
路由 meta 描述页面需求:
requiresAuthrequiresAdminrequiresPaymentrequiresRiskControl
全局守卫会恢复本地登录状态、设置页面标题、检查 setup 状态、处理登录重定向、管理员权限、合规确认、支付/风控开关、simple mode 和 backend mode。
但是这些检查只能控制“页面是否展示”。攻击者可以绕过浏览器直接调用 API,因此:
- 新增管理员页时,后端路由仍必须使用 AdminAuth。
- 新增支付页时,后端仍必须验证用户、订单归属和支付开关。
- 新增 backend mode 限制时,前后端守卫必须同步。
- 路由 meta 不能成为唯一授权来源。
API Client
frontend/src/api/client.ts 统一处理:
- API base URL。
- 从 localStorage 添加 Bearer token。
Accept-Language和 GET 请求时区。- 标准响应
{ code, message, data }解包。 - 结构化错误的
status/code/reason/metadata。 - access token 刷新并发合并。
- 401、后台模式和 Ops 关闭等全局行为。
新增 API 模块应复用这个 client,而不是每个页面单独创建 Axios 实例。否则会丢掉 token refresh、语言、错误格式和全局跳转语义。
对于 SSE、文件下载、WebSocket 和支付 SDK,通用 Axios client 不一定适用,但仍要复用 base URL、认证和错误规范。
状态放在哪里
| 状态 | 合适位置 | 示例 |
|---|---|---|
| 跨页面身份 | Pinia Auth Store | user、role、token、simple mode |
| 公共站点能力 | App Store | site name、payment enabled、backend mode |
| 管理员设置摘要 | Admin Settings Store | 管理菜单、自定义设置 |
| 单页查询结果 | Page/composable | 列表、筛选、表单草稿 |
| URL 可恢复状态 | Router query/path | page、filter、支付结果引用 |
| 服务端业务事实 | PostgreSQL/API | 余额、订单、Account 状态 |
不要把余额、订单状态或账号可调度性只更新在 Pinia 中当作成功。写 API 返回后应以服务端响应替换本地快照,必要时重新查询。
公开设置注入
嵌入式前端服务会把公开设置注入页面,减少首屏额外请求,并在后台设置更新时失效当前进程 HTML 缓存。纯静态开发部署没有该注入时,App Store 会回退请求 public settings API。
公开注入必须维持严格 DTO:
- 可以包含站点名、公开开关、公开 OAuth client ID 等。
- 不能包含 SMTP 密码、OAuth client secret、支付私钥和上游凭证。
- 新增设置时不能直接把完整
SystemSettings序列化到浏览器。
多实例部署还要注意 HTML 缓存失效是进程本地行为。配置更新后应验证不同副本返回的页面注入是否一致。
管理页面的一条完整链路
View
→ Component / composable
→ frontend/src/api/<domain>.ts
→ /api/v1/admin/*
→ AdminAuth + Compliance Guard
→ Handler DTO 校验
→ Service 业务规则
→ Repository / PostgreSQL
→ 缓存失效、outbox、审计
→ 标准响应
→ Store 或页面重新同步只改 View 和 API 并不构成完整后台功能。尤其是 Account、Group、支付和 Settings,写成功后通常还有运行时缓存、调度快照或 Provider Registry 要刷新。
新增页面的最低步骤
- 明确页面属于公开、用户还是管理员区域。
- 在 Router 添加 lazy route 和正确 meta。
- 在对应布局/导航中按 feature flag 展示入口。
- 新建类型化 API 函数,不在组件拼 URL。
- 为 loading、empty、error、disabled 和 permission denied 设计状态。
- 后端添加对应 route、middleware、DTO、Service 和审计。
- i18n 同步所有受支持语言,避免把中文写死在组件。
- 添加 Vitest,至少覆盖权限/feature flag 和关键表单。
- 生产构建验证 lazy chunk、刷新和直接打开 URL。
修改设置页时的风险
Settings 页面字段很多,部分 Secret 采用“空值表示不修改”。新增字段时要区分:
false/0/[]是用户明确设置,还是 DTO 省略后的零值?- Secret 留空是清除,还是保留原值?
- 旧版本数据库没有该键时使用什么默认?
- 一个大表单保存是否会意外固化其他隐式默认?
- 保存后哪些服务对象要刷新?
推荐使用可选字段、明确的 clear action 和服务端 diff/audit,而不是把整个设置对象无条件覆盖。
前端验证
仓库根目录提供:
make test-frontend
make build-frontend前者执行 ESLint、TypeScript 检查和关键 Vitest;后者执行 vue-tsc -b 与 Vite build。重要页面还应在浏览器验证:
- 未登录、普通用户、管理员三种身份。
- 页面刷新与深链接。
- access token 过期后的并发请求。
- feature flag 关闭。
- slow network、API 失败和重复提交。
- 移动端宽度和暗色模式。