Files
zlwu b220298f89 chore: 清理历史资料并规范工程目录与开发规则
为减少仓库噪声和重复规则,整理当前开发入口并移除已废弃资料。

- 精简根 AGENTS,补充四个应用规则与 README,统一规范入口
- 归整部署文件、SOP 原稿和参考资源,修正迁移后的脚本路径
- 取消虚拟环境、工具状态、业务导出及备份的跟踪,本机所需原件保留
- 移除停用的 graphify 配置、脚本与产物
- 删除历史业务文档、执行计划、OpenSpec 业务变更和移动 UI 原型
- 清理失效引用,历史细胞产品导入改为必须显式指定 --file

验证:git diff --cached --check、OpenSpec 严格校验、导入参数隔离检查 3/3 通过;
迁移文件完整性和现行链接已核对。未执行部署、业务数据导入或服务启停。
2026-10-29 23:08:29 +08:00

61 KiB
Raw Permalink Blame History

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: 小程序以移动端规范为视觉权威

mobile-h5 工程的界面 SHALL 以 docs/miniprogram-ui-spec-sanqiang.md 为权威来源;已实现页面和共享组件作为结构与交互参考,规范与实现差异按应用 AGENTS.md 核实;品牌绿、大麦黄与中性色 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: 弹窗 label-top 间距与字重

web-admin 中 label-position="top" 的弹窗表单 label SHALL:与输入控件间距 4px(padding-bottom: 4px)、字重 400、height: auto(SHALL NOT 沿用左右布局 label 的 36px 行高)。使用 .form-grid / .form-grid-2~.form-grid-5 时,栅格内 el-form-item SHALL margin-bottom: 0,纵向间距交给 grid gap。权威实现:el-ui.scss(label-top)、app.scss(栅格与 dialog-form-layout 兜底)。

例外:物资/试剂/耗材/质检物料等盘点、库存出入弹窗中的紧凑 11px label 可保留局部样式。

Scenario: 标准 CRUD 弹窗 label

  • WHEN 打开采用 label-position="top" 的新增/编辑弹窗
  • THEN label 与输入框间距 SHALL 为约 4px,字重 SHALL 为常规(400),且 SHALL NOT 呈现左侧 label 的 36px 行高空白

Scenario: 栅格表单行距

  • WHEN 弹窗表单字段置于 .form-grid 或 .form-grid-2~.form-grid-5 内
  • THEN 各 el-form-item SHALL NOT 再叠加默认 margin-bottom(约 18px),行距 SHALL 由栅格 gap 决定

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 仅在「查看」类按钮可见时触发对应查看函数,其余状态下可忽略或调用对应主操作函数。
  4. 弹窗样式:查看弹窗 SHALL 使用 SystemViewDialog 共享组件(src/components/business/system-view-dialog/index.vue),样式对齐待办中心「查看详情」弹窗(无标题栏、hero 渐变头图 + 图标、KV grid 字段展示、关闭按钮)。
  5. 字段内容:查看弹窗 SHALL 展示该行记录的所有业务字段(含状态映射、字典翻译),不展示 id、del_flag 等技术字段。长文本/备注字段 SHALL 使用 span: true 跨两列展示。
  6. 区块标题样式:查看弹窗中各区块标题(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 保留作为手动触发入口。

实现模式

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)

<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 与参考页一致,核心片段:

.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 页图标约定一致

标准写法示例

<!-- 工具栏 -->
<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>
// <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"> 或纯文本。

标准写法:

<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>

多状态映射标准写法(使用状态标签注册表):

<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>
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 接口扩展(已实现):

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
}

标准写法:

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> 展示。

标准写法:

<dd>
  <el-tag :type="acceptConclusionTagType(row.acceptConclusion)" size="small">
    {{ acceptConclusionLabel(row.acceptConclusion) }}
  </el-tag>
</dd>
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。

注册表结构(已实现):

/** 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)

页面使用方式:

import { protocolStatusLabel, protocolStatusTagType } from '@/utils/status-tag-registry'
<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(只读),拒绝页面内复制千分位实现