Files
zkxw/openspec/changes/zkxw-engineering-standards/specs/ui-conventions/spec.md
T
2026-07-22 09:25:27 +08:00

1029 lines
60 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.
## ADDED Requirements
### Requirement: Web 管理端以 Fresh Mint 规范为唯一视觉权威
`web-admin` 工程的界面实现 SHALL 以 `docs/web-admin-ui-spec-fresh-mint.md` 及其中引用的 `docs/fresh-mint-green.html` 为权威来源;主色链 SHALL 使用文档定义的 Mint 绿系(如 `--primary` `#10b981` 等),不得擅自替换为小程序品牌色。
#### Scenario: 新增列表页
- **WHEN** 在 `web-admin` 新增标准列表/表单页
- **THEN** 页面结构 SHALL 对齐规范中的布局顺序(筛选 → 工具栏 → 列表 → 分页等)及模板页(如 `#/demo/unit`、`#/demo/order`)所体现的模式
### Requirement: 小程序以三墙 Demo 体系为唯一视觉权威
`miniprogram` 工程的界面 SHALL 以 `docs/miniprogram-ui-spec-sanqiang.md` 及其中列出的 `mobile-ui/sanqiang-*.html` 为权威来源;品牌绿、大麦黄与中性色 SHALL 采用规范 §2 Token,不得直接复用 Web 端 Fresh Mint 色板作为主色。
#### Scenario: 新增小程序页面
- **WHEN** 新增客户端/供应商/配送端页面
- **THEN** 页面 SHALL 遵循规范中的统一骨架(Header 渐变、卡片圆角节奏、底部固定区等)及状态标签语义色
### Requirement: 禁止跨端调色板混用
除非产品书面要求,Web 管理端 SHALL NOT 使用小程序规范中的 `#0a9f6d` 等作为主色;小程序 SHALL NOT 使用 `#10b981` 作为主色替代品牌绿。
#### Scenario: Code review
- **WHEN** PR 修改全局主题或主按钮色
- **THEN** 审查者 SHALL 对照对应端 UI 规范文档中的 Token 表拒绝跨端拷贝色值
### Requirement: 树形主数据展示规则
涉及树形主数据(分类、库位、组织等)时,Web 端 SHALL 遵循:下拉/树节点展示**本级名称**,选中后在输入框、表格、导出与查看弹窗中展示**全路径**;路径分隔符 SHALL 与工程约定一致(当前为 ` / `,见 `web-admin/src/utils/tree-path.ts` 与规范 §5.6)。
#### Scenario: 树选择与表格列
- **WHEN** 用户在树形选择器中选择末级节点
- **THEN** 表格与只读视图 SHALL 展示完整祖先路径而非仅末级名称
### Requirement: 弹窗与遮罩
Web 管理端弹窗 SHALL 遵循规范:表单 `label-position="top"`;遮罩 SHALL NOT 使用背景模糊(`backdrop-filter: none` 等与仓库既有策略一致)。
#### Scenario: 新增编辑弹窗
- **WHEN** 使用 Element Plus `el-dialog` 承载表单
- **THEN** 表单标签位置与遮罩样式 SHALL 符合 `docs/web-admin-ui-spec-fresh-mint.md` 中 `el-dialog` 小节
### Requirement: 小程序媒体与触控
小程序列表缩略图、轮播与详情图 SHALL 遵循 `docs/miniprogram-ui-spec-sanqiang.md` §7.0 的宽高比与 `mode` 约定;可点击控件 SHALL 满足最小触控区域(规范中的 44×44 等要求)。
#### Scenario: 商品列表缩略图
- **WHEN** 展示商品列表缩略图
- **THEN** 图片区域 SHALL 为 1:1 且使用 `aspectFill`(与规范 §7.0.1 一致)
### Requirement: 页面不展示顶部 page-head 大标题与描述
`web-admin` 所有左侧菜单对应的页面 **SHALL NOT** 包含顶部 `<header class="page-head">`(或 `<div class="page-head">`)区块,即不在页面顶部展示 `<h1>` 大标题和 `<p class="page-desc">` 描述文字。页面内容 SHALL 直接从 `<section class="panel">`(或 `<div class="panel">`)开始。
**理由**:左侧菜单已提供当前页面名称,顶部大标题和描述属于冗余信息,移除后页面更简洁、内容区更大。
#### Scenario: 新增菜单页面无 page-head
- **WHEN** 新增一个左侧菜单对应的页面
- **THEN** 该页面 SHALL NOT 包含 `<header class="page-head">` 或 `<div class="page-head">` 元素
- **THEN** 页面模板 SHALL 直接以 `<section class="panel">` 或 `<div class="panel">` 作为第一个子内容块
#### Scenario: Code review 检查 page-head
- **WHEN** PR 包含新增或修改的页面文件(`src/views/**/*.vue`)
- **THEN** 审查者 SHALL 确认该页面不包含 `class="page-head"` 元素
### Requirement: 列表面板主标题装饰
`web-admin` 中列表面板 `panel-top` 下的主标题(`h2.title-bar-heading`)SHALL 在标题文案前展示 **主色竖条**(与全局 `app.scss` 中 `h2.title-bar-heading::before` 一致);含「租户」「用户」等易歧义文案的标题 SHALL 通过 `App.vue` 中 `panelTitleBarTextRules` 显式纳入竖条样式,避免子串误匹配为其它图标。
**重要**:`App.vue` 中 `panelTitleBarTextRules` 是标题竖条样式的唯一触发入口。新增任何包含 `panel-top > h2` 标题的页面时,开发者 SHALL 将该标题文案追加到 `panelTitleBarTextRules` 正则表达式的 `|` 分隔列表中。遗漏将导致标题无绿色竖条而显示为无样式或错误图标。
#### Scenario: 租户列表标题
- **WHEN** 打开 `#/system/tenant` 租户管理页
- **THEN** 「租户列表」标题 SHALL 显示绿色竖条装饰,且 SHALL NOT 因「租户」含「用户」子串而显示用户类图标
#### Scenario: 新增页面标题竖条
- **WHEN** 新增包含 `panel-top > h2` 标题的列表页(如标题为"XXX列表")
- **THEN** 开发者 SHALL 在 `App.vue` 的 `panelTitleBarTextRules` 正则中追加该标题文案(用 `|` 分隔)
- **THEN** 标题 SHALL 显示绿色竖条装饰
#### Scenario: Code review 新页面标题
- **WHEN** PR 包含新增页面的 `panel-top > h2` 标题
- **THEN** 审查者 SHALL 确认 `App.vue` 的 `panelTitleBarTextRules` 已同步更新
### Requirement: 弹窗与面板内子区域标题装饰
`web-admin` 中弹窗(`el-dialog`)和面板内的子区域标题(如「选择部门」「已选用户」「基本信息」「参数配置」等区块标题)SHALL 在文字左侧展示主色竖条装饰(`border-left: 3px solid #10b981`,`padding-left: 10px`),与列表面板主标题的竖条风格保持一致。子区域标题与字段标签(`el-form-item` label 或 `dt`)SHALL 形成明显视觉区分:子区域标题使用深色文字 + 竖条,字段标签使用辅助灰色、无竖条。
此规则适用于以下场景的区块标题:
1. **弹窗内分组标题**:如编辑弹窗中的「基本信息」「参数配置」等将表单字段分组的标题。
2. **多栏选择器的列标题**:如「选择部门」「选择用户」等 `data-scope-col-title` 类标题。
3. **已选摘要的分组标题**:如「已选部门」「已选用户」等 `data-scope-summary-heading` 类标题。
4. **查看弹窗的区块标题**:`SystemViewDialog` 的 section title(`h3`)。
5. **设计器/工具面板的分组标题**:如「节点类型」「属性」等 `panel-section-title` 类标题。
#### Scenario: 新增弹窗包含子区域分组
- **WHEN** 新增编辑弹窗内使用 `section-title`、`col-title`、`summary-heading` 等类名作为区块标题
- **THEN** 标题 SHALL 使用 `border-left: 3px solid #10b981` + `padding-left: 10px` 展示主色竖条
#### Scenario: Code review 弹窗子区域标题
- **WHEN** PR 包含弹窗内新增或修改的区块标题样式
- **THEN** 审查者 SHALL 确认标题包含主色竖条装饰,且与字段标签有明确视觉区分
### Requirement: 新增与编辑弹窗布局与视口留白
使用 Element Plus `el-dialog` 承载新增/编辑表单时,系统 SHALL 满足:
1. **水平栅格**:默认使用 **两列** 栅格(`.form-grid-2`,`grid-template-columns: repeat(2, minmax(0, 1fr))`,`gap: 12px 20px`);字段较多时 SHALL 通过加宽 `width` 并选用 `.form-grid-3`、`.form-grid-4` 或 `.form-grid-5` 扩展列数,**列数上限为 5**;跨整行长字段 SHALL 使用 `.form-col-span-2`(`grid-column: 1 / -1`)。
2. **窄屏回退**:视口或表单区过窄时,栅格 SHALL 回退为更少列或单列(与全局 `app.scss` 中 `@media (max-width: 900px)`、`(max-width: 560px)` 规则一致)。
3. **定位**:弹窗 SHALL 相对视口水平与垂直居中(依赖 `ElConfigProvider` 的 `dialog.alignCenter` 及 Element Plus 默认遮罩层居中行为)。
4. **高度与滚动**:使用 `.dialog-form-layout` 类名的弹窗壳层 SHALL `max-height: calc(100vh - 100px)`,保证相对视口上下各 **至少约 50px** 可视留白;弹窗 SHALL NOT 以固定高度占满视口;当内容超出时 **仅** `.el-dialog__body` 区域出现纵向滚动条,页头与底栏固定不滚动。
#### Scenario: 租户新增弹窗
- **WHEN** 在租户管理页打开新增或编辑租户弹窗
- **THEN** 表单 SHALL 以两列栅格展示常规字段、备注跨两列;弹窗 SHALL 带 `dialog-form-layout` 且正文区在内容过多时可滚动
#### Scenario: 高字段数弹窗
- **WHEN** 某业务弹窗字段超过两列舒适排布
- **THEN** 实现 SHALL 增加 `el-dialog` 的 `width` 并选用 `form-grid-3` 至 `form-grid-5` 之一,且 SHALL NOT 使用超过 5 列的栅格类
### Requirement: 图标组件使用 Element Plus 图标
`web-admin` 中的图标 SHALL 优先使用 Element Plus 内置图标(`@element-plus/icons-vue`),通过 `ArtSvgIcon` 组件的 EP 图标映射(`src/utils/ui/ep-icon-map.ts`)渲染;SHALL NOT 在新增代码中直接使用 Iconify 远程图标名(如 `ri:xxx`、`fluent:xxx`)。仅在 Element Plus 无等价图标时,方可保留 Iconify 图标名作为例外。
#### Scenario: 新增菜单路由
- **WHEN** 在 `src/router/modules/` 中新增路由并设置 `meta.icon`
- **THEN** `icon` 值 SHALL 使用 `ep-icon-map.ts` 中已注册的名称(如 `setting`、`grid`),SHALL NOT 使用 `ri:` 前缀的 Iconify 名
#### Scenario: 新增界面图标
- **WHEN** 在页面或组件中新增图标
- **THEN** SHALL 使用 `<ArtSvgIcon icon="ep-icon-name" />` 或 `<el-icon><XxxIcon /></el-icon>`,SHALL NOT 使用 `<ArtSvgIcon icon="ri:xxx" />` 除非 Element Plus 无等价图标
#### Scenario: 新增 Element Plus 图标
- **WHEN** Element Plus 提供了新的所需图标但 `ep-icon-map.ts` 尚未注册
- **THEN** 开发者 SHALL 先在 `ep-icon-map.ts` 中注册映射条目,再在业务代码中使用
### Requirement: 表格操作列固定在右侧
`web-admin` 中所有业务列表页(系统管理、业务模块等,不含 Demo 参考页)的 `el-table` 操作列 SHALL 使用 `fixed="right"` 固定在表格右侧。宽度规则:2 个按钮时 `min-width="260"`,3 个按钮时 `min-width="320"`,4 个及以上按钮时 `min-width="400"`。全局 CSS 通过 `.el-table-fixed-column--right` 选择器(注意:Element Plus 使用单短横线,非双下划线)提供固定列样式。
#### Scenario: 新增系统管理列表页
- **WHEN** 在系统管理模块下新增列表页面(如用户管理、角色管理等)
- **THEN** 表格操作列 SHALL 设置 `fixed="right"`,宽度按按钮数设置:≤ 2 按钮用 `min-width="260"`,3 按钮用 `min-width="320"`,≥ 4 按钮用 `min-width="400"`
#### Scenario: Code review 表格列
- **WHEN** PR 包含新增或修改的 `el-table-column` 且 `label="操作"`
- **THEN** 审查者 SHALL 确认该列包含 `fixed="right"` 属性,且宽度与按钮数量匹配(2 按钮 260px,3+ 按钮 320px)
### Requirement: 表格列宽使用 min-width 实现等比缩放
`web-admin` 中所有 `el-table` 的列宽属性 SHALL 使用 `min-width` 替代 `width`,使浏览器视口宽度变化时各列按基础比例等比伸缩。
**原理**:Element Plus 的布局逻辑是,所有 `min-width` 列按各自 `min-width` 值的比例分配容器可用宽度;当容器宽度小于所有 `min-width` 之和时,触发横向滚动。若部分列使用 `width`(固定宽度),则多余空间全部由 `min-width` 列吸收,导致视口加宽时只有 `min-width` 列变宽而 `width` 列不变。
**规则**:
1. 所有 `el-table-column`(包括序号列、数据列、操作列)SHALL 使用 `min-width` 设定基础宽度,SHALL NOT 使用 `width`。
2. `min-width` 值即为该列的最小可读宽度,也是等比分配的权重基数。
3. `el-table` 本身的 `width` 属性(如弹窗内嵌表格的固定总宽)不受此规则约束。
#### Scenario: 新增列表页
- **WHEN** 新增列表页包含 `el-table`
- **THEN** 所有 `el-table-column` 的宽度属性 SHALL 使用 `min-width`,SHALL NOT 使用 `width`
#### Scenario: 浏览器窗口宽度变化
- **WHEN** 用户调整浏览器窗口宽度
- **THEN** 各列宽度 SHALL 按 `min-width` 设定的基础比例等比伸缩
#### Scenario: Code review 表格列宽
- **WHEN** PR 包含新增或修改的 `el-table-column` 且使用 `width` 属性
- **THEN** 审查者 SHALL 要求改为 `min-width`
### Requirement: 侧边栏菜单图标层级规则
`web-admin` 左侧菜单区域的图标展示 SHALL 遵循以下层级规则:
1. **一级菜单**(顶层菜单项)SHALL 在菜单名称前展示图标(`meta.icon`)。
2. **二级及以下菜单**(子菜单项)SHALL NOT 展示图标,仅展示文字名称。
3. 双列菜单模式(`DualMenu`)左侧一级图标栏 SHALL 为每个一级菜单项渲染图标,与侧边栏一级规则一致。
4. 水平菜单(`HorizontalMenu`)和混合菜单(`MixedMenu`)如使用图标,SHALL 与侧边栏保持相同的层级规则。
#### Scenario: 侧边栏一级菜单图标
- **WHEN** 用户打开 `web-admin` 主页,查看左侧菜单区域的一级菜单项
- **THEN** 每个一级菜单项 SHALL 在文字名称左侧展示其 `meta.icon` 对应的图标
#### Scenario: 侧边栏二级菜单无图标
- **WHEN** 用户展开某个一级菜单,查看其下的二级(及更深层级)菜单项
- **THEN** 二级及以下菜单项 SHALL NOT 展示任何图标,仅显示菜单文字名称
#### Scenario: Code review 菜单图标
- **WHEN** PR 修改了 `SidebarSubmenu.vue` 或其他菜单组件中图标渲染逻辑
- **THEN** 审查者 SHALL 确认 `level === 0` 条件判断保持不变,仅一级菜单渲染图标
### Requirement: 标签页(WorkTab)不展示图标
`web-admin` 顶部标签页区域(WorkTab strip)的每个标签 SHALL 仅展示文字标题,SHALL NOT 展示图标。路由 `meta.icon` 仅用于侧边栏菜单,SHALL NOT 传递到标签页渲染。
#### Scenario: 标签页仅显示文字
- **WHEN** 用户打开任意页面,顶部标签页区域出现对应标签
- **THEN** 标签 SHALL 仅展示页面标题文字,SHALL NOT 在标题前展示任何图标
#### Scenario: Code review 标签页图标
- **WHEN** PR 修改了 `art-work-tab/index.vue` 或 `utils/navigation/worktab.ts`
- **THEN** 审查者 SHALL 确认标签页组件不渲染图标,且 `setWorktab` 不向 `openTab` 传递 `icon` 字段
### Requirement: 表单字段注释格式
`web-admin` 中弹窗表单(`el-form-item`)如需对某字段添加辅助说明文字(如"数值越小越靠前"),该注释 SHALL 以小字形式直接显示在 label 文字后方,SHALL NOT 放置在输入控件旁边或下方。实现方式为使用 `el-form-item` 的 `#label` 插槽,在 label 文字后追加 `<span class="form-label-tip">注释内容</span>`。样式类 `form-label-tip` 定义在全局 `app.scss` 中(字号 12px、`font-weight: 400`、使用 `--mint-text-muted` 颜色)。
#### Scenario: 排序字段注释
- **WHEN** 弹窗表单中包含排序字段(`orderNum`)
- **THEN** label SHALL 显示为"排序",其后紧跟小字注释"数值越小越靠前",使用 `.form-label-tip` 样式类,SHALL NOT 使用 `.sort-field` + `.form-tip` 的旧模式
#### Scenario: 新增带注释的表单字段
- **WHEN** 新增弹窗表单中的某字段需要辅助说明
- **THEN** SHALL 使用 `<template #label>字段名<span class="form-label-tip">注释内容</span></template>` 格式,SHALL NOT 将注释放在输入控件旁
### Requirement: 列表行查看弹窗(按钮 + 双击)
`web-admin` 中**所有** `el-table` 组件——包括主列表页(标准列表、树形列表)、弹窗内嵌列表(如编辑弹窗/查看弹窗/选择弹窗中的子表格)、侧栏面板内的列表——若操作列包含「查看」「查看详情」「查看确认信息」等只读查看类按钮,SHALL 同时提供行双击触发查看的能力,实现规则如下:
1. **操作列「查看」按钮**:操作列 SHALL 在首位包含一个「查看」按钮(`<el-icon><View /></el-icon>查看`),点击后弹出只读查看弹窗。
2. **行双击触发**:`el-table` SHALL 监听 `@row-dblclick` 事件,双击任意行时触发与「查看」按钮相同的弹窗(即调用同一 `handleView(row)` 函数)。
3. **条件按钮场景**:当操作列按钮随状态/Tab 变化(如待确认 Tab 显示「确认收款」,已确认 Tab 显示「查看确认信息」),`@row-dblclick` handler SHALL 仅在「查看」类按钮可见时触发对应查看函数,其余状态下可忽略或调用对应主操作函数。
3. **弹窗样式**:查看弹窗 SHALL 使用 `SystemViewDialog` 共享组件(`src/components/business/system-view-dialog/index.vue`),样式对齐待办中心「查看详情」弹窗(无标题栏、hero 渐变头图 + 图标、KV grid 字段展示、关闭按钮)。
4. **字段内容**:查看弹窗 SHALL 展示该行记录的所有业务字段(含状态映射、字典翻译),不展示 `id`、`del_flag` 等技术字段。长文本/备注字段 SHALL 使用 `span: true` 跨两列展示。
5. **区块标题样式**:查看弹窗中各区块标题(`SystemViewDialog` 的 section title,渲染为 `h3`)SHALL 与字段标签(`dt`)形成明显视觉区分。区块标题 SHALL 使用深色文字(`#1e293b`)配合左侧主色竖条装饰(`border-left: 3px solid #10b981`),字号 14px、`font-weight: 600`;字段标签 SHALL 保持为辅助灰色(`#64748b`)、`font-weight: 500`。区块标题样式 SHALL NOT 与字段标签使用相同颜色或相近字重。
#### Scenario: 新增系统管理列表页
- **WHEN** 在系统管理模块下新增标准列表页面
- **THEN** 操作列首位 SHALL 包含「查看」按钮,且 `el-table` SHALL 绑定 `@row-dblclick="handleView"` 事件
- **THEN** 点击「查看」按钮或双击行 SHALL 打开同一 `SystemViewDialog` 弹窗
#### Scenario: 现有页面补充查看弹窗
- **WHEN** 为现有列表页补充查看弹窗
- **THEN** SHALL 同时添加操作列「查看」按钮与 `@row-dblclick` 双击事件,两者调用同一个 `handleView` 函数
#### Scenario: Code review 查看弹窗
- **WHEN** PR 修改列表页面且页面包含 `el-table`
- **THEN** 审查者 SHALL 确认 `el-table` 绑定了 `@row-dblclick` 事件,且操作列包含「查看」按钮
### Requirement: 查看弹窗中编码字段展示为可读标签
`web-admin` 中所有只读查看弹窗(`SystemViewDialog` 及类似组件)中,存储为编码(code / value)的字段 SHALL 展示为对应的人类可读标签(label),SHALL NOT 直接展示原始编码值。这包括但不限于:字典编码→字典标签、状态编码→状态文本、站点类型编码→类型名称等。
对于多值编码字段(如逗号分隔的编码串),SHALL 逐个编码转换为标签后以顿号(`、`)连接展示;无匹配标签时 SHALL 回退展示原始编码值。
#### Scenario: 站点管理查看弹窗
- **WHEN** 用户点击站点管理列表中某行的「查看」按钮
- **THEN** 「经营范围」字段 SHALL 展示为编码对应的标签文本(如"蔬菜、水果"),SHALL NOT 展示原始编码串(如"1,2")
#### Scenario: 新增包含字典编码字段的查看弹窗
- **WHEN** 新增列表页的查看弹窗中包含字典编码、状态编码等字段
- **THEN** 实现 SHALL 使用对应字典选项(`businessScopeOptions`、`statusOptions` 等)将编码翻译为标签后展示
#### Scenario: Code review 查看弹窗编码展示
- **WHEN** PR 修改查看弹窗中涉及编码类字段的展示
- **THEN** 审查者 SHALL 确认字段值已翻译为可读标签,SHALL NOT 出现原始编码值
### Requirement: 排序字段不在界面默认展示
`web-admin` 中所有列表页的表格列、新增弹窗表单、编辑弹窗表单 SHALL NOT 默认包含排序字段(`sortOrder`、`orderNum`、`排序`等)。排序字段仅在产品明确要求时按需添加,不应作为默认 UI 元素。
#### Scenario: 新增标准列表页
- **WHEN** 新增标准 CRUD 列表页(含新增/编辑弹窗)
- **THEN** 表格列和弹窗表单中 SHALL NOT 包含排序相关字段,除非产品需求明确要求
#### Scenario: Code review 排序字段
- **WHEN** PR 新增列表页或弹窗中包含排序字段
- **THEN** 审查者 SHALL 确认产品需求中有明确排序要求,否则 SHALL 要求移除
### Requirement: 筛选区关键词字段排第一
`web-admin` 列表页筛选区(`.filter-block` 内 `.filter-grid`)若包含**关键词**模糊检索字段(`label` 为「关键词」或等效语义、`el-input` 绑定 `keyword` 等),该字段 SHALL 作为 `.filter-grid` 中**第一个** `el-form-item`,位于下拉筛选、状态筛选、日期范围等条件字段之前。落地参考:`#/demo/unit`、`#/demo/user` 及质检模块「入库放行审核」页。
#### Scenario: 新增含关键词的列表页
- **WHEN** 在列表页筛选区同时提供关键词输入与下拉/日期等筛选条件
- **THEN** 「关键词」对应的 `el-form-item` SHALL 位于 `.filter-grid` 首位
#### Scenario: 入库放行审核筛选区
- **WHEN** 用户打开质检模块「入库放行审核」列表页
- **THEN** 筛选区 `.filter-grid` 中第一个字段 SHALL 为「关键词」,其后依次为协议类别、审核结论、入库状态等条件
#### Scenario: Code review 筛选区字段顺序
- **WHEN** PR 新增或修改列表页 `.filter-block` 筛选字段
- **THEN** 审查者 SHALL 确认存在关键词字段时其位于 `.filter-grid` 第一位
### Requirement: 列表筛选自动触发搜索
`web-admin` 所有列表页的筛选表单(`.filter-block` 内 `searchForm` / `filterForm`)SHALL 使用 `watchDebounced`(来自 `@vueuse/core`,已 auto-import)对筛选表单做 deep watch,当任意字段值变更后 **300ms** 防抖自动触发搜索(重置页码为 1 并调用数据加载函数),无需用户手动点击「搜索」按钮。「搜索」按钮 SHALL 保留作为手动触发入口。
#### 实现模式
```typescript
watchDebounced(
searchForm, // 或 filterForm
() => {
pagination.page = 1
loadData()
},
{ debounce: 300, deep: true }
)
```
对于含有**独立日期范围选择器**(`dateRange` ref,不属于 searchForm)的页面(如 `material-stock-log`、`qc-material-usage`),SHALL 额外添加 `watch(dateRange, ...)` 监听,在日期变更时同样触发搜索。
#### Scenario: 单字段筛选自动搜索
- **WHEN** 用户在筛选区关键词输入框中键入内容
- **THEN** 停止输入 300ms 后 SHALL 自动执行搜索,无需点击「搜索」按钮
#### Scenario: 多字段筛选自动搜索
- **WHEN** 列表页筛选区包含关键词、下拉选择、日期选择等多个筛选控件
- **AND** 用户修改其中任意一个筛选字段的值
- **THEN** 变更后 300ms SHALL 自动执行搜索
#### Scenario: 日期范围筛选
- **WHEN** 页面使用独立的 `dateRange` ref(非 searchForm 内字段)进行日期范围筛选
- **THEN** SHALL 额外使用 `watch(dateRange, ...)` 监听变更并触发搜索
#### Scenario: 保留手动搜索
- **WHEN** 用户点击筛选区的「搜索」按钮
- **THEN** SHALL 立即执行搜索(无防抖),与自动搜索行为并行可用
#### Scenario: Code review 自动搜索
- **WHEN** PR 新增或修改列表页筛选区
- **THEN** 审查者 SHALL 确认筛选表单已添加 `watchDebounced` 自动搜索,且 `debounce` 不超过 500ms
### Requirement: 附件上传卡片化布局
`web-admin` 弹窗表单中的附件上传与展示 SHALL 采用**卡片化布局**,实现与交互以 `web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue` 中「协议附件」为权威参考;储存证等业务弹窗(如 `web-admin/src/views/cell-storage/storage-cert/index.vue`)SHALL 复用同一套 DOM 结构、类名与样式规则,仅调整 `el-form-item` 的 `label` 与上传 `folder` 等业务参数。
#### 布局与视觉
1. **结构**:`el-form-item`(建议 `class="form-col-span-2"` 占满弹窗栅格宽)→ `.attachment-upload` → `.attachment-list`(flex 行)→ 上传卡片 + 若干 `.attachment-item`;`.upload-hint` 在 flex 行**下方**独立一行。
2. **上传卡片**:宽 **140px**、高 **96px**;`border: 1px dashed #a7f3d0`;`background: #f0fdf4`;`border-radius: 8px`;居中 `Plus`(size 28,`#10b981`)+ 文案「上传附件」(`.upload-card-text`,12px `#10b981` font-weight 500)。hover:`border-color: #10b981`、`border-style: solid`、`background: #ecfdf5`。
3. **附件卡片**:宽 **140px**;预览区高 **96px**;`border: 1px solid #e2e8f0`;`border-radius: 8px`;hover:`border-color: #10b981`、`box-shadow: 0 2px 8px rgba(16, 185, 129, 0.1)`。
4. **预览区**(`.attachment-preview`):背景 `#f8fafc`;图片已上传且 `fileUrl` 存在时显示缩略图(`object-fit: cover`),可点击预览;图片上传中无 URL 时显示 `Loading`;视频显示 `.attachment-preview__icon--video`(`#ede9fe` / `#7c3aed` + `VideoPlay`);其他文件显示 `.attachment-preview__icon--doc`(`#e0f2fe` / `#0284c7` + `Document`)。
5. **卡片底部**(`.attachment-footer`):文件名 `.attachment-name`(11px `#334155`,最多 2 行省略,`title` 为完整文件名);`.attachment-actions` 居中排列 **下载**(`Download`)、**删除**(`Delete`,`.action-btn--danger` hover 红色)。
6. **上传中**:`.attachment-item.is-uploading` → `opacity: 0.6`、`pointer-events: none`。
7. **提示**:`.upload-hint` — 11px `#94a3b8`,`margin-top: 8px`;默认文案:「支持拖拽或点击上传图片、视频、PDF、Word、Excel等文件,单文件不超过 20MB」。
8. **色板**:SHALL 使用 Fresh Mint(`#10b981`、`#a7f3d0`、`#f0fdf4`、`#ecfdf5`);SHALL NOT 使用小程序主色 `#0a9f6d`。
9. **禁止**:SHALL NOT 使用 `el-upload` 的 `list-type="picture-card"`、默认 `file-list` 展示,或单独一行纯文字「选择文件」按钮替代上传卡片。
#### `el-upload` 配置
| 属性 | 值 |
|------|-----|
| `:auto-upload` | `false` |
| `:show-file-list` | `false` |
| `:multiple` | `true` |
| `accept` | `image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar` |
| `@change` | 选中后立即走上传逻辑(见下) |
上传卡片 SHALL 作为 `.attachment-list` 的**第一个子元素**,且始终渲染(不因已有附件数量而隐藏)。
#### 拖拽上传
1. **Drop zone**:卡片化附件 SHALL 以 `.attachment-upload` 根容器为拖拽落区(含 `.attachment-list` 与 `.upload-hint` 所在整块);表格行内纸质材料 SHALL 以 `.paper-att-list` 为落区。SHALL NOT 使用 `el-upload` 的 `drag` 属性替代上述布局(避免破坏 140×96 上传卡片结构)。
2. **双通道**:SHALL 同时支持**点击上传卡片选文件**与**拖拽文件到 drop zone**;两种方式 SHALL 复用同一套 `@change` / `handleAttachmentChange` 上传逻辑(校验 → 占位 → `fetchUploadAttachment` → 回填或失败移除)。
3. **多文件**:拖拽 `dataTransfer.files` 中多个文件时,SHALL 按顺序逐个走与 `:multiple="true"` 点击选择相同的上传流程。
4. **dragover 视觉**:落区 dragover 时 SHALL 添加修饰类 `.is-dragover`(全局样式见 `web-admin/src/assets/styles/core/app.scss`):Fresh Mint 虚线边框 `#10b981`、浅绿底 `#ecfdf5`、圆角 8px。
5. **实现**:SHALL 使用 `web-admin/src/composables/useAttachmentDropZone.ts` 在各页面/组件的 drop zone 上独立挂载(`v-bind="dropZoneHandlers"` + `:class="{ 'is-dragover': isDragover }"`);只读/禁用态 SHALL 传入 `disabled` 且不响应 drop。
6. **校验与提交**:拖拽文件的类型、单文件 20MB 限制、上传中提交拦截 SHALL 与点击上传一致。
#### 数据模型与上传逻辑
附件列表 SHALL 为响应式数组,元素至少包含:`id`、`fileName`、`fileUrl`、`fileType`(可选)、`_uploading`(可选布尔)。
1. **选择文件后**:校验单文件 ≤ **20MB**;超限则 `ElMessage.error` 并 `clearFiles()`。
2. **立即上传**:向数组 push 占位项(`_uploading: true`、`fileUrl: ''`);`FormData` 调用 `fetchUploadAttachment`(`@/api/business`),`fd.append('folder', '<业务目录>')`(如 `protocol`、`storage-cert`、`protocol-payment`);成功后回填 `fileUrl` 等字段并 `_uploading: false`;失败则移除该项并提示。
3. **收尾**:`nextTick` 后 `uploadRef.clearFiles()`,避免同一文件无法重复选择。
4. **删除**:从数组 `splice` 移除,不单独调删除接口(除非产品另有要求)。
5. **下载**:使用 `@/utils/download-attachment` 的 `downloadAttachmentFile(file, { fullFileUrl })`;`fullFileUrl` 需拼接 `VITE_MINIO_BASE_URL`(与参考页一致)。
6. **预览**:图片/视频已上传项点击预览区,打开独立 `el-dialog`(宽 800px,`destroy-on-close`);图片用 `<img>`,视频用 `<video controls>`。
7. **提交**:若存在 `_uploading === true` 的项,SHALL 阻止提交并提示「附件上传中,请稍候」;业务字段若存逗号分隔 URL,提交前 SHALL 从数组中过滤 `fileUrl` 非空且非上传中的项拼接。
缴费明细等嵌套场景可使用 `.upload-card--sm`(宽 96px、高 76px),其余规则不变。
#### 标准模板(Vue)
```vue
<el-form-item label="协议附件" class="form-col-span-2">
<div class="attachment-upload">
<div class="attachment-list">
<el-upload
ref="attachmentUploadRef"
:auto-upload="false"
:show-file-list="false"
:multiple="true"
accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar"
@change="handleAttachmentChange"
>
<div class="upload-card">
<el-icon :size="28" color="#10b981"><Plus /></el-icon>
<span class="upload-card-text">上传附件</span>
</div>
</el-upload>
<div
v-for="(file, index) in attachments"
:key="file.id || index"
class="attachment-item"
:class="{ 'is-uploading': file._uploading }"
>
<div
class="attachment-preview"
@click="isImageFile(file) && file.fileUrl ? handlePreviewAttachment(file) : undefined"
>
<template v-if="isImageFile(file) && file.fileUrl">
<img :src="fullFileUrl(file.fileUrl)" :alt="file.fileName" />
</template>
<template v-else-if="isImageFile(file) && !file.fileUrl">
<el-icon :size="24" color="#94a3b8"><Loading /></el-icon>
</template>
<template v-else-if="isVideoFile(file)">
<div class="attachment-preview__icon attachment-preview__icon--video">
<el-icon :size="28"><VideoPlay /></el-icon>
</div>
</template>
<template v-else>
<div class="attachment-preview__icon attachment-preview__icon--doc">
<el-icon :size="28"><Document /></el-icon>
</div>
</template>
</div>
<div class="attachment-footer">
<span class="attachment-name" :title="file.fileName">{{ file.fileName }}</span>
<div class="attachment-actions">
<el-icon class="action-btn" title="下载" @click="handleDownloadAttachment(file)">
<Download />
</el-icon>
<el-icon
class="action-btn action-btn--danger"
title="删除"
@click="removeAttachment(index)"
>
<Delete />
</el-icon>
</div>
</div>
</div>
</div>
<span class="upload-hint">支持图片、视频、PDF、Word、Excel等文件,单文件不超过 20MB</span>
</div>
</el-form-item>
```
> 说明:`attachments` 为业务侧附件数组变量名;图标 `Plus`、`Loading`、`VideoPlay`、`Document`、`Download`、`Delete` SHALL 在 `<script setup>` 中从 `@element-plus/icons-vue` 显式 import。
#### 标准样式(SCSS,弹窗 `scoped` 内)
完整类名与尺寸 SHALL 与参考页一致,核心片段:
```scss
.attachment-upload { width: 100%; }
.attachment-list { display: flex; flex-wrap: wrap; gap: 12px; }
.upload-card {
width: 140px; height: 96px;
border: 1px dashed #a7f3d0; border-radius: 8px; background: #f0fdf4;
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 6px; cursor: pointer;
&:hover { border-color: #10b981; border-style: solid; background: #ecfdf5; }
}
.upload-card-text { font-size: 12px; color: #10b981; font-weight: 500; line-height: 1; }
.upload-hint { display: block; font-size: 11px; color: #94a3b8; margin-top: 8px; line-height: 1; }
.attachment-item {
width: 140px; border: 1px solid #e2e8f0; border-radius: 8px; background: #fff; overflow: hidden;
&.is-uploading { opacity: 0.6; pointer-events: none; }
&:hover { border-color: #10b981; box-shadow: 0 2px 8px rgba(16, 185, 129, 0.1); }
}
.attachment-preview {
width: 100%; height: 96px; display: flex; align-items: center; justify-content: center;
background: #f8fafc; overflow: hidden; cursor: pointer;
img { width: 100%; height: 100%; object-fit: cover; display: block; }
}
.attachment-preview__icon {
width: 48px; height: 48px; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
&--video { background: #ede9fe; color: #7c3aed; }
&--doc { background: #e0f2fe; color: #0284c7; }
}
.attachment-footer { display: flex; flex-direction: column; padding: 6px 8px; }
.attachment-name {
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
overflow: hidden; font-size: 11px; color: #334155; line-height: 1.4; word-break: break-all;
}
.attachment-actions {
display: flex; justify-content: center; gap: 8px; margin-top: 4px;
}
.action-btn {
width: 22px; height: 22px; border-radius: 4px; background: #f1f5f9; color: #64748b;
cursor: pointer;
&:hover { background: #10b981; color: #fff; }
&--danger:hover { background: #ef4444; color: #fff; }
}
```
只读查看态 SHALL 使用 `web-admin/src/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue`(无上传卡片、无删除,可保留下载/预览)。
#### Scenario: 新增弹窗包含附件上传
- **WHEN** 在弹窗表单中新增附件上传
- **THEN** SHALL 使用上述 DOM 结构、类名与 140×96 卡片尺寸
- **THEN** 上传卡片 SHALL 位于 `.attachment-list` 第一项且始终可见
- **THEN** `.upload-hint` SHALL 位于卡片行下方
#### Scenario: 附件卡片预览与操作
- **WHEN** 附件为已上传图片
- **THEN** 预览区 SHALL 显示缩略图,点击 SHALL 打开预览弹窗
- **WHEN** 附件为视频或其他文档
- **THEN** 预览区 SHALL 分别显示视频/文档图标(见布局与视觉 §4)
- **WHEN** 用户点击下载或删除
- **THEN** SHALL 分别调用 `downloadAttachmentFile` 与数组移除逻辑
#### Scenario: 附件上传中状态
- **WHEN** 文件正在上传
- **THEN** 对应卡片 SHALL 为 `is-uploading`(半透明、不可点),预览区显示 `Loading`
- **WHEN** 用户提交表单且仍有 `_uploading` 项
- **THEN** SHALL 阻止提交并提示等待上传完成
#### Scenario: 拖拽文件到附件落区
- **WHEN** 用户将合法文件拖拽到 `.attachment-upload`(或纸质材料 `.paper-att-list`)
- **THEN** 落区 SHALL 显示 `.is-dragover` 高亮
- **THEN** 松手后 SHALL 与点击选文件相同:立即上传、展示卡片、支持预览/下载/删除
- **WHEN** 用户拖拽多个文件
- **THEN** SHALL 逐个上传
- **WHEN** 落区为只读或 `disabled`
- **THEN** SHALL NOT 响应 drop
#### Scenario: Code review 附件上传
- **WHEN** PR 新增或修改附件上传 UI
- **THEN** 审查者 SHALL 对照 `ProtocolFormDialog.vue` 确认卡片化布局与立即上传逻辑
- **THEN** SHALL 拒绝 `picture-card`、`show-file-list` 默认列表、与规范不一致的尺寸/色值
### Requirement: 按钮须携带图标
`web-admin` 中所有带文字标签的 `el-button`(或 `ElButton`)SHALL 在文字**前方**包含 `<el-icon>` 子元素,使用 `@element-plus/icons-vue` 提供的图标组件;SHALL NOT 出现仅有文字而无图标的按钮。该规则适用于列表工具栏、表格操作列、`el-dialog` / 抽屉的 `#footer` 底栏、表单内 `link` / `plain` / `size="small"` 等**凡渲染可见文案的按钮**,不限于 `type="primary"`。图标的引入方式为在文件 `<script setup>` 中显式 `import` 所需图标组件(`@element-plus/icons-vue` 不属于 auto-import 范围)。图标与文字间距由全局样式保证(与 Fresh Mint 弹窗底栏约定一致,约 6px)。
#### 常用按钮图标映射
| 按钮语义 | 图标组件 | 适用场景 |
|---|---|---|
| 新增 | `Plus` | 工具栏「新增 XXX」按钮 |
| 搜索 | `Search` | 筛选区「搜索」按钮 |
| 重置 | `RefreshLeft` | 筛选区「重置」按钮 |
| 确定 / 确认 / 提交 / 保存 | `Check` | 弹窗确认类按钮 |
| 取消 / 关闭 | `Close` | 弹窗取消、关闭按钮 |
| 编辑 / 修改 | `Edit` | 操作列「编辑」按钮 |
| 删除 / 批量删除 | `Delete` | 操作列、工具栏「删除」按钮 |
| 查看 / 详情 / 查看详情 | `View` | 操作列「查看」「详情」按钮 |
| 审批 / 审核 | `EditPen` | 操作列「审批」按钮 |
| 配置 / 设置 | `Setting` 或 `SetUp` | 操作列「配置」按钮 |
| 导入 / 上传 | `Upload` | 工具栏「导入」、表单「上传」按钮 |
| 折叠 / 展开 | `ArrowDown` / `ArrowUp` | 工具栏「展开/收起」按钮 |
| 充值 / 发起充值 | `Wallet` | 渠道服务订单等预付款充值操作 |
| 退款 / 申请退款 | `Sell` | 退款、退费类操作 |
| 购买产品 | `ShoppingCart` | 购买、下单类操作 |
| 退订 | `Remove` | 退订、减少持有类操作 |
| 作废 | `Delete` | 作废流水/订单(与删除同图标,可用 `op-warn` 样式区分) |
| 操作流水 / 记录列表 | `List` | 「操作流水」等子列表入口 |
#### Scenario: 新增列表页工具栏按钮
- **WHEN** 在列表页筛选区添加「搜索」和「重置」按钮
- **THEN** 按钮 SHALL 包含 `<el-icon><Search /></el-icon>` 和 `<el-icon><RefreshLeft /></el-icon>`,SHALL NOT 仅有文字
#### Scenario: 新增弹窗底部按钮
- **WHEN** 在 `el-dialog` 的 `#footer` 插槽中添加「取消」和「确定」按钮
- **THEN** 取消按钮 SHALL 包含 `<el-icon><Close /></el-icon>`,确定按钮 SHALL 包含 `<el-icon><Check /></el-icon>`
#### Scenario: 新增表格操作列按钮
- **WHEN** 在表格操作列添加「编辑」「删除」「查看」等操作按钮
- **THEN** 每个按钮 SHALL 包含对应的 `<el-icon>` 子元素(如 `<el-icon><Edit /></el-icon>`),SHALL NOT 仅有文字
#### Scenario: 新增工具栏主操作按钮
- **WHEN** 在列表工具栏添加「新增 XXX」主操作按钮
- **THEN** 按钮 SHALL 包含 `<el-icon><Plus /></el-icon>`
#### Scenario: Code review 按钮图标
- **WHEN** PR 包含新增或修改的 `el-button`
- **THEN** 审查者 SHALL 确认该按钮包含 `<el-icon>` 子元素,且图标组件已从 `@element-plus/icons-vue` 显式导入
#### Scenario: 渠道服务订单与财务确认页
- **WHEN** 实现或修改 `web-admin/src/views/pilot-service/channel-service-order/**` 或 `channel-service-order-finance/**` 中的按钮(含弹窗底栏、表格操作列、行内 link 删除)
- **THEN** 每个带文案的 `el-button` SHALL 按上表携带图标(如查看 `View`、确认 `Check`、取消 `Close`、作废 `Delete`、发起充值 `Wallet` 等)
- **THEN** 弹窗底栏取消/确定 SHALL 使用 `btn-ghost` / `btn-main` 类名并与 Demo 页图标约定一致
#### 标准写法示例
```html
<!-- 工具栏 -->
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增单位
</el-button>
<!-- 筛选区 -->
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>搜索
</el-button>
<!-- 操作列 -->
<el-button class="btn-op-item primary" @click="handleEdit(row)">
<el-icon><Edit /></el-icon>编辑
</el-button>
<el-button class="btn-op-item" @click="handleDelete(row)">
<el-icon><Delete /></el-icon>删除
</el-button>
<!-- 弹窗底部 -->
<el-button class="btn-ghost" @click="dialogVisible = false">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button class="btn-main" type="primary" @click="handleSubmit">
<el-icon><Check /></el-icon>确定
</el-button>
```
```typescript
// <script setup> 中显式导入
import { Plus, Close, Check, Search, RefreshLeft, Edit, Delete, View } from '@element-plus/icons-vue'
```
### Requirement: 状态、结论与类型字段使用 Tag 标签展示
`web-admin` 中所有列表表格和查看弹窗中的**状态**、**结论**、**类型**等枚举类字段 SHALL 使用 Element Plus `<el-tag>` 组件以彩色标签形式展示,SHALL NOT 以纯文本形式展示。具体规则如下:
#### 1. 表格列中的 Tag 展示
`el-table` 中所有状态/结论/类型列 SHALL 在 `<template #default="{ row }">` 内使用 `<el-tag>` 渲染,并通过 `:type` 属性区分语义色。SHALL NOT 使用自定义 `<span class="tag">` 或纯文本。
**标准写法:**
```html
<el-table-column prop="status" label="状态" min-width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">
{{ row.status === '1' ? '正常' : '停用' }}
</el-tag>
</template>
</el-table-column>
```
**多状态映射标准写法(使用状态标签注册表):**
```html
<el-table-column prop="workOrderStatus" label="状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag :type="workOrderStatusTagType(row.workOrderStatus)" size="small">
{{ workOrderStatusLabel(row.workOrderStatus) }}
</el-tag>
</template>
</el-table-column>
```
```typescript
import { workOrderStatusLabel, workOrderStatusTagType } from '@/utils/status-tag-registry'
```
#### 2. 查看弹窗(SystemViewDialog)中的 Tag 展示
使用 `SystemViewDialog` 组件(`src/components/business/system-view-dialog/index.vue`)时,状态/结论/类型字段 SHALL 在 `ViewField` 定义中传入 `tagType` 属性。组件检测到 `tagType` 后自动以 `<el-tag>` 渲染该字段值。
**ViewField 接口扩展(已实现):**
```typescript
export type TagType = 'success' | 'warning' | 'danger' | 'info' | '' | 'primary'
export interface ViewField {
label: string
value: string | number | boolean | null | undefined
span?: boolean
/** 传入后以 el-tag 渲染,值为 el-tag 的 type 属性 */
tagType?: TagType
}
```
**标准写法:**
```typescript
import { taskStatusLabel, taskStatusTagType, qcTaskConclusionLabel, qcTaskConclusionTagType } from '@/utils/status-tag-registry'
const viewSections = computed<ViewSection[]>(() => [
{
title: '任务信息',
fields: [
{ label: '任务号', value: d.taskNo },
{ label: '状态', value: taskStatusLabel(d.status), tagType: taskStatusTagType(d.status) },
{ label: '检测结论', value: qcTaskConclusionLabel(d.conclusion), tagType: qcTaskConclusionTagType(d.conclusion) },
// 普通字段不加 tagType,仍以纯文本展示
{ label: '备注', value: d.remark || '—', span: true },
]
}
])
```
#### 3. 自定义查看弹窗中的 Tag 展示
不使用 `SystemViewDialog` 的自定义查看弹窗(如 `ProtocolViewDialog`、`DonationViewDialog` 等)中的状态/结论字段 SHALL 同样使用 `<el-tag>` 展示。
**标准写法:**
```html
<dd>
<el-tag :type="acceptConclusionTagType(row.acceptConclusion)" size="small">
{{ acceptConclusionLabel(row.acceptConclusion) }}
</el-tag>
</dd>
```
```typescript
import { acceptConclusionLabel, acceptConclusionTagType } from '@/utils/status-tag-registry'
```
#### 4. Tag 类型语义色映射规范
| Tag Type | 颜色 | 适用语义 | 示例 |
|---|---|---|---|
| `success` | 绿色 | 正常、启用、已完成、生效中、通过 | 正常/启用/已完成/接收/通过 |
| `warning` | 橙色 | 进行中、待确认、已过期、预警 | 进行中/已排配/已过期/待确认 |
| `danger` | 红色 | 异常、停用、已作废、拒绝、不通过;质检任务待办醒目 | 异常/已作废/拒绝/不通过/质检待执行/质检待结论 |
| `info` | 灰色 | 待生效、未知、默认、停用 | 待生效/停用/默认 |
| `primary` | 蓝色 | 特殊分类、来源类型、转出 | 转科研/转细胞存储/来源类型/菜单类型 |
**对于启用/停用的状态字段**,SHALL 使用 `success`(启用)和 `info`(停用),SHALL NOT 使用 `danger` 表示停用状态。
#### 5. 状态标签注册表(集中定义 label + tagType)
`web-admin` 中所有状态、结论、类型等枚举字段的 **文案(label)与 Tag 语义色(tagType)SHALL 在单一注册表中集中定义**,权威文件为 `web-admin/src/utils/status-tag-registry.ts`。
**核心原则:**
1. **同一枚举值全局一致**:同一枚举 code 在列表、查看弹窗、导出、嵌套子列表等任何展示位置,SHALL 使用相同的 label 与 tagType;SHALL NOT 在页面内用三元表达式或局部 `Record` 重复定义映射。
2. **成对导出**:每个枚举域 SHALL 导出 registry 对象及配套的 `xxxLabel(code)`、`xxxTagType(code)` 两个 getter;未知 code 时 label 回退为原值、`tagType` 回退为 `'info'`。
3. **新增枚举先注册后使用**:新增业务枚举时,SHALL 先在 `status-tag-registry.ts` 追加 registry 与 getter,再在页面/组件中 import 使用;SHALL NOT 直接在 `.vue` 中写 `if/else` 或内联映射。
4. **域内分组**:registry 内按业务域分区维护(协议/存储、质检、渠道、订单、系统通用等),便于检索与 code review。
**注册表结构(已实现):**
```typescript
/** el-tag 的 type 属性值 */
export type TagType = 'success' | 'warning' | 'danger' | 'info' | '' | 'primary'
export interface StatusItem {
label: string
tagType: TagType
}
export type StatusRegistry = Record<string, StatusItem>
export const protocolStatus: StatusRegistry = {
dsx: { label: '待生效', tagType: 'info' },
sxz: { label: '生效中', tagType: 'success' },
ygq: { label: '已过期', tagType: 'warning' },
yzf: { label: '已作废', tagType: 'danger' },
zky: { label: '转科研', tagType: 'primary' }
}
export const protocolStatusLabel = createLabelGetter(protocolStatus)
export const protocolStatusTagType = createTagTypeGetter(protocolStatus)
```
**页面使用方式:**
```typescript
import { protocolStatusLabel, protocolStatusTagType } from '@/utils/status-tag-registry'
```
```html
<el-tag :type="protocolStatusTagType(row.status)" size="small">
{{ protocolStatusLabel(row.status) }}
</el-tag>
```
**兼容 re-export(非新增入口):**
`utils/protocol-payment-labels.ts`、`utils/qc-labels.ts` 等历史文件 MAY 继续 re-export 注册表中的 getter 以保持既有 import 路径;**新增枚举 SHALL 直接写入 `status-tag-registry.ts`**,SHALL NOT 再新建独立的 label/tag 映射文件。
**禁止写法:**
- 页面 `<script setup>` 内定义 `function xxxTagType()` / `const xxxMap = { ... }` 映射已在或应在注册表中的枚举
- 查看弹窗 `ViewField.tagType` 使用内联三元表达式(如 `d.status === 'ywc' ? 'success' : ...`)替代 registry getter
- 同一枚举在不同页面使用不同 tagType
#### Scenario: 新增业务枚举的状态展示
- **WHEN** 新增页面需展示新的状态/结论/类型枚举字段
- **THEN** 开发者 SHALL 先在 `status-tag-registry.ts` 注册该枚举的 label 与 tagType,并在页面中 import 对应的 `xxxLabel` / `xxxTagType` getter 使用
#### Scenario: 复用已有枚举的状态展示
- **WHEN** 页面需展示已在注册表中定义的枚举(如 `protocolStatus`、`taskStatus`、`enableStatus`)
- **THEN** 页面 SHALL 直接从 `@/utils/status-tag-registry` import 并使用,SHALL NOT 在页面内重复定义映射
#### Scenario: 新增列表页包含状态列
- **WHEN** 新增列表页的表格中包含状态、结论或类型字段
- **THEN** 该列 SHALL 使用 `<el-tag>` 渲染,通过 `:type` 绑定语义色,SHALL NOT 使用纯文本或自定义 `<span class="tag">`
#### Scenario: 新增查看弹窗包含状态字段
- **WHEN** 新增 `SystemViewDialog` 查看弹窗,且 fields 中包含状态/结论/类型字段
- **THEN** 该字段 SHALL 传入 `tagType` 属性,组件自动以 `<el-tag>` 渲染
#### Scenario: Code review Tag 展示
- **WHEN** PR 修改列表页或查看弹窗中的状态/结论/类型字段展示
- **THEN** 审查者 SHALL 确认使用 `<el-tag>` 组件(表格列)或 `tagType` 属性(SystemViewDialog),SHALL NOT 出现纯文本状态展示或自定义 `<span class="tag">`;若枚举已在或应进入注册表,SHALL 确认映射来自 `status-tag-registry.ts` 而非页面内联定义
### Requirement: 嵌套弹窗与子弹窗内列表风格一致
`web-admin` 中所有界面(含主列表页、编辑弹窗、查看弹窗、子弹窗、侧栏详情面板等)内的 `el-table` 列表 SHALL 与标准列表页(如 `#/demo/unit`、`#/demo/order`)保持一致的表格视觉规格,**不得**因处于弹窗或嵌套区域而缩小字号、压缩行高或改用 `size="small"` 等紧凑样式。
具体规则:
1. **字号与行高**:表格正文 SHALL 使用规范默认 **14px** 字号与标准行高(与 `.list-table` / 主列表一致);SHALL NOT 为弹窗内表格单独设置更小 `font-size` 或通过 `size="small"` / `size="mini"` 缩小表格。
2. **表格类名**:业务列表(含弹窗内嵌列表)SHALL 使用 `.list-table`(可与 `.inner-table` 等场景类叠加),以复用全局表格 Token 与行高,而非在 scoped 样式中覆盖为紧凑尺寸。
3. **只读查看**:查看详情、任务详情等只读场景中的子列表 SHALL 以纯文本/Tag 展示数据,SHALL NOT 保留可编辑控件;同时 SHALL NOT 隐藏操作列后改用更小表格尺寸补偿空间。
4. **高度**:弹窗内列表可通过 `height` / `max-height` 控制可视区域高度以便滚动,但 SHALL NOT 以缩小字体或行高换取更多行展示。
#### Scenario: 任务详情弹窗内嵌列表
- **WHEN** 用户在生产任务「查看详情」弹窗中查看原材料、产出或步骤详情中的试剂/耗材等子列表
- **THEN** 表格字号与行高 SHALL 与主列表页一致(14px 标准规格)
- **THEN** 只读模式下 SHALL NOT 显示添加、删除等编辑操作按钮
#### Scenario: 新增弹窗内嵌业务列表
- **WHEN** 在 `el-dialog` 或子弹窗中新增 `el-table` 展示业务数据
- **THEN** 实现 SHALL 使用 `.list-table` 且 SHALL NOT 设置 `size="small"` 或自定义更小字号/行高以适配弹窗
#### Scenario: Code review 弹窗内表格
- **WHEN** PR 在弹窗、侧栏详情或嵌套面板中新增或修改 `el-table`
- **THEN** 审查者 SHALL 确认未使用 `size="small"` / 紧凑字号覆盖,且列表风格与主列表页一致
### Requirement: web-admin 界面模式索引与参考页选型
`web-admin` 新增或改造业务页面时,开发者 SHALL 在编码前按 **功能特征 → 模式编号(P01–P27)→ 参考页** 完成界面选型;模式定义、决策表、各模式标准写法与全站页面映射的**详细说明**以 `docs/web-admin-ui-pattern-index.md` 为权威索引,本 Requirement 给出 SHALL 级门禁与摘要。
**选型流程(SHALL 按序执行):**
1. 对照下方「功能特征 → 模式」决策表或索引文档 §二,确定模式编号(如 P01、P12)。
2. 打开该模式对应的 **Demo 模板页** 或 **业务参考页**(Hash + 源码路径见索引文档 §三)。
3. 复制页面骨架与交互模式,替换 API、字段、`menuCode`;**SHALL NOT** 从零设计布局或另起一套类名习惯。
4. 枚举字段 SHALL 注册至 `status-tag-registry.ts`(见本 spec「状态标签注册表」Requirement)。
**官方 Demo 模板(永远优先对照,SHALL 优先于业务页):**
| 用途 | Hash | 源码 |
|------|------|------|
| 标准列表 CRUD | `/#/demo/unit` | `web-admin/src/views/system/unit/index.vue` |
| 复杂业务列表 | `/#/demo/order` | `web-admin/src/views/system/order/index.vue` |
| 商品类 + 查看弹窗 | `/#/demo/product` | `web-admin/src/views/system/product/index.vue` |
| 树形主数据 | `/#/demo/category` | `web-admin/src/views/system/category/index.vue` |
| 用户类 CRUD | `/#/demo/user` | `web-admin/src/views/system/user/index.vue` |
| 工作台说明 | `/#/demo/console` | `web-admin/src/views/dashboard/console/index.vue` |
**功能特征 → 模式编号(决策表):**
| 功能特征 | 模式 | 首选参考 |
|----------|------|----------|
| 单表主数据,增删改查 + 分页 + 关键词筛选 | **P01** | `/#/demo/unit` → `basic-info/customer` |
| 列表多状态,操作按钮随状态变化 | **P02** | `/#/demo/order` → `cell-storage/protocol-audit` |
| 树形结构,父子级联,无分页 | **P03** | `/#/demo/category` → `system/dept` |
| 左侧组织/分类树 + 右侧分页列表 | **P04** | `/#/demo/user` → `system/user` |
| 同页上下两块表(类型 + 明细) | **P05** | `system/dict` |
| 主表列表 + 弹窗内维护子表 | **P06** | `basic-info/collection-region` |
| 批量生成编号/编码 + 下发/收回 | **P07** | `basic-info/sample-code` |
| 列表顶部 Tab 按状态切换(非路由 Tab) | **P08** | `cell-storage/payment-info` |
| 只读查看,字段分 Section(≤20 字段) | **P09** | `SystemViewDialog` → `system/express` |
| 只读查看,多 Tab/KPI/审核历史/附件 | **P10** | `ProtocolViewDialog` / `DonationViewDialog` |
| 简单审核/状态变更(单表单 ~560px) | **P11** | `ProtocolAuditDialog` |
| 多分区大表单(Tab 锚点 + 行内子表) | **P12** | `ProtocolFormDialog` / `DonationFormDialog` |
| 财务/业务确认(只读明细 + 上传附件) | **P13** | `cell-storage/payment-confirm` |
| 主表 + 嵌套配置大弹窗(子资源 CRUD) | **P14** | `basic-info/insurance-company` |
| 主表 + 多 Tab 子模块(渠道/协议等) | **P15** | `pilot-service/channel` |
| 三栏:资源库 \| 步骤列表 \| 步骤详情 | **P16** | `production/process-route` |
| 工单列表 + 派发/执行弹窗链 | **P17** | `production/production-work-order` |
| 任务执行(虚拟步骤 + 步骤状态机) | **P18** | `TaskExecutionDialog` + `useTaskStepEditor` |
| 主数据 + append-to-body 选择器弹窗 | **P19** | `production/process` |
| 库存主数据 + 入库/出库 + 流水只读页 | **P20** | `production/consumable` + `material-stock-log` |
| 质检工单/任务(配置检测项) | **P21** | `quality-inspection/qc-work-order` |
| 审核列表 + 专用审核弹窗 | **P22** | `quality-inspection/storage-release-review` |
| 接收/登记(关联协议样本) | **P23** | `quality-inspection/sample-reception` |
| 工作流配置 / 待办 / 审批 | **P24** | `system/workflow/*` |
| 独立详情页(非弹窗) | **P25** | `system/user/detail` |
| 角色/权限扩展(菜单树/数据范围) | **P26** | `system/role` |
| 衍生列表(跳转主业务查看弹窗) | **P27** | `cell-storage/storage-cert` |
**各模式 SHALL 级要点(摘要):**
- **P01**:`filter-block` → `list-toolbar`(已选 N 项在前)→ `list-table stripe` → 分页;`dialog-form-layout` 编辑 + `SystemViewDialog` 查看。
- **P02**:在 P01 基础上增加状态筛选、操作列多按钮、业务弹窗(审核/状态变更)。
- **P03**:`el-table` 树形、无分页;路径展示遵循「树形主数据展示规则」Requirement。
- **P04**:左 `el-tree` + 右 P01 列表;组织全路径用 `tree-path.ts`。
- **P06**:主从平铺 + 子 API;**SHALL NOT** 误用树形主数据 UI。
- **P07**:`/batch-generate`、`/batch-issue`、`/batch-revoke`;批量请求超时 SHALL ≥ 120s。
- **P08**:`region-tabs` 切换列表筛选;各 Tab 可有不同批量操作。
- **P09**:查看 SHALL 使用 `SystemViewDialog`;状态字段传 `tagType`。
- **P10**:复杂查看 SHALL 使用独立 `*ViewDialog` 组件;与编辑弹窗 **同 Tab/section 命名**;只读附件复用 `ProtocolAttachmentReadonly` 等。
- **P11/P22**:轻量审核弹窗 width ~560px;结论字段用 registry Tag。
- **P12**:重表单 SHALL 拆为独立 `.vue` 组件;`DialogFormBody` + `detail-tabs` + `scroll-to-error`;附件卡片化布局遵循本 spec 附件 Requirement。
- **P16**:三栏布局 SHALL 使用 `_route-config.scss` 的 `.route-config-*` 类;新三栏场景 SHALL 复用 `useTaskStepEditor` + `StepDetailPanel`,**SHALL NOT** 复制 `process-route` 内联逻辑。
- **P17/P18**:工单/任务流 SHALL 对齐 `production-work-order` 弹窗链与 composable 模式。
- **P27**:衍生列表查看 SHALL 打开主业务 ViewDialog(如 `ProtocolViewDialog`),**SHALL NOT** 重复实现完整查看 UI。
**维护约定:**
- 新增页面落地后,SHALL 在 `docs/web-admin-ui-pattern-index.md` §四补充「页面 → 模式」映射。
- 出现现有 P01–P27 无法覆盖的新交互形态时,SHALL 先在索引文档新增模式编号与参考页,再更新本 Requirement 决策表。
#### Scenario: 新增标准主数据页
- **WHEN** 新增单表 CRUD 主数据页(如供应商、配置项)
- **THEN** 开发者 SHALL 选定模式 **P01**,以 `/#/demo/unit` 或 `basic-info/customer` 为骨架参考
- **THEN** PR 描述或设计说明中 SHALL 注明所选模式编号与参考页路径
#### Scenario: 新增复杂业务或工单页
- **WHEN** 新增含多状态操作、工单派发、任务执行或三栏编辑的页面
- **THEN** 开发者 SHALL 从 P02、P16、P17、P18、P21 等决策表中选定模式,并对照索引文档 §三对应参考页实现
- **THEN** SHALL NOT 仅参照 P01 标准列表强行裁剪复杂交互
#### Scenario: 新增重表单或专用查看弹窗
- **WHEN** 新增字段多、需 Tab 分区或 KPI 的业务表单/查看
- **THEN** 编辑 SHALL 采用 **P12**(独立 FormDialog 组件);查看字段极多 SHALL 采用 **P10**(专用 ViewDialog)
- **THEN** SHALL NOT 在 `index.vue` 内堆叠超过 500 行表单/查看模板
#### Scenario: Code review 界面模式选型
- **WHEN** PR 新增或大幅改造 `web-admin` 业务页面
- **THEN** 审查者 SHALL 确认 PR 注明模式编号(Pxx)及参考页
- **THEN** 页面骨架、工具栏顺序、弹窗类名与参考页一致;若偏离 SHALL 有书面理由
#### Scenario: 索引文档与 OpenSpec 同步
- **WHEN** 新增 P28 或更高级别模式,或全站页面映射变更
- **THEN** 维护者 SHALL 同步更新 `docs/web-admin-ui-pattern-index.md` 与本 Requirement 决策表
### Requirement: 金额与单价千分位输入与展示
金额、单价等可编辑输入字段 SHALL 使用 `AmountInput` 组件(路径 `@/components/common/amount-input/index.vue`),满足以下行为:
| 时机 | 行为 |
|------|------|
| 聚焦输入中 | 纯数字展示,无千分位逗号,如 `1234567.89` |
| 失焦后 | 千分位展示,如 `1,234,567.89` |
| 再次聚焦 | 自动去掉逗号,恢复纯数字便于修改 |
| 提交/API | `number` 类型,不受逗号影响 |
金额、单价等只读展示(列表列、查看弹窗、合计文案)SHALL 使用 `formatMoneyDisplay` 工具函数(路径 `@/utils/money-format.ts`),整数部分千分位、小数位默认 2 位。
业务 `.vue` 文件 SHALL NOT 复制千分位正则或 `addThousandSeparators` 等函数实现;SHALL 统一引用 `@/utils/money-format.ts`。
不在范围内:数量、份数、排序号、百分比系数等非金额字段(仍用 `el-input-number`)。
#### Scenario: 新增金额输入字段
- **WHEN** 新增可编辑的金额或单价字段
- **THEN** SHALL 使用 `<AmountInput v-model="..." />` 组件
- **THEN** SHALL NOT 使用 `<el-input-number>` 或页面内联 focus/blur 千分位逻辑
#### Scenario: 新增金额只读展示
- **WHEN** 在列表列、查看弹窗、合计文案中展示金额
- **THEN** SHALL 使用 `formatMoneyDisplay(v, 2)` 格式化
- **THEN** 货币符号 `¥` 仍在模板侧拼接,不内置于函数
#### Scenario: Code review 金额展示
- **WHEN** PR 涉及金额/单价字段展示
- **THEN** 审查者 SHALL 确认使用 `AmountInput`(可编辑)或 `formatMoneyDisplay`(只读),拒绝页面内复制千分位实现