本地 RAG 知识库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

21 KiB

前端 UI 开发准则

本准则适用于 frontend/ 目录下的管理后台前端(Vue 3 + TypeScript + Vite + TDesign Vue Next)。 面向后续新增/改造页面的统一依据。核心目标:不重复造轮子 —— 能用 TDesign 组件就不手写原生标签,能用项目共享基建就不重复造状态逻辑;布局统一、符合 TDesign 风格。


① 总则与铁律

技术栈

类别 选型
框架 Vue 3.5(<script setup lang="ts">
语言 TypeScript
构建 Vite
UI 组件库 tdesign-vue-nextpackage.json 声明 ^1.10.6,当前锁定 1.20.5
图标库 tdesign-icons-vue-next
状态管理 Pinia(src/stores/
路由 Vue Router 4(hash 模式 #/path

组件按需自动引入

项目通过 unplugin-vue-components + TDesignResolver(见 vite.config.ts)自动按需引入 TDesign 组件,因此:

  • 模板中直接写 <t-button> / <t-table> / <t-dialog> 等标签,无需手动 import
  • 组件 props/事件用法以 TDesign 官方文档为准。
  • ⚠️ 图标例外:图标需从 tdesign-icons-vue-next 手动具名导入(见 §⑤ 图标规范)。

三条铁律

  1. 组件优先:能用 TDesign 组件实现的交互/展示,禁止用原生标签 + 手写 CSS 复刻(树、下拉、分页、开关、标签、空态、加载、通知等,见 §⑤ 对照表)。
  2. 基建优先:分页/加载/表单/确认等状态逻辑,强制复用 src/composables/src/components/ 的共享能力(见 §⑥),禁止每页手写一套 page/total/loading/visible ref。
  3. Token 优先:颜色一律走设计 token(--color-* 语义色 / palette.ts 系列色),禁止硬编码 hex(见 §②)。

② 设计 Token 与配色

颜色来源只有两个入口

场景 使用 定义位置
语义色(主色/成功/警告/错误)、文本、边框、背景、圆角、阴影 var(--color-*) src/styles/tokens.css
多色区分系列色(图表 / mermaid 流程图 / 分类标签) palette / paletteBg / paletteBorder src/utils/palette.ts

语义 token(--color-*,已映射到 TDesign --td-*

主色     --color-primary / --color-primary-hover / -active / -light
状态色   --color-success / --color-warning / --color-error
状态阶   --color-{success|error|warning|info}-{bg|border|text|text-strong}
文本     --color-text-primary / -secondary / -tertiary / -disabled / -link
边框     --color-border / --color-border-strong
背景     --color-bg-page / --color-bg-subtle / --color-bg-code
圆角     --radius-sm / --radius-md / --radius-lg / --radius-card
阴影     --shadow-sm / --shadow-md / --shadow-lg

系列色(palette.ts,仅用于图表/流程图/分类多色)

import { palette, paletteBg, paletteBorder } from '@/utils/palette'
// palette.blue / .purple / .green / .red / .orange / .indigo / .pink / .gray
// palette.blueRgb 用于 rgba(..., 0.1) 填充背景

强制规则

  • 禁止在组件 <style> 里写死 hex 颜色(#3b82f6#fff 等);唯一例外是 tokens.css 内部的代码块深色 --color-code-block-bg/text
  • 禁止业务样式直接混用 --td-*(现状 RoleManager/SystemConfigManager/LoginPage 等混用两套变量)。业务自定义样式统一 --color-*
  • 布局壳 layouts/(MainLayout/Sidebar/Topbar)已用 --td-*,属可接受的历史例外,不强制回改,但新增业务样式一律 --color-*
  • 优先用组件自带 theme / variant / color 属性表达状态(如 <t-tag theme="success"><t-statistic color>),不要用 CSS 覆盖组件色。

复用 shared.css 工具类

src/styles/shared.css 已提供,禁止各页重复定义:

类名 用途
.toolbar 工具栏(flex + gap:8px + 换行)
.dialog-footer 弹窗页脚按钮行(右对齐)
.card-box 圆角边框卡片容器
.desc-text 页头下的描述性文字
.code-tag 代码/键名展示
.total-hint 统计提示文字

③ 页面布局规范

应用壳(无需业务页关心)

src/layouts/MainLayout.vue 已提供「Topbar + Sidebar + 主内容区(<router-view>)」的固定视口壳,主内容区是唯一主滚动容器main.main-content)。业务页面只实现自己那一块内容,禁止自建全屏容器 / 自己的滚动 body。

业务页外壳

统一用 <t-card> 作为最外层,页头统一走 title 属性(不手写 #title 插槽 + <h2>):

<template>
  <t-card :bordered="false" title="用户管理">
    <!-- 页面内容 -->
  </t-card>
</template>
  • 页头下的补充说明统一用 <p class="desc-text">…</p>shared.css)。
  • 栅格布局统一 <t-row :gutter="16"> + <t-col :span="n">(24 栅格)。
  • 主从分栏(左列表右详情)应复用统一的布局组件,不要像 RoleManager/SystemConfigManager 那样各自复制一套约 250 行的 scoped CSS。

④ 标准「列表管理页」模板

以下为 CRUD 列表页的统一骨架,逐块遵循。完整示例见 src/views/ 现有页面(SensitiveWordManager 相对标准)。

<template>
  <t-card :bordered="false" title="用户管理">
    <p class="desc-text">管理后台用户账号与角色</p>

    <!--  工具栏 -->
    <div class="toolbar">
      <t-input v-model="keyword" placeholder="搜索关键词" clearable
               style="width: 220px" @enter="onSearch" @clear="onSearch" />
      <t-select v-model="statusFilter" :options="statusOptions" clearable
                placeholder="状态" style="width: 140px" @change="onFilterChange" />
      <div style="flex: 1" />
      <t-button theme="primary" @click="form.openCreate()">
        <template #icon><AddIcon /></template>新建
      </t-button>
    </div>

    <!-- ② 表格 -->
    <t-table
      :data="pagination.data.value"
      :columns="columns"
      :loading="pagination.loading.value"
      row-key="id"
      :pagination="{
        current: pagination.page.value,
        total: pagination.total.value,
        pageSize: pagination.pageSize,
        showJumper: true,
      }"
      @page-change="pagination.onPageChange"
    >
      <template #op="{ row }">
        <t-space :size="4">
          <t-button size="small" variant="text" theme="primary" @click="form.openEdit(row)">编辑</t-button>
          <t-button size="small" variant="text" theme="danger" @click="onDelete(row)">删除</t-button>
        </t-space>
      </template>
    </t-table>
  </t-card>

  <!-- ③ 弹窗表单 -->
  <FormDialog
    v-model:visible="form.visible.value"
    :title="form.isEdit.value ? '编辑用户' : '新建用户'"
    :saving="form.saving.value"
    @confirm="onSave"
  >
    <t-form ref="formRef" :data="form.form.value" :rules="rules" label-align="top">
      <t-form-item label="用户名" name="username">
        <t-input v-model="form.form.value.username" placeholder="请输入用户名" />
      </t-form-item>
      <!-- 更多字段,多字段用 <t-row :gutter="16"><t-col :span="6"> -->
    </t-form>
  </FormDialog>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { AddIcon } from 'tdesign-icons-vue-next'
import { usePagination } from '@/composables/usePagination'
import { useCrudForm } from '@/composables/useCrudForm'
import { useConfirm } from '@/composables/useConfirm'
import { toast } from '@/utils/toast'
import { formatDate } from '@/utils/format'
import FormDialog from '@/components/FormDialog.vue'
import { getUserPage, createUser, updateUser, deleteUser } from '@/api/user'

// 分页 + 列表
const pagination = usePagination({ fetchFn: getUserPage })
const keyword = ref('')
const statusFilter = ref('')
const statusOptions = [{ label: '启用', value: 'enabled' }, { label: '禁用', value: 'disabled' }]
function onSearch() { pagination.reset(); pagination.load() }
function onFilterChange() { pagination.reset(); pagination.load() }

// 表单(创建/编辑)
const formRef = ref()
const form = useCrudForm({
  defaultForm: { username: '', nickname: '' },
  createFn: createUser,
  updateFn: updateUser,
  onSuccess: () => pagination.load(),
})
const rules = { username: [{ required: true, message: '用户名必填', trigger: 'blur' }] }
async function onSave() {
  const result = await formRef.value?.validate()
  if (result !== true) return // 校验失败(result 为 ValidateResultObj),不提交
  const ok = await form.save()
  if (!ok) toast('保存失败', 'error')
}

// 删除
const { confirm } = useConfirm()
async function onDelete(row: any) {
  if (!(await confirm(`确定删除「${row.username}」?`, '删除后不可恢复'))) return
  const r = await deleteUser(row.id)
  r.success ? toast('删除成功', 'success') : toast(r.message || '删除失败', 'error')
  pagination.load()
}

// 列定义(写在 script 中,传 :columns,不用 <t-table-column> 标签)
const columns = [
  { colKey: 'username', title: '用户名' },
  { colKey: 'nickname', title: '昵称' },
  { colKey: 'createTime', title: '创建时间', cell: (_, { row }) => formatDate(row.createTime) },
  { colKey: 'op', title: '操作', width: 140 },
]
</script>

模板要点裁决

规范
表格列定义 统一写在 <script>columns 数组 + :columns禁用 <t-table-column> 标签写法
分页 统一用 <t-table> 内置 :pagination + @page-change + showJumper禁用独立 <t-pagination> 组件
表格 loading 必须绑定 :loading(现状有 4 个页面缺失)
pageSize 默认统一 20(现状 10/20/30 三档不一)
操作列 <t-space :size="4"> + <t-button size="small" variant="text">;危险操作 theme="danger"、主操作 theme="primary"
状态列 <t-tag variant="light" theme="..."> 表达,不用带色 <span>
开关列 <t-switch v-model="row.xxx" @change="...">
弹窗 统一 FormDialog 组件 + .dialog-footer<t-form label-align="top">;富文本/宽内容表单改用 FormDrawert-drawer
删除确认 统一 useConfirm().confirm()(已 100% 采用)
提示 统一 @/utils/toast禁止直接 import { MessagePlugin }

⑤ 组件对照表(「不重复造轮子」核心)

现状中大量「手写原生标签复刻组件」的做法必须废止,改回 TDesign 组件:

场景 应使用 禁止的手写做法(现状反例)
树形结构 t-tree(可 checkable / expand-on-click-node CategoryManager 手写 div.tree-row + 内联 paddingLeft + expandedIds Set
级联/勾选选择 t-tree checkable 或 t-cascader / t-tree-select RoleManager 手写嵌套 checkbox + 全选/半选 computed
下拉选择 t-select / t-auto-complete ModelConfigManager 手写 div 列表 + 键盘导航 + document 点击关闭
多标签输入 t-tag-input DocUpload 手写 t-tag closable + t-input borderless 拼装
键值/描述展示 t-descriptions DocDetail 手写 .info-grid + .info-item
列表展示 t-list(或 t-table DashboardPanel/DocDetail/LogViewer 手写 div 列表
统计指标卡 t-card + t-statistic ModelConfigManager 手写内联 div(🟢在线/🔴离线)
全局通知/结果浮层 MessagePlugin / NotificationPlugin McpServerManager/ModelConfigManager 手写 position:fixed div + setTimeout
页签切换 t-tabs / t-tab-panel ChatPanel 手写 .assistant-card + click 切 active
导航/文件列表 t-menu LogViewer/ChatPanel 手写 .file-item/.assistant-card
加载态 t-loading / v-loading / 表格 :loading PipelineFlow 手写 .pipeline-loading
空态 t-empty(可配 description / image) 各页自定义空 div
错误态 t-alert theme="error" + t-empty(tdesign-vue-next t-result PipelineFlow 手写 .pipeline-error
表单校验 t-form:rules(见 §⑦) 手写 if(!x){toast(...);return}:disabled 禁用按钮
确认操作 useConfirmDialogPlugin.confirm 封装) 原生 window.confirm
提示 @/utils/toastMessagePlugin 封装) 直接 MessagePlugin / 自写浮层
拖拽排序 t-tabledrag-sort,或抽共享 composable ModelConfigManager 手写 HTML5 draggable + @dragstart/@drop

图标规范(统一 tdesign-icons-vue-next

  • 功能图标(按钮、菜单、状态)从 tdesign-icons-vue-next 具名导入
import { AddIcon, SearchIcon, DeleteIcon, RefreshIcon, SettingIcon } from 'tdesign-icons-vue-next'
<t-button theme="primary"><template #icon><AddIcon /></template>新建</t-button>
<!-- 或直接用 t-icon -->
<t-icon name="add" />
  • 禁止用 emoji 作功能图标/菜单图标/操作按钮图标(现状 ModelConfigManager 操作列纯 emoji 按钮、各页标题 👥/📚 前缀、navigation.ts 菜单 icon 字段用 emoji)。
  • 存量 emoji 标题/菜单图标,新增代码不再使用;菜单 icon 字段迁移方案:改用 tdesign-icons 组件名,Sidebar.vue 渲染 <t-icon :name="item.icon" />

⑥ 共享基建使用规范

以下能力已存在于 src/新增页面必须复用,禁止手写等价的 state 逻辑

Composables(src/composables/

能力 路径 用法 / 返回
分页 + 列表加载 @/composables/usePagination usePagination({ fetchFn }){ page, total, pageSize, loading, data, load, onPageChange, reset }
表单状态(创建/编辑) @/composables/useCrudForm useCrudForm({ defaultForm, createFn, updateFn, onSuccess }){ visible, editingId, saving, form, isEdit, openCreate, openEdit, close, save }
loading/saving @/composables/useLoading { loading, saving, withLoading, withSaving }
删除确认 @/composables/useConfirm const { confirm } = useConfirm(); await confirm(title, body?)
防抖 @/composables/useDebounce const { debounce, cancel } = useDebounce()

⚠️ 现状 usePagination / useLoading / useCrudForm / FormDialog 均为 0 使用,各页手写了一套等价逻辑。这是最需要纠正的「重复造轮子」。

说明:上述 composable 返回的是普通对象内嵌 ref(如 pagination.dataRef<T[]>),故模板中需显式写 .valuepagination.data.value)。若后续优化,可让 composable 用 reactive/toRefs 返回以便模板免 .value,但当前以既有实现为准。

组件(src/components/

组件 路径 用途
FormDialog @/components/FormDialog.vue 新增/编辑弹窗壳(封装 t-dialog + 页脚取消/保存按钮),表单内容走默认 slot,extra-buttons slot 放额外按钮
FormDrawer @/components/FormDrawer.vue 新增/编辑抽屉表单壳(封装 t-drawer + 页脚取消/保存按钮),用于富文本/宽内容表单,接口同 FormDialog
MessageSources @/components/MessageSources.vue AI 引用来源展示

工具(src/utils/

工具 路径 用途
toast(msg, type?) @/utils/toast 全站唯一提示入口(MessagePlugin 封装),type: info|success|error|warning
formatDate(d) / formatBytes(b) @/utils/format 日期 YYYY-MM-DD HH:mm:ss、文件大小
renderMarkdown(text) @/utils/markdown marked + DOMPurify 消毒,唯一 Markdown 渲染入口
palette / paletteBg / paletteBorder @/utils/palette 图表/流程图系列色
readSSEStream* @/utils/sse SSE 流式统一处理

状态(src/stores/

  • 跨组件共享状态用 Pinia;navigation.ts(菜单配置 MENU_ITEMS)、dialog.ts(全局文档详情弹窗)、auth.tscategory.tsdocument.ts 已有。
  • 页面局部状态用 <script setup>ref/reactive,不要为单页数据建 store。

⑦ 表单与校验规范

  • 表单统一 <t-form :data="..." :rules="..." label-align="top"> + <t-form-item name="...">用声明式 :rules 做校验
  • 废止两种土办法:① 手写 if(!x){toast('...','error');return};② 用 :disabled 禁用保存按钮代替校验。
  • 提交前调用 formRef.value?.validate() 触发校验(见 §④ 模板)。
  • 多字段布局统一 <t-row :gutter="16"><t-col :span="6">span=6 为四列)。
  • 弹窗统一 <t-dialog :footer="false"> + 页脚 .dialog-footer 放「取消 / 保存」,保存按钮 :loading="saving" 防重复提交。
  • 弹窗 vs 抽屉的取舍:简单表单用 t-dialogFormDialog 组件);富文本/宽内容表单(Markdown 编辑器、宽预览、长文本、宽表格编辑)统一用 t-drawerFormDrawer 组件,size 可设 'large'/'1000px'/'85%'),避免窄弹窗挤压双栏编辑器。
  • 必填/格式校验规则统一以 TDesign Form 校验规则为准(requiredtrigger: 'blur' 等)。

⑧ 交互与反馈规范

  • 搜索触发(统一裁定):
    • 关键词输入:@enter 触发(或 useDebounce 实时触发),二选一并全站一致;不裸用 @change 做全量重载。
    • 下拉筛选:@change 应自动重载列表(重置到第 1 页)。
  • 删除/危险操作:统一 useConfirm().confirm(title, body?),body 说明后果。
  • 错误处理(沿用项目规则):catch 必须透传服务器错误信息,禁止吞掉:
try {
  const r = await doSomething()
  r.success ? toast(r.message || '操作成功', 'success') : toast(r.message || '操作失败', 'error')
} catch (e: any) {
  toast(e.message || '操作失败', 'error')   // ✅ 透传
}
// ❌ 禁止:catch (e) { console.error(e) } / catch {} / /* 静默失败 */
  • 成功/失败文案统一 r.message || '默认文案';标点用半角 :(不混用全角 )。
  • 提交类操作按钮必须 :loading,防重复提交。

⑨ API 层规范

  • 所有后端调用统一走 src/api/*.ts 模块(axios 实例 + 拦截器见 src/api/request.ts),组件内禁止手写 fetch / XMLHttpRequest、硬编码 URL、直接读 localStorage.getItem('sb_token')
  • 现状反例:DocUpload 手写 XHR 到 /upload/json/basicfetch/upload/string 并直接读 token,绕过了 @/api@/utils/token 封装。
  • 新增接口:在对应 src/api/<module>.ts 增加函数,组件 import 使用。
  • 响应结构统一 { success, code?, message?, data }src/types/api.ts)。

⑩ 图表 / 流程图规范

  • 图表(Chart.js)、流程图(mermaid)颜色唯一来源@/utils/palettepalette hex/rgb + paletteBg + paletteBorder),禁止散落 hex。
  • 新增图表/流程图建议复用统一的初始化封装(mermaid.initialize 的 themeVariables、Chart.js 的 register/render 目前散落在 DashboardPanel/PipelineFlow 内联,重复可抽成 src/utils/chart.ts / src/utils/mermaid.ts)。
  • Markdown 渲染统一 renderMarkdown(已消毒),v-html 只允许用于已消毒内容(AI 回复、mermaid 生成的 svg、先转义后高亮的日志)。

附录:存量页面已知偏差清单

截至 2026-08-15 调研时点,以下为现状与准则不符之处,供后续增量整改(行号随代码演进会漂移,仅作定位参考)。本准则落地不要求一次性整改这些存量页,但新增代码不得再犯

高优先级(「重复造轮子」重灾区)

  1. 共享 composables / FormDialog 全部 0 使用(usePagination / useLoading / useCrudForm / FormDialog.vue),各 CRUD 页手写分页/loading/弹窗。
  2. 手写树替代 t-treeCategoryManager.vue
  3. 手写复选框树替代 t-tree checkable/t-cascaderRoleManager.vue
  4. 手写下拉替代 t-selectModelConfigManager.vue(模型下拉)。
  5. 手写通知浮层替代 MessagePlugin/NotificationPluginMcpServerManager.vueModelConfigManager.vue
  6. 主从分栏布局重复两套 scoped CSS:RoleManager.vue vs SystemConfigManager.vue
  7. 手写标签输入 / XHR / fetch / 直读 token:DocUpload.vue

一致性偏差

  1. 页头 3 种写法并存:title 属性(多数) vs #title+<h2>ModelConfigManager) vs #title+图标(SystemConfigManager)。
  2. 表格缺 :loadingUserManager / AccountManager / McpServerManager / ConversationManager
  3. 分页缺失/不统一:AccountManager 无分页;pageSize 10/20/30 三档。
  4. 表单校验分裂:全无 :rules,手写 if:disabled 两种土办法并存。
  5. 图标体系分裂:emoji 主导,仅 SystemConfigManager 用 tdesign-icons;ModelConfigManager 操作列纯 emoji 按钮。
  6. CSS 变量双体系混用:--color-*--td-* 交叉,甚至同文件混用。
  7. 搜索触发不统一:@enter vs @change+debounce vs 显式搜索按钮。
  8. 错误处理吞异常:多处 console.error / 空 catch{} / /* 静默失败 */
  9. 死 import:ModelConfigManager.vue import 了未使用的 MessagePlugin
  10. 操作列 t-space 间距 :size="2"UserManager)与 :size="4"(其余)不一致;v-show/v-if/动态 theme 三种显隐写法并存。