- 导入 TDesign react-19-adapter 修复 MessagePlugin 在 React 19 下的渲染错误 - Dialog 禁用蒙版点击和 ESC 键关闭,防止误操作丢失表单数据 - 重构弹窗关闭逻辑,使用 mutateAsync 替代 useEffect 监听 isSuccess - 成功后自动关闭弹窗,失败后保持弹窗打开并显示错误提示
441 lines
16 KiB
Markdown
441 lines
16 KiB
Markdown
# 前端配置界面
|
||
|
||
## Purpose
|
||
|
||
TBD - 提供供应商、模型配置和用量统计的前端管理界面
|
||
|
||
## Requirements
|
||
|
||
### Requirement: 提供供应商管理页面
|
||
|
||
前端 SHALL 使用 TDesign 组件提供供应商管理页面。
|
||
|
||
#### Scenario: 显示供应商列表
|
||
|
||
- **WHEN** 加载供应商管理页面
|
||
- **THEN** 前端 SHALL 使用 TDesign Table 显示所有已配置供应商
|
||
- **THEN** 每个供应商 SHALL 显示 name、base_url 和 enabled 状态(使用 Tag 组件)
|
||
- **THEN** API Key SHALL 被脱敏显示(掩码处理)
|
||
- **THEN** 表格 SHALL 支持展开行以显示关联模型
|
||
|
||
#### Scenario: 表格列宽约束
|
||
|
||
- **WHEN** 渲染供应商表格
|
||
- **THEN** 名称列 SHALL 固定宽度 180px 并启用 ellipsis(超长文本显示省略号,hover 显示 Tooltip)
|
||
- **THEN** Base URL 列 SHALL 不设固定宽度(浮动填充剩余空间)并启用 ellipsis + Tooltip
|
||
- **THEN** API Key 列 SHALL 固定宽度 120px 并启用 ellipsis
|
||
- **THEN** 状态列 SHALL 固定宽度 80px
|
||
- **THEN** 操作列 SHALL 固定宽度 160px
|
||
|
||
#### Scenario: 表格空状态
|
||
|
||
- **WHEN** 供应商列表为空
|
||
- **THEN** 表格 SHALL 显示自定义空状态文案 "暂无供应商,点击上方按钮添加"
|
||
|
||
#### Scenario: 添加新供应商
|
||
|
||
- **WHEN** 用户点击"添加供应商"按钮
|
||
- **THEN** 前端 SHALL 使用 TDesign Dialog + Form 显示输入表单
|
||
- **THEN** 表单 SHALL 包含 id、name、api_key、base_url 字段,带校验规则
|
||
- **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 使用 TDesign Dialog + Form 显示预填充数据的表单
|
||
- **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 使用 TDesign Popconfirm 弹出确认
|
||
- **WHEN** 用户确认删除
|
||
- **THEN** 前端 SHALL 通过 useMutation 调用删除 API
|
||
- **THEN** 成功后 SHALL 刷新供应商列表
|
||
|
||
### Requirement: 提供模型管理界面
|
||
|
||
前端 SHALL 在供应商页面展开行中提供模型管理。
|
||
|
||
#### Scenario: 显示供应商的模型
|
||
|
||
- **WHEN** 展开供应商行
|
||
- **THEN** 前端 SHALL 显示该供应商的模型列表
|
||
- **THEN** 每个模型 SHALL 显示 model_name 和 enabled 状态
|
||
- **THEN** 模型名称列 SHALL 启用 ellipsis(超长文本显示省略号,hover 显示 Tooltip)
|
||
|
||
#### Scenario: 模型表格空状态
|
||
|
||
- **WHEN** 模型列表为空
|
||
- **THEN** 表格 SHALL 显示自定义空状态文案 "暂无模型,点击上方按钮添加"
|
||
|
||
#### Scenario: 为供应商添加模型
|
||
|
||
- **WHEN** 用户在展开行中点击"添加模型"
|
||
- **THEN** 前端 SHALL 显示 TDesign Dialog + Form
|
||
- **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** 编辑表单 SHALL 显示统一模型 ID(只读)
|
||
- **THEN** ID 字段 SHALL 为禁用状态
|
||
- **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 使用 Popconfirm 弹出确认
|
||
- **WHEN** 用户确认删除
|
||
- **THEN** 前端 SHALL 通过 useMutation 调用删除 API
|
||
- **THEN** 成功后 SHALL 刷新模型列表
|
||
|
||
### Requirement: 显示协议字段
|
||
|
||
前端 SHALL 在供应商管理界面显示协议字段。
|
||
|
||
#### Scenario: 供应商表格显示协议列
|
||
|
||
- **WHEN** 渲染供应商表格
|
||
- **THEN** 表格 SHALL 包含协议列
|
||
- **THEN** 协议列 SHALL 显示 "OpenAI" 或 "Anthropic" 标签
|
||
- **THEN** OpenAI 协议 SHALL 使用主题色标签
|
||
- **THEN** Anthropic 协议 SHALL 使用成功色标签
|
||
|
||
#### Scenario: 供应商表单选择协议
|
||
|
||
- **WHEN** 创建或编辑供应商
|
||
- **THEN** 表单 SHALL 包含协议选择下拉框
|
||
- **THEN** 下拉框 SHALL 提供 "OpenAI" 和 "Anthropic" 选项
|
||
- **THEN** 协议字段 SHALL 为必填项
|
||
|
||
### 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** 编辑模型表单
|
||
- **THEN** 表单 SHALL 显示统一模型 ID 字段
|
||
- **THEN** 统一模型 ID 字段 SHALL 为只读(disabled)
|
||
- **THEN** 统一模型 ID 字段 SHALL 显示格式说明 "格式:provider_id/model_name"
|
||
|
||
#### 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: 提供统计查看页面
|
||
|
||
前端 SHALL 使用 TDesign 组件提供统计仪表盘页面。
|
||
|
||
#### Scenario: 显示统计概览
|
||
|
||
- **WHEN** 加载统计页面
|
||
- **THEN** 前端 SHALL 在顶部显示统计摘要卡片(总请求量、活跃模型数、活跃供应商数、今日请求量)
|
||
- **THEN** 统计摘要数据 SHALL 从 stats API 返回数据中前端聚合
|
||
- **THEN** 前端 SHALL 显示请求趋势折线图
|
||
- **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: 优雅处理 API 错误
|
||
|
||
前端 SHALL 处理 API 错误并显示用户友好的消息。
|
||
|
||
#### Scenario: API 请求失败
|
||
|
||
- **WHEN** API 请求失败(网络错误、4xx、5xx)
|
||
- **THEN** 前端 SHALL 显示全局错误提示
|
||
- **THEN** 错误消息 SHALL 具有描述性
|
||
|
||
#### Scenario: 验证错误
|
||
|
||
- **WHEN** 用户提交包含无效数据的表单
|
||
- **THEN** 前端 SHALL 在相关字段旁显示验证错误
|
||
- **THEN** 前端 SHALL 阻止表单提交
|
||
|
||
### Requirement: 提供响应式布局
|
||
|
||
前端 SHALL 使用 TDesign Layout 提供侧边栏导航布局。
|
||
|
||
#### Scenario: 桌面布局
|
||
|
||
- **WHEN** 在桌面屏幕上查看前端
|
||
- **THEN** 布局 SHALL 使用 TDesign `Layout.Aside` + `Menu`
|
||
- **THEN** 侧边栏 SHALL 显示导航菜单,包含图标和文字标签
|
||
- **THEN** 侧边栏 SHALL 使用固定宽度 232px
|
||
|
||
#### Scenario: 页面内容区域
|
||
|
||
- **WHEN** 显示页面内容
|
||
- **THEN** 内容区域 SHALL 在 `Layout.Content` 中渲染
|
||
- **THEN** 页面之间 SHALL 通过 React Router Outlet 渲染
|
||
|
||
#### Scenario: Header 区域
|
||
|
||
- **WHEN** 渲染页面 Header
|
||
- **THEN** Header SHALL 仅显示当前页面标题
|
||
- **THEN** Header SHALL 不包含导航菜单
|
||
|
||
### Requirement: 使用 TDesign UI 组件库
|
||
|
||
前端 SHALL 使用 TDesign 作为 UI 组件库。
|
||
|
||
### Requirement: 样式体系
|
||
|
||
前端样式 SHALL 优先使用 TDesign 样式体系,SCSS 作为补充工具。
|
||
|
||
#### Scenario: TDesign 样式优先
|
||
|
||
- **WHEN** 实现组件样式
|
||
- **THEN** 前端 SHALL 优先使用 TDesign 组件的 style prop
|
||
- **THEN** 前端 SHALL 使用 TDesign Layout 组件处理布局
|
||
|
||
#### Scenario: SCSS 补充使用
|
||
|
||
- **WHEN** TDesign 样式体系无法满足需求
|
||
- **THEN** 前端 MAY 使用 SCSS 作为补充
|
||
- **THEN** SCSS 文件 SHALL 使用 *.module.scss(SCSS Modules)
|
||
- **THEN** 前端 SHALL NOT 使用纯 CSS 文件(*.css)
|
||
|
||
#### Scenario: 移除冗余 SCSS
|
||
|
||
- **WHEN** SCSS 文件仅实现 TDesign 已有的功能
|
||
- **THEN** 前端 SHALL 移除该 SCSS 文件
|
||
- **THEN** 前端 SHALL 使用 TDesign 内置功能替代
|
||
|
||
### 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 导航到 `/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 具有描述性
|