2 changed files with 415 additions and 7 deletions
@ -0,0 +1,406 @@ |
|||||
|
# 前端 UI 开发准则 |
||||
|
|
||||
|
> 本准则适用于 `frontend/` 目录下的管理后台前端(Vue 3 + TypeScript + Vite + TDesign Vue Next)。 |
||||
|
> 面向**后续新增/改造页面**的统一依据。核心目标:**不重复造轮子** —— 能用 TDesign 组件就不手写原生标签,能用项目共享基建就不重复造状态逻辑;布局统一、符合 TDesign 风格。 |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## ① 总则与铁律 |
||||
|
|
||||
|
### 技术栈 |
||||
|
|
||||
|
| 类别 | 选型 | |
||||
|
| --- | --- | |
||||
|
| 框架 | Vue 3.5(`<script setup lang="ts">`) | |
||||
|
| 语言 | TypeScript | |
||||
|
| 构建 | Vite | |
||||
|
| UI 组件库 | `tdesign-vue-next`(`package.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`,仅用于图表/流程图/分类多色) |
||||
|
|
||||
|
```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>`): |
||||
|
|
||||
|
```vue |
||||
|
<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` 相对标准)。 |
||||
|
|
||||
|
```vue |
||||
|
<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">` | |
||||
|
| 删除确认 | 统一 `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` 禁用按钮 | |
||||
|
| 确认操作 | `useConfirm`(`DialogPlugin.confirm` 封装) | 原生 `window.confirm` | |
||||
|
| 提示 | `@/utils/toast`(`MessagePlugin` 封装) | 直接 `MessagePlugin` / 自写浮层 | |
||||
|
| 拖拽排序 | `t-table` 的 `drag-sort`,或抽共享 composable | `ModelConfigManager` 手写 HTML5 `draggable` + `@dragstart/@drop` | |
||||
|
|
||||
|
### 图标规范(统一 `tdesign-icons-vue-next`) |
||||
|
|
||||
|
- ✅ 功能图标(按钮、菜单、状态)从 `tdesign-icons-vue-next` **具名导入**: |
||||
|
|
||||
|
```ts |
||||
|
import { AddIcon, SearchIcon, DeleteIcon, RefreshIcon, SettingIcon } from 'tdesign-icons-vue-next' |
||||
|
``` |
||||
|
|
||||
|
```vue |
||||
|
<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.data` 是 `Ref<T[]>`),故模板中需显式写 `.value`(`pagination.data.value`)。若后续优化,可让 composable 用 `reactive`/`toRefs` 返回以便模板免 `.value`,但当前以既有实现为准。 |
||||
|
|
||||
|
### 组件(`src/components/`) |
||||
|
|
||||
|
| 组件 | 路径 | 用途 | |
||||
|
| --- | --- | --- | |
||||
|
| `FormDialog` | `@/components/FormDialog.vue` | 新增/编辑弹窗壳(封装 `t-dialog` + 页脚取消/保存按钮),表单内容走默认 slot,`extra-buttons` slot 放额外按钮 | |
||||
|
| `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.ts`、`category.ts`、`document.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"` 防重复提交。 |
||||
|
- 必填/格式校验规则统一以 TDesign Form 校验规则为准(`required`、`trigger: 'blur'` 等)。 |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## ⑧ 交互与反馈规范 |
||||
|
|
||||
|
- **搜索触发**(统一裁定): |
||||
|
- 关键词输入:`@enter` 触发(或 `useDebounce` 实时触发),**二选一并全站一致**;不裸用 `@change` 做全量重载。 |
||||
|
- 下拉筛选:`@change` 应自动重载列表(重置到第 1 页)。 |
||||
|
- **删除/危险操作**:统一 `useConfirm().confirm(title, body?)`,body 说明后果。 |
||||
|
- **错误处理**(沿用项目规则):`catch` 必须透传服务器错误信息,禁止吞掉: |
||||
|
|
||||
|
```ts |
||||
|
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/basic`、`fetch` 到 `/upload/string` 并直接读 token,绕过了 `@/api` 与 `@/utils/token` 封装。 |
||||
|
- 新增接口:在对应 `src/api/<module>.ts` 增加函数,组件 import 使用。 |
||||
|
- 响应结构统一 `{ success, code?, message?, data }`(`src/types/api.ts`)。 |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## ⑩ 图表 / 流程图规范 |
||||
|
|
||||
|
- 图表(Chart.js)、流程图(mermaid)颜色**唯一来源**是 `@/utils/palette`(`palette` 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-tree`:`CategoryManager.vue`。 |
||||
|
3. 手写复选框树替代 `t-tree checkable`/`t-cascader`:`RoleManager.vue`。 |
||||
|
4. 手写下拉替代 `t-select`:`ModelConfigManager.vue`(模型下拉)。 |
||||
|
5. 手写通知浮层替代 `MessagePlugin`/`NotificationPlugin`:`McpServerManager.vue`、`ModelConfigManager.vue`。 |
||||
|
6. 主从分栏布局重复两套 scoped CSS:`RoleManager.vue` vs `SystemConfigManager.vue`。 |
||||
|
7. 手写标签输入 / XHR / fetch / 直读 token:`DocUpload.vue`。 |
||||
|
|
||||
|
### 一致性偏差 |
||||
|
|
||||
|
8. 页头 3 种写法并存:`title` 属性(多数) vs `#title`+`<h2>`(`ModelConfigManager`) vs `#title`+图标(`SystemConfigManager`)。 |
||||
|
9. 表格缺 `:loading`:`UserManager` / `AccountManager` / `McpServerManager` / `ConversationManager`。 |
||||
|
10. 分页缺失/不统一:`AccountManager` 无分页;pageSize 10/20/30 三档。 |
||||
|
11. 表单校验分裂:全无 `:rules`,手写 `if` 与 `:disabled` 两种土办法并存。 |
||||
|
12. 图标体系分裂:emoji 主导,仅 `SystemConfigManager` 用 tdesign-icons;`ModelConfigManager` 操作列纯 emoji 按钮。 |
||||
|
13. CSS 变量双体系混用:`--color-*` 与 `--td-*` 交叉,甚至同文件混用。 |
||||
|
14. 搜索触发不统一:`@enter` vs `@change`+debounce vs 显式搜索按钮。 |
||||
|
15. 错误处理吞异常:多处 `console.error` / 空 `catch{}` / `/* 静默失败 */`。 |
||||
|
16. 死 import:`ModelConfigManager.vue` import 了未使用的 `MessagePlugin`。 |
||||
|
17. 操作列 `t-space` 间距 `:size="2"`(`UserManager`)与 `:size="4"`(其余)不一致;`v-show`/`v-if`/动态 theme 三种显隐写法并存。 |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue