Files
Alfred/docs/development/frontend.md

18 KiB
Raw Blame History

前端开发

本文档说明 alfred 前端的 React、Ant Design、TanStack Query、组件、样式和前端测试约定。

适用场景:修改 src/web/、前端共享类型使用方式、组件结构、样式规则或前端测试。

技术栈

层面 技术 用途
框架 React 19 UI 组件开发
构建 Vite开发+ Bun compile生产 开发服务 HMR 与生产构建
语言 TypeScript 类型安全
UI 库 Ant Design (antd) + @ant-design/icons UI 组件与图标
数据层 TanStack Query (React Query) + React Query Devtools 服务端状态管理与自动刷新
AI 层 Vercel AI SDK (@ai-sdk/react) 聊天状态管理与流式通信
路由 React Router v7 (Declarative mode) SPA 路由与页面导航

不引入额外状态管理库。TanStack Query 承担服务端状态,组件内状态使用 useState。

组件开发规范

  • 每个 React 组件一个 .tsx 文件,文件名使用 PascalCase
  • 组件 props 定义为 interface XxxProps紧邻组件函数声明
  • 类型从 src/shared/api.ts 导入,使用 import type
  • 展示组件放在 components/,通过 props 接收数据,通过回调返回事件;页面专属展示组件可就近放在 pages/*/components/
  • 容器逻辑放在 hooks 中,组件只做数据消费;全局共享查询可提取为独立 hook如 use-meta
  • 工具函数放在 utils/,保持纯函数无副作用

页面组件保持编排职责,组合 hooks 和展示组件;当页面同时承担查询、筛选、分页、表格列、弹窗表单和 mutation 时,应按工具栏、表格、表单弹窗等功能边界拆分。拆分以降低职责复杂度为目标,避免为了拆分而拆分。

Ant Design 使用规范

  • 优先使用 antd 组件默认状态和官方交互模式;没有明确产品定制需求时,不额外改写组件视觉。
  • 优先通过组件 props 配置行为和外观,例如 collapsiblethemescrolllocalestatusvariantcolor
  • 全局使用 ConfigProvider 配置 antd 中文 localeantd/locale/zh_CN 导入 zhCN
  • 需要 message、modal、notification 等 antd 应用级能力时,在 ConfigProvider 内包裹 App(代码中可别名为 AntApp),组件内通过 App.useApp() 获取。
  • 状态页、异常页、空结果优先使用 ResultEmptyAlertSpin 等 antd 组件。
  • 信息展示优先使用 TypographyCardDescriptionsTable 等 antd 组件,避免用原生标签加自定义 CSS 复刻。
  • 搜索输入优先使用 Input.Search,保持回车搜索、按钮搜索和清空行为一致。
  • 表格在窄屏有挤压风险时必须提供明确的 scroll 或响应式列策略。

样式开发规范

前端基于 Ant Design 构建 UI。样式管理目标是让 antd 继续承担主视觉系统,项目 CSS 只补足页面外壳、局部布局和自有组件视觉,不另起一套与 antd 竞争的样式体系。

样式开发优先级:

  1. antd 组件默认能力,例如 ButtonCardTableFormResultEmpty
  2. antd 组件 props例如 sizetypevariantcolorstatuslayoutscrollgutter
  3. antd 布局组件,例如 LayoutFlexSpaceRowCol,避免为普通排列关系新增 CSS。
  4. ConfigProvider theme token 和 antd 组件 token处理主题级或组件级统一调整。
  5. antd CSS 变量(--ant-*),用于项目自有 CSS 中引用颜色、间距、字体、圆角和阴影等设计值。
  6. 全局 CSS仅承载应用外壳、全局基础样式和少量明确复用的工具类。
  7. CSS Modules用于页面专属布局或项目自有组件视觉仅在局部样式增长到需要就近维护时使用。
  8. 自行开发视觉组件,仅在 antd 组件和组合方式无法表达明确产品需求时使用。

红线:

  • 严禁在组件中使用 style 属性内联调整样式。
  • 严禁通过 CSS 覆盖 antd 组件内部类名,例如 .ant-*
  • 严禁使用 !important
  • 颜色统一使用 antd Design Token / CSS 变量,不使用硬编码色值。
  • 默认不引入 Tailwind、UnoCSS、Sass、Less、CSS-in-JS 或额外 PostCSS 插件;确需引入时必须先说明现有 antd + CSS Modules 无法满足的具体问题、影响范围和迁移成本。

默认状态原则:如果 antd 组件默认样式已经满足当前需求,不为其增加额外 CSS 类;不要通过外层 CSS 修改 Sider、Menu、Table、Modal 等组件内部结构样式。

全局 CSS 归属:

  • 当前入口保留 src/web/styles.css;当文件继续增长时,优先拆分为 src/web/styles/global.csssrc/web/styles/app-shell.csssrc/web/styles/utilities.css 等按职责命名的文件,再由入口样式文件集中导入。
  • global.css 仅放 htmlbody:root、字体渲染、全局背景等应用级基础样式。
  • app-shell.css 仅放应用外壳样式,例如 app-layoutapp-headerapp-content、Header 内容分布和主内容间距。
  • utilities.css 只放至少两处复用、语义稳定、不会与 antd props 重叠的工具类;只有一处使用时优先改为 antd 布局组件或局部 CSS Modules。
  • 全局类名必须带有明确前缀,应用外壳使用 app-*,工具类使用 u-*;禁止新增 .container.title.content 等容易跨页面冲突的泛名类。

CSS Modules 归属:

  • 页面专属样式与页面就近放置,例如 src/web/pages/projects/projects.module.css
  • 自有组件样式与组件就近放置,例如 src/web/components/FooCard/foo-card.module.css
  • CSS Modules 中类名使用职责语义,例如 .root.toolbar.summaryCard.emptyState;通过导入对象绑定到组件,避免字符串拼写散落。
  • 同一类样式只服务当前页面或组件;一旦被多处复用,应先判断能否用 antd 组件或 props 表达,再考虑提取共享组件,而不是直接提升为全局 CSS。
  • 首次实际使用 *.module.css 时,同步补全 TypeScript 声明和必要测试,确保类型检查与构建链路稳定。

antd 定制边界:

  • 优先使用官方 props、theme token 和组件 token不要因为视觉微调直接写 CSS。
  • antd v6 组件暴露 classNames 语义插槽时,可以把项目自有类绑定到官方稳定插槽;仍然不得选择 .ant-* 内部 DOM 类名。
  • 避免使用 antd styles 语义插槽写内联样式;如果必须使用,应先评估是否可以通过 token、CSS 变量或 CSS Modules 表达。
  • 弹窗、下拉、表格、菜单等复杂组件不依赖内部 DOM 结构做布局修补;发现必须修补时,优先调整组件组合或交互设计。

token 和 CSS 变量规则:

  • 颜色使用 var(--ant-color-*),例如文本、边框、背景和状态色。
  • 间距、字号、圆角和阴影优先使用 var(--ant-padding-*)var(--ant-margin-*)var(--ant-font-size-*)var(--ant-border-radius*)var(--ant-box-shadow*) 等 antd 变量。
  • 项目自定义 CSS 变量只能定义在 :root 或清晰的主题容器上,并且必须基于 antd token 派生;不要创建与 antd 平行的颜色、间距、字号体系。
  • 主题切换统一通过 ConfigProvider theme algorithm 和 token 控制,不在 CSS 中硬编码亮色或暗色分支。

响应式规则:

  • 页面必须在桌面和移动端正常加载和可读。
  • 优先使用 antd FlexGridTable scroll、响应式列配置处理布局收缩。
  • 媒体查询只处理页面或自有组件的布局断点;不要用媒体查询覆盖 antd 内部结构。
  • 移动端适配优先保证内容可访问、操作可点击和横向溢出可控,不追求与桌面完全一致的排版。

新增样式前检查:

  1. 这个需求是否可以由 antd 组件或 props 完成?
  2. 这个样式是否属于主题级统一调整,应该放到 ConfigProvider theme token
  3. 这个样式是否只服务页面外壳,应该留在全局 CSS
  4. 这个样式是否只服务单个页面或自有组件,应该使用 CSS Modules
  5. 这个样式是否在覆盖 antd 内部结构?如果是,应重新设计组件组合。
  6. 这个样式是否引入了硬编码色值、style.ant-*!important?如果是,不应合入。

表单与交互规范

  • Modal + Form 提交使用 Form onFinish 处理业务提交,Modal onOk 只触发 form.submit()
  • 不在 Modal onOk 中直接执行异步 validateFields 和提交逻辑,也不通过 lint disable 绕过该问题。
  • 文本必填字段同时配置 required: truewhitespace: true,保持前端校验与后端 trim 后校验一致。
  • 提交中状态传给 antd 组件的 loading/confirmLoading 等 props避免自行实现重复状态样式。
  • 操作确认优先使用 Popconfirm,成功/失败反馈优先使用 antd message。

运行时外壳规范

前端提供两个入口外壳,共享通用 Console Shell 组件:

  • Admin管理台src/web/consoles/admin/AdminConsoleLayout.tsx,菜单配置在 menu.tsx,路由 //projects/models
  • Workbench工作台src/web/consoles/workbench/WorkbenchProjectGate.tsxWorkbenchConsoleLayout.tsx,菜单配置和路由构造在 routes.ts,路由 /workbench/:projectId。默认菜单为"聊天室",使用 ChatPage 作为主页面。

通用 Console Shellsrc/web/components/ConsoleShell/ConsoleShell.tsx)包含 Layout、Header、Sider、Content、主题切换、版本展示和侧边栏折叠状态由 Admin 和 Workbench 复用。Header 显示品牌名、版本号和控制台标题Admin 显示"管理台"Workbench 显示"工作台 · 项目名")。

Sidebarsrc/web/components/Sidebar/index.tsx)是纯展示/导航组件,通过 menuItems props 接收菜单配置由调用方决定菜单内容和路径。Admin 传入静态路径 //projects/modelsWorkbench 通过 route builderbuildWorkbenchPath)将相对菜单路径拼成 /workbench/:projectId 的子路径。

Workbench 项目上下文通过 ProjectContext 提供,在 WorkbenchProjectGate 中从 URL path param 读取 projectId,通过 useProject(projectId) 加载项目,仅 active 项目渲染工作台布局,不存在或 archived 项目显示"项目不存在或不可访问"。

模型管理页面(src/web/pages/models/index.tsx)属于 Admin 路由 /models,通过 antd Tabs 在同页组织供应商和模型两个视图。页面使用 ModelsToolbarProviderTableProviderFormModalModelTableModelFormModal 拆分筛选、表格和表单职责;模型表单和模型表格必须使用 GET /api/providers/options 获取最小供应商选项,不能复用供应商标签页当前分页或搜索结果作为全量选项。

供应商表单必须支持未保存配置的连通性测试,新建供应商时 type 默认 openai-compatiblebaseURL 不设默认值。连通性测试返回 ok: false 时应展示失败反馈,不得使用成功提示样式;/models 不支持或响应格式不兼容属于可忽略提醒,不得阻止保存。

聊天页面

Workbench 聊天页面位于 src/web/consoles/workbench/pages/ChatPage.tsx,组合 ChatSidebarChatPanel 两个子组件。

  • ChatSidebar:使用 TanStack Query 管理会话列表,提供创建和删除会话操作。

  • ChatPanel:使用 Vercel AI SDK useChat hook来自 @ai-sdk/react)管理聊天状态,通过 DefaultChatTransport(来自 ai 包)与后端 SSE 端点通信。采用论坛式单列垂直布局,使用 antd Card 组件承载每条消息,通过 PartRendererpart.type 分派渲染(文本/工具调用/推理/步骤分隔。AI 文本使用 streamdown 流式 Markdown 渲染,工具调用使用 ToolCallCard 展示参数和执行结果。输入区使用 antd Input.TextArea + Button

  • ToolCallCard:工具调用状态组件,支持 input-streaming/input-available/output-available/output-error 四态。

  • MessageBubble:已删除,由 PartRenderer + Card 容器替代。

  • use-conversations hook:位于 src/web/hooks/use-conversations.ts,封装会话 CRUD 的 fetch 调用。

  • 生产入口必须启用 ErrorBoundary,运行时渲染异常使用 antd Result status="500" 或等价组件展示。

  • ReactQueryDevtools 仅在 import.meta.env.DEV 条件下渲染,不进入生产渲染路径。

  • 主题切换统一通过 ConfigProvider 的 antd theme algorithm 控制,不使用硬编码主题色。

TanStack Query 规范

  • Query key 使用 structured array使用 as const 保持字面量类型
  • 全局面板级查询可持续刷新,详情级查询必须按状态条件启用
  • 多处页面使用同一后端资源时,应提取共享 hook避免重复定义 fetch 函数。

fetch 封装

统一使用 fetch不引入 axios。错误抛异常由 TanStack Query 的 error 状态承接。

前后端共享的请求和响应类型定义在 src/shared/api.ts。前端 fetch 函数的返回类型必须匹配后端真实 JSON 形状;如果后端返回包装对象,例如 { project: Project },应声明对应响应类型并在 hook 内提取业务对象。

前端测试

  • 测试目录为 tests/web/,结构对应 src/web/
  • 单元测试重点覆盖 utils/ 和 hooks 中的纯逻辑
  • 组件测试使用 jsdom 和 @testing-library/react
  • 测试用户行为而非实现细节
  • 只 mock 系统边界,使用真实的 QueryClientProvider 包裹组件
  • 组件测试环境由 tests/setup.ts 和 bunfig.toml preload 提供
  • 断言优先基于用户可见文本、role、按钮和交互结果不依赖 .ant-* 内部类名。
  • 对 antd 组件只断言本项目传入的可观察行为或配置结果,避免把 antd 内部 DOM 结构当作稳定契约。
  • fetch mock、路由、QueryClientProvider 等系统边界优先复用 tests/web/test-utils.tsx避免在每个测试文件重复安装 window.fetch
  • 项目页这类数据驱动页面至少覆盖请求 URL/query、method/body、成功后的用户可见结果以及关键错误路径或失败后状态。
  • ErrorBoundary、hooks 纯逻辑和 fetch request helper 应使用单元测试覆盖异常回退,页面测试只保留真实用户路径。

更新触发条件

修改前端技术栈、组件边界、数据流、样式规则、测试环境或前端验证方式时,必须更新本文档。

日志模块

Logger 接口

src/web/utils/logger.ts 提供与后端镜像的 Logger 抽象:

export interface Logger {
  child(bindings: Record<string, unknown>): Logger;
  debug(message: string, data?: unknown): void;
  error(message: string, data?: unknown): void;
  info(message: string, data?: unknown): void;
  setLevel(level: LogLevel): void;
  warn(message: string, data?: unknown): void;
}

实现

实现 工厂函数 用途
DefaultLogger + Sinks useLogger() / createDefaultLogger() 组件内使用ConsoleSink + AntdMessageSink 双流
ConsoleLogger createConsoleLogger() 非组件纯函数ErrorBoundary、工具函数仅 ConsoleSink
NoopLogger createNoopLogger() 测试中不需要日志的场景
MemoryLogger createMemoryLogger() 测试断言日志条目

使用方式

组件内(推荐):

import { useLogger } from "../hooks/use-logger";

function MyComponent() {
  const logger = useLogger();
  logger.info("数据加载完成", { count: 42 });
  logger.warn("即将超时");
  logger.error("操作失败", { error: new Error("...") });
}

非组件纯函数:

import { createConsoleLogger } from "../utils/logger";

const logger = createConsoleLogger();
logger.debug("调试信息");

作用域绑定:

const pageLogger = logger.child({ page: "projects" });
pageLogger.info("页面加载"); // [Alfred:INFO] 页面加载 [page=projects]

notification 红线

  • AntdMessageSink 仅对 warnmessage.warning)和 errormessage.error)触发用户可见通知。
  • debuginfo 级别绝不对用户弹出 notification仅在开发者控制台输出。
  • 错误详情通过 data 参数传入(如 logger.error("提交失败", { error })data 不经序列化透传,保留 Error 堆栈展开能力。

生产环境行为

生产环境(import.meta.env["PROD"])自动将 ConsoleSink 最小级别设为 warn,屏蔽 debug/info 输出。useLogger()createConsoleLogger() 自动处理此逻辑,调用方无需关心环境判断。

ErrorBoundary 特殊说明

ErrorBoundary 是 class 组件,无法使用 useLogger() hook。它以 createConsoleLogger() 直接创建独立的 ConsoleLogger 实例,仅输出到控制台不触发用户通知。

测试

  • 单元测试使用 createMemoryLogger() 断言日志记录,使用 createNoopLogger() 静默无关日志。
  • createDefaultLogger(sinks, isProduction) 接受 isProduction 参数,测试中可显式控制级别过滤行为,不依赖 import.meta.env