1
0
Files
nex/openspec/specs/frontend/spec.md
lanyuanxiaoyao b3258e76df perf: 前端打包产物优化——路由级懒加载和 vendor 分包
- 使用 React.lazy() + Suspense 实现路由级代码分割
- 配置 manualChunks 将 react/tdesign/recharts 拆分为独立 vendor chunk
- 页面组件改为 export default 以支持动态导入
- 新增 bundle-optimization 规范,更新 frontend 导航规范
2026-04-23 00:26:54 +08:00

552 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 前端配置界面
## Purpose
TBD - 提供供应商、模型配置和用量统计的前端管理界面
## Requirements
### Requirement: 样式体系
前端样式 SHALL 优先使用 TDesign 样式体系SCSS 作为补充工具。
#### Scenario: TDesign 组件 Props 优先
- **WHEN** 实现组件视觉效果
- **THEN** 前端 SHALL 优先使用 TDesign 组件的视觉增强 Props如 color、trend、hoverShadow、stripe、variant、shape 等)
- **THEN** 前端 SHALL NOT 通过 CSS 类名覆盖组件内部样式
#### Scenario: CSS Variables 主题微调
- **WHEN** 需要调整全局视觉风格
- **THEN** 前端 SHALL 通过 \`:root\` 中声明 TDesign CSS Variables\`--td-*\`)进行覆盖
- **THEN** 前端 SHALL NOT 使用 \`!important\` 或高优先级选择器覆盖组件样式
#### Scenario: 布局样式 Token 化
- **WHEN** 编写布局级 inline style
- **THEN** 前端 SHALL 使用 TDesign CSS Token 引用(\`var(--td-*)\`)替代硬编码颜色值
- **THEN** 前端 SHALL NOT 在布局样式中硬编码 \`#fff\`、\`#e7e7e7\`、\`#999\` 等颜色值
#### Scenario: SCSS 补充使用
- **WHEN** TDesign 样式体系无法满足需求
- **THEN** 前端 MAY 使用 SCSS 作为补充
- **THEN** SCSS 文件 SHALL 仅用于 \`:root\` 级别的 CSS Variables 声明和全局 reset
- **THEN** 前端 SHALL NOT 使用纯 CSS 文件(*.css
### Requirement: 提供统计查看页面
前端 SHALL 使用 TDesign 组件提供统计仪表盘页面。
#### Scenario: 显示统计概览
- **WHEN** 加载统计页面
- **THEN** 前端 SHALL 在顶部显示统计摘要卡片(总请求量、活跃模型数、活跃供应商数、今日请求量)
- **THEN** 统计摘要数据 SHALL 从 stats API 返回数据中前端聚合
- **THEN** 每个 Statistic 组件 SHALL 使用 \`color\` prop 设置预设颜色风格
- **THEN** 每个 Statistic 组件 SHALL 使用 \`prefix\` prop 显示图标前缀
- **THEN** 每个 Statistic 组件 SHALL 使用 \`suffix\` prop 显示单位文字
- **THEN** Statistic 组件首次加载 SHALL 使用 \`animation\` prop 展示数字滚动动画
- **THEN** Card 组件 SHALL 设置 \`bordered={false}\` 和 \`hoverShadow\`
- **THEN** 前端 SHALL 显示请求趋势折线图
- **THEN** 趋势图表卡片 SHALL 设置 \`headerBordered\`
- **THEN** 前端 SHALL 使用 TDesign Table 显示统计数据
- **THEN** 统计数据 SHALL 按供应商和模型显示请求计数
#### Scenario: 统计表格列宽约束
- **WHEN** 渲染统计表格
- **THEN** 供应商列 SHALL 固定宽度 180px 并启用 ellipsis + Tooltip
- **THEN** 模型列 SHALL 固定宽度 250px 并启用 ellipsis + Tooltip
- **THEN** 日期列 SHALL 固定宽度 120px
- **THEN** 请求数列 SHALL 固定宽度 100px 并右对齐
#### Scenario: 统计表格空状态
- **WHEN** 统计数据为空
- **THEN** 表格 SHALL 显示自定义空状态文案 "暂无统计数据"
#### Scenario: 按供应商过滤统计
- **WHEN** 用户从 TDesign Select 选择供应商
- **THEN** 前端 SHALL 自动查询并过滤统计
- **THEN** 统计摘要卡片和趋势图表 SHALL 同步更新
#### Scenario: 按日期范围过滤统计
- **WHEN** 用户使用 TDesign DateRangePicker 选择日期范围
- **THEN** 前端 SHALL 自动查询并过滤统计
- **THEN** 统计摘要卡片和趋势图表 SHALL 同步更新
#### Scenario: 仪表盘区域排列
- **WHEN** 渲染统计页面
- **THEN** 页面 SHALL 按以下顺序从上到下排列:统计摘要卡片、趋势图表、筛选栏和数据表格
- **THEN** 各区域之间 SHALL 有合理的垂直间距
- **THEN** 筛选栏和数据表格 SHALL 保持在同一个卡片中
#### Scenario: 数据联动
- **WHEN** 用户通过筛选栏修改筛选条件
- **THEN** 统计摘要卡片和趋势图表 SHALL 随筛选条件变化更新
- **THEN** 数据表格 SHALL 同步更新
- **THEN** 所有区域 SHALL 共享同一份筛选后的数据
### Requirement: 提供供应商管理页面
前端 SHALL 使用 TDesign 组件提供供应商管理页面。
#### Scenario: 显示供应商列表
- **WHEN** 加载供应商管理页面
- **THEN** 前端 SHALL 使用 TDesign Table 显示所有已配置供应商
- **THEN** Table SHALL 启用 \`stripe\`(斑马纹)和 \`hover\`(行悬浮高亮)
- **THEN** 每个供应商 SHALL 显示 name、base_url 和 enabled 状态(使用 Tag 组件)
- **THEN** 状态 Tag SHALL 使用 \`variant="light"\` 和 \`shape="round"\`
- **THEN** 协议 Tag SHALL 使用 \`variant="light"\` 和 \`shape="round"\`
- **THEN** API Key SHALL 显示完整值(不进行掩码处理)
- **THEN** 表格 SHALL 支持展开行以显示关联模型
- **THEN** Card 组件 SHALL 设置 \`hoverShadow\` 和 \`headerBordered\`
#### Scenario: 表格列宽约束
- **WHEN** 渲染供应商表格
- **THEN** 名称列 SHALL 固定宽度 180px 并启用 ellipsis超长文本显示省略号hover 显示 Tooltip
- **THEN** Base URL 列 SHALL 不设固定宽度(浮动填充剩余空间)并启用 ellipsis + Tooltip
- **THEN** API Key 列 SHALL 不设固定宽度(浮动填充剩余空间)并启用 ellipsis + Tooltip
- **THEN** 状态列 SHALL 固定宽度 80px
- **THEN** 操作列 SHALL 固定宽度 160px
#### Scenario: 表格空状态
- **WHEN** 供应商列表为空
- **THEN** 表格 SHALL 显示自定义空状态文案 "暂无供应商,点击上方按钮添加"
#### Scenario: 添加新供应商
- **WHEN** 用户点击"添加供应商"按钮
- **THEN** 前端 SHALL 使用 TDesign Dialog + Form 显示输入表单
- **THEN** Dialog SHALL 使用 \`placement="center"\` 居中显示
- **THEN** Dialog SHALL 设置 \`width="520px"\`
- **THEN** 表单 SHALL 包含 id、name、api_key、base_url 字段,带校验规则
- **THEN** Dialog SHALL 禁用蒙版点击关闭closeOnOverlayClick={false}
- **THEN** Dialog SHALL 禁用 ESC 键关闭closeOnEscKeydown={false}
- **THEN** Dialog SHALL 设置 lazy={false} 禁用懒加载
- **WHEN** 用户提交包含有效数据的表单
- **THEN** 前端 SHALL 通过 mutateAsync 调用创建 API
- **THEN** 成功后 SHALL 关闭 Dialog 并刷新供应商列表
- **THEN** 失败 SHALL 保持 Dialog 打开并显示错误提示MessagePlugin.error
#### Scenario: 编辑现有供应商
- **WHEN** 用户点击供应商的"编辑"按钮
- **THEN** 前端 SHALL 使用 TDesign Dialog + Form 显示预填充数据的表单
- **THEN** Dialog SHALL 使用 \`placement="center"\` 居中显示
- **THEN** Dialog SHALL 设置 \`width="520px"\`
- **THEN** API Key SHALL 回显当前值(完整值)
- **THEN** API Key 输入框 SHALL 为普通文本输入(不使用 password 类型)
- **THEN** API Key 字段 SHALL 始终为必填
- **THEN** Dialog SHALL 禁用蒙版点击关闭closeOnOverlayClick={false}
- **THEN** Dialog SHALL 禁用 ESC 键关闭closeOnEscKeydown={false}
- **THEN** Dialog SHALL 设置 lazy={false} 禁用懒加载
- **WHEN** 用户提交包含更新数据的表单
- **THEN** 前端 SHALL 通过 mutateAsync 调用更新 API
- **THEN** 成功后 SHALL 关闭 Dialog 并刷新供应商列表
- **THEN** 失败 SHALL 保持 Dialog 打开并显示错误提示MessagePlugin.error
#### Scenario: 删除供应商
- **WHEN** 用户点击供应商的"删除"按钮
- **THEN** 前端 SHALL 使用 TDesign Popconfirm 弹出确认
- **WHEN** 用户确认删除
- **THEN** 前端 SHALL 通过 useMutation 调用删除 API
- **THEN** 成功后 SHALL 刷新供应商列表
### Requirement: 提供模型管理界面
前端 SHALL 在供应商页面展开行中提供模型管理。
#### Scenario: 显示供应商的模型
- **WHEN** 展开供应商行
- **THEN** 前端 SHALL 显示该供应商的模型列表
- **THEN** Table SHALL 启用 \`stripe\`(斑马纹)和 \`hover\`(行悬浮高亮)
- **THEN** 每个模型 SHALL 显示 model_name 和 enabled 状态
- **THEN** 状态 Tag SHALL 使用 \`variant="light"\` 和 \`shape="round"\`
- **THEN** 模型名称列 SHALL 启用 ellipsis超长文本显示省略号hover 显示 Tooltip
#### Scenario: 模型表格空状态
- **WHEN** 模型列表为空
- **THEN** 表格 SHALL 显示自定义空状态文案 "暂无模型,点击上方按钮添加"
#### Scenario: 为供应商添加模型
- **WHEN** 用户在展开行中点击"添加模型"
- **THEN** 前端 SHALL 显示 TDesign Dialog + Form
- **THEN** Dialog SHALL 使用 \`placement="center"\` 居中显示
- **THEN** Dialog SHALL 设置 \`width="520px"\`
- **THEN** provider_id SHALL 自动关联当前供应商
- **THEN** 供应商选择 SHALL 使用 \`options\` 属性
- **THEN** 创建表单 SHALL NOT 包含 ID 输入框(后端自动生成 UUID
- **THEN** Dialog SHALL 禁用蒙版点击关闭closeOnOverlayClick={false}
- **THEN** Dialog SHALL 禁用 ESC 键关闭closeOnEscKeydown={false}
- **WHEN** 用户提交表单
- **THEN** 前端 SHALL 通过 mutateAsync 调用创建 API
- **THEN** 成功后 SHALL 关闭 Dialog 并刷新模型列表
- **THEN** 失败 SHALL 保持 Dialog 打开并显示错误提示MessagePlugin.error
#### Scenario: 编辑模型
- **WHEN** 用户点击模型的"编辑"
- **THEN** 前端 SHALL 显示编辑表单
- **THEN** Dialog SHALL 使用 \`placement="center"\` 居中显示
- **THEN** Dialog SHALL 设置 \`width="520px"\`
- **THEN** 编辑表单 SHALL NOT 包含统一模型 ID 字段
- **THEN** Dialog SHALL 禁用蒙版点击关闭closeOnOverlayClick={false}
- **THEN** Dialog SHALL 禁用 ESC 键关闭closeOnEscKeydown={false}
- **THEN** Dialog SHALL 设置 lazy={false} 禁用懒加载
- **WHEN** 用户提交表单
- **THEN** 前端 SHALL 通过 mutateAsync 调用更新 API
- **THEN** 成功后 SHALL 关闭 Dialog 并刷新模型列表
- **THEN** 失败 SHALL 保持 Dialog 打开并显示错误提示MessagePlugin.error
#### Scenario: 删除模型
- **WHEN** 用户点击模型的"删除"
- **THEN** 前端 SHALL 使用 Popconfirm 弹出确认
- **WHEN** 用户确认删除
- **THEN** 前端 SHALL 通过 useMutation 调用删除 API
- **THEN** 成功后 SHALL 刷新模型列表
### Requirement: 提供响应式布局
前端 SHALL 使用 TDesign Layout 提供侧边栏导航布局。
#### Scenario: 桌面布局
- **WHEN** 在桌面屏幕上查看前端
- **THEN** 布局 SHALL 使用 TDesign \`Layout.Aside\` + \`Menu\`
- **THEN** 侧边栏 SHALL 显示导航菜单,包含图标和文字标签
- **THEN** 侧边栏 SHALL 使用固定宽度 232px
- **THEN** Menu 组件 SHALL 使用 \`logo\` prop 显示品牌标识
- **THEN** Menu 组件 SHALL 使用 \`operations\` prop 在底部显示操作区域
- **THEN** Menu 组件 SHALL 支持 \`collapsed\` 折叠功能
#### Scenario: 页面内容区域
- **WHEN** 显示页面内容
- **THEN** 内容区域 SHALL 在 \`Layout.Content\` 中渲染
- **THEN** 页面之间 SHALL 通过 React Router Outlet 渲染
#### Scenario: Header 区域
- **WHEN** 渲染页面 Header
- **THEN** Header SHALL 仅显示当前页面标题
- **THEN** Header SHALL 不包含导航菜单
- **THEN** Header 背景色 SHALL 使用 \`var(--td-bg-color-container)\` Token
- **THEN** Header 底部分割线 SHALL 使用 \`var(--td-component-stroke)\` Token
### Requirement: 显示协议字段
前端 SHALL 在供应商管理界面显示协议字段。
#### Scenario: 供应商表格显示协议列
- **WHEN** 渲染供应商表格
- **THEN** 表格 SHALL 包含协议列
- **THEN** 协议列 SHALL 显示 "OpenAI" 或 "Anthropic" 标签
- **THEN** 标签 SHALL 使用 \`variant="light"\` 和 \`shape="round"\`
- **THEN** OpenAI 协议 SHALL 使用主题色标签
- **THEN** Anthropic 协议 SHALL 使用成功色标签
#### Scenario: 供应商表单选择协议
- **WHEN** 创建或编辑供应商
- **THEN** 表单 SHALL 包含协议选择下拉框
- **THEN** 下拉框 SHALL 提供 "OpenAI" 和 "Anthropic" 选项
- **THEN** 协议字段 SHALL 为必填项
### Requirement: 使用 TDesign UI 组件库
前端 SHALL 使用 TDesign 作为 UI 组件库。
#### Scenario: 全局配置
- **WHEN** 应用启动
- **THEN** ConfigProvider SHALL 注入全局配置
- **THEN** 全局配置 SHALL 包含 \`animation\` 配置ripple、expand、fade 动画开启)
- **THEN** 全局配置 SHALL 包含 \`table.size\` 默认尺寸设置
### Requirement: 主题定制
前端 SHALL 通过 TDesign CSS Variables 进行主题微调。
#### Scenario: 页面背景色
- **WHEN** 渲染页面
- **THEN** \`:root\` SHALL 设置 \`--td-bg-color-page\` 为 \`#f5f7fa\`
#### Scenario: 圆角调整
- **WHEN** 渲染组件
- **THEN** \`:root\` SHALL 设置 \`--td-radius-default\` 为 \`6px\`
- **THEN** \`:root\` SHALL 设置 \`--td-radius-medium\` 为 \`9px\`
- **THEN** \`:root\` SHALL 设置 \`--td-radius-large\` 为 \`12px\`
- **THEN** \`:root\` SHALL 设置 \`--td-radius-extraLarge\` 为 \`16px\`
#### Scenario: 字体栈声明
- **WHEN** 渲染页面
- **THEN** \`:root\` SHALL 设置 \`--td-font-family\` 为系统字体栈(-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto 等)
### Requirement: 提供请求趋势图表
前端 SHALL 使用 Recharts 提供请求趋势可视化。
#### Scenario: 显示面积图表
- **WHEN** 渲染请求趋势图表
- **THEN** 前端 SHALL 使用 Recharts AreaChart + Area 组件(替代 LineChart + Line
- **THEN** 面积填充 SHALL 使用线性渐变(从品牌色到透明)
- **THEN** 图表 SHALL 仅使用 Recharts 组件 Props API 定制样式
- **THEN** 前端 SHALL NOT 通过 CSS 覆盖 Recharts 内部样式类名
#### Scenario: 图表加载态
- **WHEN** 统计数据加载中
- **THEN** UsageChart 的 Card 组件 SHALL 显示骨架屏loading prop
- **THEN** StatsPage SHALL 向 UsageChart 传递 isLoading 状态
### Requirement: 提供 404 页面
前端 SHALL 提供页面未找到的 404 页面。
#### Scenario: 显示 404 页面
- **WHEN** 用户访问不存在的路由
- **THEN** 前端 SHALL 显示 404 页面
- **THEN** 404 标题颜色 SHALL 使用 \`var(--td-text-color-placeholder)\` Token
- **THEN** 描述文字颜色 SHALL 使用 \`var(--td-text-color-secondary)\` Token
### Requirement: 提供设置页面
前端 SHALL 提供设置页面。
#### Scenario: 显示设置页面
- **WHEN** 用户访问设置页面
- **THEN** 前端 SHALL 显示设置页面
- **THEN** 开发中提示文字颜色 SHALL 使用 \`var(--td-text-color-placeholder)\` Token
### Requirement: 显示统一模型 ID
前端 SHALL 在所有显示模型的地方使用统一模型 ID。
#### Scenario: 模型表格显示统一 ID 列
- **WHEN** 渲染模型表格
- **THEN** 表格 SHALL 包含统一模型 ID 列
- **THEN** 统一模型 ID 列 SHALL 显示 \`provider_id/model_name\` 格式
- **THEN** 统一模型 ID 列 SHALL 启用 ellipsis超长文本显示省略号hover 显示 Tooltip
- **THEN** 统一模型 ID 列 SHALL 固定宽度 250px
#### Scenario: 统一模型 ID 降级显示
- **WHEN** 后端未返回 unified_id 字段
- **THEN** 前端 SHALL 拼接 providerId 和 modelName 显示
- **THEN** 拼接格式 SHALL 为 \`{providerId}/{modelName}\`
### Requirement: 提取并映射错误码
前端 SHALL 提取后端结构化错误响应中的错误码并映射为友好消息。
#### Scenario: API 客户端提取错误码
- **WHEN** 后端返回结构化错误响应 \`{error: string, code: string}\`
- **THEN** API 客户端 SHALL 提取 code 字段
- **THEN** ApiError 对象 SHALL 包含 code 字段
- **THEN** code 字段 SHALL 为可选(兼容旧错误格式)
#### Scenario: Hooks 映射错误码为中文消息
- **WHEN** 处理 API 错误
- **THEN** Hooks SHALL 使用错误码映射表
- **THEN** 映射表 SHALL 包含以下错误码:
- \`duplicate_model\` → "同一供应商下模型名称已存在"
- \`invalid_provider_id\` → "供应商 ID 仅允许字母、数字、下划线,长度 1-64"
- \`immutable_field\` → "供应商 ID 不允许修改"
- **THEN** 未定义的错误码 SHALL 降级使用原始错误消息
### Requirement: 优雅处理 API 错误
前端 SHALL 处理 API 错误并显示用户友好的消息。
#### Scenario: API 请求失败
- **WHEN** API 请求失败网络错误、4xx、5xx
- **THEN** 前端 SHALL 显示全局错误提示
- **THEN** 错误消息 SHALL 具有描述性
#### Scenario: 验证错误
- **WHEN** 用户提交包含无效数据的表单
- **THEN** 前端 SHALL 在相关字段旁显示验证错误
- **THEN** 前端 SHALL 阻止表单提交
### Requirement: 提供侧边栏导航
前端 SHALL 使用 TDesign \`Layout.Aside\` 提供侧边栏导航。
#### Scenario: 侧边栏内容
- **WHEN** 渲染侧边栏
- **THEN** 侧边栏顶部 SHALL 显示应用名称/Logo
- **THEN** 侧边栏 SHALL 包含导航菜单
- **THEN** 导航菜单项 SHALL 包含供应商管理ServerIcon 图标、用量统计ChartLineIcon 图标、设置SettingIcon 图标)
#### Scenario: 导航菜单交互
- **WHEN** 用户点击导航中的"供应商管理"
- **THEN** 前端 SHALL 导航到 \`/providers\` 并高亮当前菜单项
- **WHEN** 用户点击导航中的"用量统计"
- **THEN** 前端 SHALL 导航到 \`/stats\` 并高亮当前菜单项
- **WHEN** 用户点击导航中的"设置"
- **THEN** 前端 SHALL 导航到 \`/settings\` 并高亮当前菜单项
### Requirement: 提供导航
前端 SHALL 使用 React Router v7 提供导航,并支持路由级懒加载。
#### Scenario: 路由配置
- **WHEN** 应用启动
- **THEN** 前端 SHALL 使用 React Router v7 Library 模式BrowserRouter
- **THEN** `/providers` 路径 SHALL 显示供应商管理页面
- **THEN** `/stats` 路径 SHALL 显示用量统计页面
- **THEN** `/` 路径 SHALL 重定向到 `/providers`
- **THEN** 不存在的路径 SHALL 显示 404 页面
#### Scenario: 路由级懒加载
- **WHEN** 用户访问某个路由
- **THEN** 前端 SHALL 使用 `React.lazy()` 按需加载对应页面组件
- **THEN** 页面组件加载期间 SHALL 显示 TDesign `Loading` 组件作为 fallback
- **THEN** 所有页面组件 SHALL 通过动态 `import()` 导入
#### Scenario: 导航菜单
- **WHEN** 用户点击导航中的"供应商管理"
- **THEN** 前端 SHALL 导航到 `/providers` 并高亮当前菜单项
- **WHEN** 用户点击导航中的"用量统计"
- **THEN** 前端 SHALL 导航到 `/stats` 并高亮当前菜单项
#### Scenario: URL 同步
- **WHEN** 用户在供应商页面刷新浏览器
- **THEN** 前端 SHALL 保持在供应商页面URL 持久化)
- **WHEN** 用户使用浏览器后退按钮
- **THEN** 前端 SHALL 正确导航到上一个页面
### Requirement: React 19 适配器
前端 SHALL 导入 TDesign react-19-adapter 以支持 React 19。
#### Scenario: 导入适配器
- **WHEN** 应用启动
- **THEN** main.tsx SHALL 导入 'tdesign-react/es/_util/react-19-adapter'
- **THEN** MessagePlugin、DialogPlugin 等插件式调用 SHALL 正常工作
#### Scenario: 错误提示显示
- **WHEN** API 请求失败
- **THEN** MessagePlugin.error SHALL 正确渲染错误提示
- **THEN** 错误提示 SHALL 显示在页面顶部placement: top
- **THEN** 错误提示 SHALL 在 3 秒后自动消失
### Requirement: 使用 React 和 TypeScript
前端 SHALL 使用 React 和 TypeScript 实现,遵循 strict 模式。
#### Scenario: TypeScript strict 模式
- **WHEN** 编写前端代码
- **THEN** TypeScript 配置 SHALL 开启 strict: true
- **THEN** TypeScript 配置 SHALL 开启 noUncheckedIndexedAccess
- **THEN** 所有代码 SHALL NOT 使用 any 类型
- **THEN** tsconfig SHALL 合并为单文件(不使用 project references
#### Scenario: React 函数组件
- **WHEN** 实现 UI
- **THEN** 它 SHALL 使用 React 函数组件
- **THEN** 它 SHALL 使用自定义 Hooks 封装业务逻辑
### Requirement: 使用 Vite 构建
前端 SHALL 使用 Vite 作为构建工具。
#### Scenario: 开发服务器
- **WHEN** 在开发模式下启动前端
- **THEN** Vite SHALL 使用热模块替换服务应用
#### Scenario: 生产构建
- **WHEN** 为生产构建前端
- **THEN** Vite SHALL 生成优化的静态文件
### Requirement: 与后端 API 通信
前端 SHALL 使用 TanStack Query v5 和统一 API 客户端与后端通信。
#### Scenario: API 基础 URL 配置
- **WHEN** 前端发起 API 请求
- **THEN** 开发环境 SHALL 通过 Vite proxy 转发 /api 请求到后端
- **THEN** 生产环境 SHALL 使用环境变量 VITE_API_BASE 配置基础 URL
- **THEN** 前端 SHALL NOT 硬编码 API 基础 URL
#### Scenario: 统一 API 客户端
- **WHEN** 进行 API 调用
- **THEN** 所有调用 SHALL 通过 api/client.ts 的 request<T>() 方法
- **THEN** 错误处理 SHALL 统一抛出 ApiError包含 status 和 message
- **THEN** 开发环境 SHALL 使用 Vite proxy 转发 API 请求
#### Scenario: 字段名转换
- **WHEN** 接收后端 API 响应
- **THEN** API 层 SHALL 将 snake_case 字段转换为 camelCase
- **WHEN** 发送请求到后端 API
- **THEN** API 层 SHALL 将 camelCase 字段转换为 snake_case
- **THEN** hooks 和组件 SHALL 仅使用 camelCase 字段
#### Scenario: TanStack Query 数据管理
- **WHEN** 页面加载数据
- **THEN** 前端 SHALL 使用 TanStack Query 的 useQuery hook
- **THEN** 前端 SHALL 自动缓存请求结果
- **THEN** 前端 SHALL 自动处理加载和错误状态
#### Scenario: TanStack Query 写操作
- **WHEN** 用户执行创建、更新或删除操作
- **THEN** 前端 SHALL 使用 TanStack Query 的 useMutation hook
- **THEN** 操作成功后 SHALL 自动失效相关查询缓存
- **THEN** 操作失败 SHALL 显示错误提示
#### Scenario: 错误提示
- **WHEN** API 请求失败网络错误、4xx、5xx
- **THEN** 前端 SHALL 显示全局错误提示
- **THEN** 错误消息 SHALL 具有描述性