107 KiB
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 形成明显视觉区分:子区域标题使用深色文字 + 竖条,字段标签使用辅助灰色、无竖条。
此规则适用于以下场景的区块标题:
- 弹窗内分组标题:如编辑弹窗中的「基本信息」「参数配置」等将表单字段分组的标题。
- 多栏选择器的列标题:如「选择部门」「选择用户」等
data-scope-col-title类标题。 - 已选摘要的分组标题:如「已选部门」「已选用户」等
data-scope-summary-heading类标题。 - 查看弹窗的区块标题:
SystemViewDialog的 section title(h3)。 - 设计器/工具面板的分组标题:如「节点类型」「属性」等
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 满足:
- 水平栅格:默认使用 两列 栅格(
.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)。 - 窄屏回退:视口或表单区过窄时,栅格 SHALL 回退为更少列或单列(与全局
app.scss中@media (max-width: 900px)、(max-width: 560px)规则一致)。 - 定位:弹窗 SHALL 相对视口水平与垂直居中(依赖
ElConfigProvider的dialog.alignCenter及 Element Plus 默认遮罩层居中行为)。 - 高度与滚动:使用
.dialog-form-layout类名的弹窗壳层 SHALLmax-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 名
Requirement: Web 管理端侧栏菜单须同步注册静态路由
在默认 VITE_ACCESS_MODE=frontend 下,侧栏菜单与可访问路由来自 web-admin/src/router/modules/sqxp.ts(asyncRoutes),不是仅写入数据库 sys_menu 即可展示。完整注册流程、字段对齐、超管/非超管可见性及 sys_menu + sqxp.ts + backend/app/api/menu.py 三件套同步规则,SHALL 以 permission-conventions 中 「Web 管理端菜单注册与侧栏展示(三件套同步)」 为准。
Scenario: 新增 CRM 类侧栏菜单项
- WHEN 为 CRM 等业务模块新增「客户管理」「公海池」等侧栏入口
- THEN 除
docs/sql/*-menu.sql外,SHALL 在sqxp.ts的父级children与menu.py的STATIC_MENU_TREE同步追加节点;meta.menuCodeSHALL 等于sys_menu.perms;子路由pathSHALL 为相对路径(如customer、customer-pool)
Scenario: 菜单变更 PR 评审(UI 视角)
- WHEN PR 仅包含
docs/sql菜单脚本而无sqxp.ts变更 - THEN 在 frontend 模式下 SHALL 判定为不完整,要求补全静态路由后再合并
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中注册映射条目,再在业务代码中使用(参见反例 EC-UI-002)
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 使用单短横线,非双下划线)提供固定列样式。
例外:遵循下文「列表主标识列链接查看与编辑删除外置」、且列表动作仅为常规「编辑」「删除」的标准 CRUD 页 SHALL NOT 设置操作列,此时本规则不适用。
Scenario: 新增系统管理列表页
- WHEN 在系统管理模块下新增列表页面(如用户管理、角色管理等),且页面保留操作列
- THEN 表格操作列 SHALL 设置
fixed="right",宽度按按钮数设置:≤ 2 按钮用min-width="260",3 按钮用min-width="320",≥ 4 按钮用min-width="400"
Scenario: 标准 CRUD 列表无外置操作列
- WHEN 新增列表页仅含常规「编辑」「删除」,并采用工具栏 + 查看弹窗 Hero 外置模式
- THEN 表格 SHALL NOT 包含
label="操作"列 - THEN 本「操作列固定右侧」规则 SHALL NOT 适用
Scenario: Code review 表格列
- WHEN PR 包含新增或修改的
el-table-column且label="操作" - THEN 审查者 SHALL 确认该列包含
fixed="right"属性,且宽度与按钮数量匹配(2 按钮 260px,3+ 按钮 320px) - THEN 若页面属于标准 CRUD + 查看弹窗且仅编辑/删除,审查者 SHALL 要求移除外置至工具栏与
#hero-actions,而非保留操作列
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 列不变。
规则:
- 业务数据列、操作列 SHALL 使用
min-width设定基础宽度,SHALL NOT 使用width。 min-width值即为该列的最小可读宽度,也是等比分配的权重基数。el-table本身的width属性(如弹窗内嵌表格的固定总宽)不受此规则约束。- 例外(固定窄列):
type="selection"(多选框)与type="index"(序号)列 SHALL 使用固定width(建议多选约48~55、序号约60~68),SHALL NOT 使用min-width,避免宽屏下被等比拉得过宽。公共组件ListIndexColumnSHALL 遵循同一例外。
Scenario: 新增列表页
- WHEN 新增列表页包含
el-table - THEN 业务数据列与操作列的宽度属性 SHALL 使用
min-width,SHALL NOT 使用width;多选列与序号列 SHALL 使用固定width
Scenario: 浏览器窗口宽度变化
- WHEN 用户调整浏览器窗口宽度
- THEN 业务数据列与操作列宽度 SHALL 按
min-width设定的基础比例等比伸缩;多选列与序号列宽度 SHALL 保持固定,不随视口加宽而膨胀
Scenario: Code review 表格列宽
- WHEN PR 包含新增或修改的业务数据列 / 操作列
el-table-column且使用width属性 - THEN 审查者 SHALL 要求改为
min-width
Scenario: Code review 多选与序号列宽
- WHEN PR 包含
type="selection"或type="index"的el-table-column且使用min-width - THEN 审查者 SHALL 要求改为固定
width
Requirement: 侧边栏菜单图标层级规则
web-admin 左侧菜单区域的图标展示 SHALL 遵循以下层级规则:
- 一级菜单(顶层菜单项)SHALL 在菜单名称前展示图标(
meta.icon)。 - 二级及以下菜单(子菜单项)SHALL NOT 展示图标,仅展示文字名称。
- 双列菜单模式(
DualMenu)左侧一级图标栏 SHALL 为每个一级菜单项渲染图标,与侧边栏一级规则一致。 - 水平菜单(
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 遵循:
- 主标识列链接:承载业务主标识的列(通常为「名称」「单号」「编号」「主题」等,如
productName、customerName、orderNo、applyCode、planNo)SHALL 使用<el-link type="primary">包裹展示文案,点击 SHALL 调用打开只读详情的同一函数(如handleView(row)、openView(row)、handleDetail(row)等,与@row-dblclick共用)。 - 操作列去重(查看):当主标识列已提供上述链接查看入口时,操作列 SHALL NOT 再包含仅用于打开同一只读详情/查看弹窗的「查看」「查看详情」等按钮。
- 编辑/删除外置(标准 CRUD):对于列表行级动作仅含常规「编辑」「删除」(及同类 CRUD,无审批、状态变更、安排运输等行内复合业务按钮)的标准列表页,表格 SHALL NOT 设置「操作」列;「编辑」「删除」SHALL 外置至以下两处:
- 列表工具栏:位于「已选 N 项」与「新增」主按钮(
type="primary"/btn-main)之后;编辑 SHALL 在selectedRows.length === 1时可用,点击 SHALL 对选中行调用与行内编辑相同的 handler(如handleUpdate(selectedRows[0]));删除 SHALL 支持批量删除,在selectedRows.length === 0时禁用,且 SHALL 复用既有创建人校验(如allRowsDeletableByCreator/canDeleteByCreator+CREATOR_DELETE_TIP);两按钮 SHALL 使用btn-ghost描边样式,SHALL NOT 与「新增」同列使用主色实心。 - 查看弹窗 Hero:通过
SystemViewDialog的#hero-actions插槽,在 Hero 区右侧展示「编辑」「删除」,对当前查看行执行与列表相同的 handler;删除成功后 SHALL 关闭查看弹窗并刷新列表;从详情进入编辑并保存成功后,若查看弹窗仍打开,SHALL 刷新当前详情数据。
- 列表工具栏:位于「已选 N 项」与「新增」主按钮(
- 操作列保留场景:存在审批、状态变更、安排运输、复合主操作等行级、状态依赖业务按钮的列表页 SHALL 保留操作列,并仍遵循「表格操作列固定在右侧」;此类页面 MAY 在
#hero-actions放置通用动作,但 SHOULD 避免与操作列完全重复同一入口。 - 行双击:
el-tableSHALL 继续绑定@row-dblclick,双击行 SHALL 触发与主标识列链接相同的查看函数。 - 条件按钮场景:当操作列按钮随状态/Tab 变化(如待确认 Tab 显示「确认收款」,已确认 Tab 显示「查看确认信息」),
@row-dblclickhandler SHALL 仅在「查看」类按钮可见时触发对应查看函数,其余状态下可忽略或调用对应主操作函数。 - 复合主操作例外:主操作本身为复合业务入口、不仅打开只读弹窗的按钮(如订单「查看/运营」、待办「审批」)SHALL NOT 因本规则被删除;此时仍 SHALL 在主标识列提供链接以打开只读查看/详情。
- 工具栏查看:列表工具栏中的「查看」类按钮在表格主标识列已提供链接时 SHALL NOT 保留;单选后查看 SHALL 改由点击行内主标识链接或双击行触发。
- 弹窗组件与字段:查看弹窗 SHALL 优先使用
SystemViewDialog(web-admin/src/components/business/system-view-dialog/index.vue);样式对齐 Hero 渐变头图 + 分区 KV 信息区 + 底部「关闭」按钮(详见下文「查看弹窗 KV 布局与组件选型」)。KV 信息区 SHALL 使用ViewKvGrid(上下堆叠栅格)或ViewKvTable(多组多列表格);SHALL NOT 使用el-descriptions或历史.dlg-kv布局承载列表行查看详情。查看弹窗 SHALL 展示该行记录的所有业务字段(含状态映射、字典翻译),不展示id、del_flag等技术字段。 - 区块标题样式:查看弹窗中各区块标题 SHALL 与字段标签形成明显视觉区分。区块标题 SHALL 使用深色文字(
#1e293b)配合左侧主色竖条装饰(border-left: 3px solid #10b981),字号 14px、font-weight: 600;字段标签 SHALL 为 13px、#64748b、font-weight: 400。
权威参考:
- 列表无外置操作列 + 工具栏编辑/删除:
web-admin/src/views/system/user/index.vue - 主标识列链接 + 工具栏编辑/删除 + 查看弹窗 Hero 编辑/删除:
web-admin/src/views/crm/product/index.vue - 查看弹窗 Hero 动作区:
web-admin/src/views/crm/customer/CustomerDetailDialog.vue(#hero-actions)
Scenario: 新增标准 CRUD 列表页
- WHEN 在
web-admin新增带只读查看弹窗的标准 CRUD 列表页,且行级动作仅为编辑/删除 - THEN 主标识列 SHALL 使用
el-link type="primary"触发handleView - THEN 表格 SHALL NOT 包含「操作」列
- THEN 工具栏 SHALL 在「新增」之后提供「编辑」(单选可用)与「删除」(批量可用)按钮
- THEN
SystemViewDialogSHALL 通过#hero-actions提供「编辑」「删除」 - THEN
el-tableSHALL 绑定@row-dblclick="handleView"(或等价 handler)
Scenario: 现有页面补充查看弹窗
- WHEN 为现有列表页补充查看弹窗,且行级动作仅为编辑/删除
- THEN SHALL 在主标识列添加
el-link与@row-dblclick双击事件,两者调用同一个查看函数 - THEN SHALL NOT 在操作列再添加「查看」按钮
- THEN SHOULD 移除「操作」列,将「编辑」「删除」迁移至工具栏与
#hero-actions
Scenario: 保留操作列的复杂业务列表
- WHEN 列表页存在审批、状态变更、安排运输等行级状态依赖按钮
- THEN SHALL 保留操作列并设置
fixed="right" - THEN 主标识列仍 SHALL 提供
el-link查看入口,操作列 SHALL NOT 包含冗余「查看」按钮
Scenario: Code review 列表查看与 CRUD 入口
- WHEN PR 修改列表页面且页面包含
el-table与只读查看能力 - THEN 审查者 SHALL 确认主标识列含
el-link查看入口 - THEN 审查者 SHALL 确认
el-table绑定了@row-dblclick事件 - THEN 若为标准 CRUD(仅编辑/删除),审查者 SHALL 确认无「操作」列,且工具栏与
#hero-actions均提供编辑/删除 - THEN 审查者 SHALL 确认操作列无冗余「查看」/「查看详情」按钮(复合主操作例外除外)
Requirement: 查看弹窗 KV 布局与组件选型
web-admin 列表行【查看】弹窗及同类只读详情弹窗的键值(KV)信息区 SHALL 采用下列两种规范布局之一,SHALL NOT 使用 Element Plus el-descriptions,SHALL NOT 使用已废弃的 .dlg-kv 四列「label|value|label|value」横向布局,SHALL NOT 在业务页手写无统一样式的 <table> 承载主体 KV。
| 布局 | 说明 | 默认场景 |
|---|---|---|
| 上下堆叠栅格 | label 在上、value 在下,多列 grid | 字段较多、需 3~4 列密度;SystemViewDialog 默认 |
| 多组多列表格 | 每行左 label(th)、右 value(td);短字段一行可排多组 |
字段较少(通常 ≤10)、需表格对齐感;如报销备用金申请「基础信息」 |
权威视觉细节见 docs/web-admin-ui-spec-fresh-mint.md §9.5 / §9.8;交互 Demo 见 docs/fresh-mint-view-wide-dialog-demos.html(上下堆叠见主区;多组多列表格见 #table-section,模式 M~R)。
1. 组件选型
| 场景 | 组件 | 路径 |
|---|---|---|
| 门户 / CRM / 系统管理等(默认) | SystemViewDialog |
web-admin/src/components/business/system-view-dialog/index.vue |
| KV 栅格渲染(上下堆叠,被上述组件复用) | ViewKvGrid |
web-admin/src/components/business/system-view-dialog/ViewKvGrid.vue |
| KV 表格渲染(多组多列,被上述组件复用) | ViewKvTable |
web-admin/src/components/business/system-view-dialog/ViewKvTable.vue |
报销管理(bx 模块,Hero 自定义壳) |
BxViewKvGrid / BxViewKvTable |
web-admin/src/components/bx/base/BxViewKvGrid.vue、BxViewKvTable.vue(内部分别复用 ViewKvGrid / ViewKvTable) |
- 新增业务查看弹窗 SHALL 优先
SystemViewDialog+ViewField[]/ViewSection[]。 - 字段极多、需 Tab / KPI / 分栏的复杂查看 SHALL 采用界面模式 P10(独立
*ViewDialog,内部仍复用ViewKvGrid/ViewKvTable或SystemViewDialog的 section 结构)。 bx模块保留.dlg-view-hero等既有 Hero 壳时,KV 区 SHALL 使用BxViewKvGrid或BxViewKvTable,SHALL NOT 继续手写.dlg-kv或裸<table>。
2. 弹窗宽度与默认列数
SystemViewDialog默认宽度 SHALL 为1040px(widthprop 可覆盖;合同/报价等宽表场景可用calc(100vw - Npx)或更大固定宽)。- 上下堆叠栅格默认列数 SHALL 为 4 列(
gridColumns: 4);紧凑模式(compact)降为 2 列。 - 多组多列表格 SHALL 在
1040px弹窗内使用;少字段(≤8)可选用720px~920px(见 Demo Q/R)。 bx模块.dialog-view-xl宽度 SHALL 与上述一致(min(1040px, calc(100vw - 32px)))。
2.5 查看弹窗 KV 字号(14px / 13px 模式)
查看弹窗内所有 KV 信息区 SHALL 统一采用 value 14px / label 13px 字号模式,与全站列表正文(14px)对齐,label 略小一级以区分层级:
| 元素 | 字号 | 字重 | 说明 |
|---|---|---|---|
value(ViewKvGrid 的 dd、ViewKvTable 的 td) |
14px | 600 | 字段值正文 |
label(ViewKvGrid 的 dt、ViewKvTable 的 th、.view-field-label) |
13px | 400(表格 th 可为 500) |
字段名 / 次级分组标签 |
- 实现权威来源:
ViewKvGrid.vue、ViewKvTable.vue,以及app.scss内:is(.view-kv, [class*='-view-kv'], .pl-plan-kv)全局规则。 SystemViewDialog的compact模式 SHALL 降为 value 13px / label 12px,以保持紧凑弹窗内的层级差。- SHALL NOT 在业务页对 KV 单元单独覆写为其它字号(除上述 compact 降级外)。
3. KV 单元格结构(上下堆叠栅格)
每个字段渲染为 <dl> 内的 .view-kv 单元:
- dt(label):上,13px,
#64748b,font-weight: 400 - dd(value):下,14px,
font-weight: 600,word-break: break-word - SHALL NOT 在同一 grid cell 内使用左右并排的 label|value(历史
el-descriptions/.dlg-kv .k/.v模式)
4. 四列栅格跨列规则
栅格容器为 .view-kv-grid.view-kv-grid--cols-4,gap: 12px 18px。跨列逻辑 SHALL 由 web-admin/src/utils/view-field-layout.ts 与 ViewField 属性共同决定:
| 类型 | 每行个数 | 触发条件 | CSS class |
|---|---|---|---|
| 短值 | 4 个/行 | 默认;显示文本 < 17 字且非长 label 启发式 | (无 span) |
| 长值 | 2 个/行 | long: true 或 span: true;或自动:文本 ≥17 字、含 / 路径、label 匹配备注/地址/说明等 |
view-kv-span-2 |
| 整行 | 1 个/行 | fullSpan: true 或 span: 'full';或自动:文本 ≥80 字 |
view-kv-span-full |
常量:VIEW_KV_LONG_MIN_LEN = 17,VIEW_KV_FULL_MIN_LEN = 80(见 view-field-layout.ts)。
响应式:SHALL 在 ≤1100px 降为 3 列、≤900px 降为 2 列、≤640px 降为 1 列(ViewKvGrid.vue 内 media query);长值在单列下 SHALL 占满整行。
5. ViewField 定义约定
类型定义见 web-admin/src/components/business/system-view-dialog/types.ts。
interface ViewField {
label: string
value?: string | number | boolean | null
items?: string[] // 无编号列表,与 value 二选一
span?: boolean | 'full' // true=长值(2列);'full'=整行
long?: boolean // 显式长值,等同 span:true
fullSpan?: boolean // 显式整行
tagType?: TagType // 状态/类型 → el-tag
money?: boolean // 金额红色 .money-display
}
- 空值 SHALL 展示为
—(formatViewValue)。 - 字典/状态 SHALL 预先翻译为 label 并视情况设
tagType(见「查看弹窗中编码字段展示为可读标签」「状态、结论与类型字段使用 Tag 标签展示」)。 - 金额 SHALL 设
money: true并使用¥前缀(见「价格字段展示规范」)。 long: false:显式禁用view-field-layout.ts自动长值检测;SHALL 用于ViewKvTable的 quad compact section(见 §8「紧凑八列模式」),避免字段因值 ≥17 字被拆成单行一组。
6. 弹窗骨架(SystemViewDialog)
标准只读查看弹窗 SHALL 按以下顺序组织:
- Hero(
.view-hero):图标 + 主标题 + 可选meta/code+ 状态 Tag;标准 CRUD 列表页的「编辑」「删除」SHALL 通过#hero-actions插槽置于 Hero 区右侧(见「列表主标识列链接查看与编辑删除外置」),Footer 仍仅保留「关闭」类按钮 - 分区(
.view-section):区块标题 +ViewKvGrid/ViewKvTable或具名插槽(子表、附件、审批流等) - Footer:仅「关闭」类按钮(带
Close图标),SHALL NOT 在查看态混入提交语义
子表、附件、审批历史等 SHALL 作为独立 section 或 slot 置于 KV 区之后,SHALL NOT 嵌入 KV 栅格 cell 或 KV 表格 <td>。
7. 模块辅助工具(推荐)
| 模块 | 工具 | 用途 |
|---|---|---|
| CRM | web-admin/src/utils/crm/view-fields.ts |
crmField / crmLongField / crmDictField / crmTagField 构建 ViewField;宽表「基本信息」八列 compact 见各 *DetailDialog 内 viewQuadTableSection(如 QuoteDetailDialog.vue) |
| 报销 bx | web-admin/src/utils/bx/view-kv.ts |
bxViewKv / bxViewKvLong / bxViewKvMoney |
8. KV 表格模式(多组多列)
当业务选用多组多列表格展示 KV 时,SHALL 使用 ViewKvTable(或 BxViewKvTable),数据源仍为 ViewField[];跨行/长值逻辑 SHALL 复用 web-admin/src/utils/view-field-layout.ts(isViewFieldLong / isViewFieldFullSpan / buildViewKvTableDualRows / buildViewKvTableTripleRows / buildViewKvTableQuadRows)。
模式(mode prop / ViewSection.tableMode)
| mode | 每行组数 | 列结构 | 典型场景 |
|---|---|---|---|
pair |
1 组 | th + td(两列) |
少字段、需逐行阅读;Demo Q |
dual |
2 组 | th + td × 2(四列) |
中等字段量;Demo N / R |
triple |
3 组 | th + td × 3(六列) |
少字段高密度;Demo 扩展;参考 bx/advance/apply 基础信息 |
quad |
4 组 | th + td × 4(八列) |
宽弹窗(≥1680px 或 1040px+)、字段较多且需表格对齐;CRM 报价单/合同「基本信息」等 |
行分组规则
- 短字段 SHALL 按 mode 尽可能填满一行(
dual最多 2 组、triple最多 3 组、quad最多 4 组);末行不足时 SHALL 留空th/td占位,SHALL NOT 强行与长字段同组。 - 长值 / 整行字段(
long/fullSpan或view-field-layout.ts自动检测:显示文本 ≥17 字、含/路径、label 匹配备注/地址/说明等)SHALL 独占整行:pair为单行th+td;dual为td colspan="3";triple为td colspan="5";quad为td colspan="7"。 - 字段顺序 SHOULD 将长文本(申请原因、备注等)置于数组末尾,以便短字段先组成完整多组行;备注、条款等超长文本 SHOULD 独立 section,与「基本信息」分区展示。
紧凑八列模式(quad compact)
当 section 选用 quad(八列) 且业务要求每行尽量排满 4 组(不因字段值略长而拆成「一行仅 1 组」)时:
- SHALL 在
SystemViewDialog的ViewSection上设置fieldLayout: 'table'、tableMode: 'quad'。 - SHALL 对该 section 内所有常规短字段统一设置
long: false、fullSpan: false,以禁用view-field-layout.ts的自动长值拆行;较长内容 SHALL 在td内换行展示(ViewKvTable已支持word-break),SHALL NOT 为此单独占整行。 - SHALL NOT 在「基本信息」等 quad compact section 内混入
long: true/span: true/fullSpan: true字段;若确需整行展示,SHALL 拆至独立 section(如「报价条款」「备注」)。 - 推荐封装:在
*DetailDialog内提供辅助函数,对传入ViewField[]映射{ ...field, long: false, fullSpan: false }后返回ViewSection;参考web-admin/src/views/crm/quote/components/QuoteDetailDialog.vue的viewQuadTableSection。 - 宽表弹窗宽度 SHOULD 与八列密度匹配(如报价单查看
1680px);字段数量 SHOULD 为 4 的倍数或接受末行留空占位。
表格样式(Fresh Mint)
- 外层 SHALL 使用
.view-kv-table-wrap(圆角 8px、边框#d9e8e2)。 - 表头单元格
th(label):背景var(--table-head),13px、#64748b;SHALL 支持换行(white-space: normal、word-break: break-word、overflow-wrap: anywhere),SHALL NOT 使用white-space: nowrap导致长 label(如「实际发放金额(元)」)溢出。 - 数据单元格
td(value):14px、font-weight: 600;金额 SHALL 使用.money-display;状态/类型 SHALL 使用el-tag(ViewKvTableCell)。 triple模式 label 列宽约 10%、value 列宽约 23.33%(三组合计六列);quad模式 label 列宽约 8%、value 列宽约 17%(四组合计八列);具体比例以ViewKvTable.vue为准。
选型建议
- 字段 ≤8 且弹窗显空 → 优先
pair或triple+ 适当缩窄弹窗宽度。 - 字段 9~18 且需表格感 → 优先
dual(1040px)。 - 字段 ≥12 且宽弹窗(报价单、合同等)→ 优先
quadcompact(见上「紧凑八列模式」);「基本信息」SHALL 每行排满 4 组,SHALL NOT 因报价主题、客户名称等值略长而逐字段独占一行。 - 同一弹窗内 SHALL NOT 混用
ViewKvGrid与ViewKvTable于同一 section;不同 section 可按需分别选用(如「基础信息」用表格、「扩展信息」用栅格)。
9. 禁止与废弃
- SHALL NOT 在新代码中使用
el-descriptions实现列表行【查看】弹窗主体 KV。 - SHALL NOT 使用
.dlg-kv、.dlg-kv--triple横向 KV(web-admin/src/styles/bx/index.scss内已标记 deprecated,仅兼容旧页)。 - SHALL NOT 手写
<table class="...">实现 KV 主体区;SHALL 使用ViewKvTable/BxViewKvTable。 - 迁移存量页时 SHALL 保留 Hero / 子表 / 附件 / 审批流结构,仅替换 KV 区为
ViewKvGrid/ViewKvTable/BxViewKvGrid/BxViewKvTable/SystemViewDialog。
Scenario: 新增 CRM 列表查看弹窗
- WHEN 在 CRM 模块新增带只读查看能力的标准 CRUD 列表页(行级动作仅为编辑/删除)
- THEN 主标识列 SHALL 使用
el-link触发查看,表格 SHALL NOT 含「操作」列 - THEN 工具栏与
SystemViewDialog#hero-actionsSHALL 提供「编辑」「删除」 - THEN SHALL 使用
SystemViewDialog(或独立*DetailDialog封装该组件) - THEN KV 字段 SHALL 通过
ViewSection[]+ViewField[]定义,默认 4 列上下堆叠 - THEN SHALL NOT 使用
el-descriptions
Scenario: 报销模块查看弹窗(上下堆叠)
- WHEN 在
web-admin/src/views/bx/**实现或改造查看弹窗,且 KV 区采用上下堆叠栅格 - THEN KV 区 SHALL 使用
BxViewKvGrid(columns 默认 4) - THEN 弹窗宽度 SHALL 为 1040px 级(
.dialog-view-xl)
Scenario: 报销模块查看弹窗(多组多列表格)
- WHEN 在
web-admin/src/views/bx/**的查看弹窗中,某 section(如「基础信息」)采用多组多列表格 - THEN SHALL 使用
BxViewKvTable+ViewField[],并显式声明mode(pair/dual/triple) - THEN 长 label SHALL 在
th内换行展示,SHALL NOT 溢出列宽 - THEN 参考实现:
web-admin/src/views/bx/advance/apply/index.vue(mode="triple")
Scenario: CRM 报价单查看弹窗(紧凑八列基本信息)
- WHEN 在 CRM 报价单管理(
/#/crm/quote)打开【查看】弹窗,且「基本信息」section 采用八列表格 - THEN SHALL 使用
SystemViewDialog+fieldLayout: 'table'+tableMode: 'quad' - THEN 「基本信息」内各字段 SHALL 设置
long: false、fullSpan: false,使每行排满 4 组(八列);末行不足 4 组时留空占位 - THEN 较长字段值(报价主题、客户名称、有效期等)SHALL 在单元格内换行,SHALL NOT 单独占整行
- THEN 备注、条款等长文本 SHALL 置于独立 section(如「报价条款」),SHALL NOT 与基本信息混在同一 quad section
- THEN 参考实现:
web-admin/src/views/crm/quote/components/QuoteDetailDialog.vue(viewQuadTableSection)
Scenario: 长文本与备注字段
- WHEN 查看弹窗包含申请原因、备注、条款等长文本
- THEN SHALL 为对应
ViewField设置long: true或fullSpan: true(或依赖view-field-layout.ts自动检测) - THEN 4 列栅格下 SHALL 占 2 列或整行,SHALL NOT 与短字段同一行挤 4 个
Scenario: Code review KV 布局
- WHEN PR 新增或修改查看弹窗
- THEN 审查者 SHALL 确认 KV 区为
ViewKvGrid(上下堆叠 + 声明的gridColumns)或ViewKvTable(多组多列 + 声明的mode) - THEN SHALL 确认未使用
el-descriptions/.dlg-kv/ 手写 KV<table>作为主体字段区 - THEN 状态/金额/字典字段 SHALL 符合 Tag、
money、字典翻译等关联 Requirement - THEN 若使用
ViewKvTable,SHALL 确认长 label 可换行、长 value 在 compact quad section 内换行而非误拆行;非 compact section 中长 value 可独占整行
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: 业务字典下拉统一使用 useDictStore
web-admin 中凡选项来源于系统字典(sys_dict_type / sys_dict_data,以 dict_type 编码区分,如 crm_customer_level、crm_clue_source、crm_customer_type)的下拉类控件(el-select、el-radio-group 等),在列表筛选区、新增/编辑弹窗、子弹窗及同类表单中 SHALL 统一通过 Pinia useDictStore(web-admin/src/store/modules/dict.ts)获取选项数据,SHALL NOT 在单文件内硬编码 el-option 的 label/value,SHALL NOT 对同一 dict_type 在页面内重复调用 fetchDictDataOptions 等分散接口(全量字典已由 GET /web/admin/dict-data/all 经 dictStore.loadAllDict() 加载)。
实现约定:
- 加载时机:页面或弹窗首次需要字典时调用
await dictStore.loadAllDict()(loaded === true时 Store 内部跳过重复请求)。 - 选项绑定:使用
computed(() => dictStore.getOptions('<dict_type>'))生成选项列表,el-option的:label="opt.label"、:value="opt.value"与 Store 项一致。 - 同页一致:同一业务模块的列表筛选与新增/编辑弹窗 SHALL 对同一业务字段使用相同的
dict_type与 Store 数据源,保证筛选条件、表单提交值、列表展示编码一致。 - 只读展示:表格列、查看弹窗中字典字段 SHALL 使用
dictStore.getLabel(dictType, value)(或getTagType配合el-tag)翻译,与上文「查看弹窗中编码字段展示为可读标签」一致。 - 字典类型登记:新增业务字段所用
dict_typeSHALL 在docs/sql/中提供对应字典类型/数据脚本(或说明沿用既有类型),并在需求/表字段注释中标明编码。
参考实现:CRM 客户管理 — 列表 web-admin/src/views/crm/customer/index.vue(crm_clue_source、crm_customer_level、crm_customer_industry);表单弹窗 web-admin/src/views/crm/customer/CustomerFormDialog.vue(另含 crm_customer_type、crm_customer_park、crm_cert_type 等,与列表共用 Store)。
Scenario: 新增业务模块列表与表单弹窗
- WHEN 新增含字典下拉的列表页及对应新增/编辑弹窗
- THEN 筛选区与弹窗 SHALL 均通过
useDictStore+getOptions绑定选项 - THEN 弹窗打开时 SHALL 调用
loadAllDict()(或依赖父页已加载的全局字典) - THEN SHALL NOT 在模板中写死业务字典选项
Scenario: CRM 客户管理字典下拉
- WHEN 用户打开
#/crm/customer并打开「新增客户」弹窗 - THEN 客户类型、级别、来源、行业、园区、证件类型等下拉 SHALL 与列表筛选使用同一
dict_type编码体系,且数据来自useDictStore
Scenario: Code review 禁止硬编码字典选项
- WHEN PR 在
web-admin新增或修改含el-select的业务表单/筛选 - AND 字段语义为系统字典(状态、类型、来源、级别等)
- THEN 审查者 SHALL 确认使用
useDictStore.getOptions,SHALL 要求移除硬编码el-option及重复的 per-type 字典 API 调用
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-itemSHALL 位于.filter-grid首位
Scenario: 入库放行审核筛选区
- WHEN 用户打开质检模块「入库放行审核」列表页
- THEN 筛选区
.filter-grid中第一个字段 SHALL 为「关键词」,其后依次为协议类别、审核结论、入库状态等条件
Scenario: Code review 筛选区字段顺序
- WHEN PR 新增或修改列表页
.filter-block筛选字段 - THEN 审查者 SHALL 确认存在关键词字段时其位于
.filter-grid第一位
Requirement: 筛选区多行条件默认折叠与展开更多
web-admin 列表页筛选区(.filter-block)当检索条件在桌面端 超过一行(即 .filter-grid 默认 四列 栅格下字段数 大于 4)时,SHALL 提供「展开更多 / 收起更多」切换,默认仅展示首行常用条件(首段 .filter-grid 内 最多 4 个 el-form-item);其余条件 SHALL 放入 .filter-extra 区域,默认隐藏(v-show 或等效),由用户主动展开。视觉与结构 SHALL 对齐 docs/web-admin-ui-spec-fresh-mint.md §5.3 及 docs/fresh-mint-green.html 任务列表示例;落地参考 #/demo/order、#/crm/customer(web-admin/src/views/crm/customer/index.vue)。
结构与样式
- 首行常用条件:
.filter-block→.filter-form→ 首段.filter-grid(桌面repeat(4, 1fr),与全局app.scss一致),放置最常用的 4 个筛选项。 - 扩展条件:
.filter-extra(建议id供aria-controls引用)→ 内嵌第二段.filter-grid,承载超出首行的其余el-form-item;默认filterExpanded === false时不展示。 - 操作行:使用
.filter-actions(非.filter-actions-row单独承载按钮时):左侧.btn-link.btn-filter-toggle(链式文字按钮);右侧.filter-actions-end内「重置」「搜索」按钮(带 loading,见按钮图标规范)。 - 筛选区内部:首段
.filter-grid、.filter-extra与.filter-actions之间 SHALL NOT 使用横向分割线(border-top虚线/实线);仅用gap/padding-top/margin-top做间距(与 Fresh Mint §5.3 一致)。 - 全局样式:
.filter-extra、.filter-actions、.btn-link/.btn-filter-toggleSHALL 在web-admin/src/assets/styles/core/app.scss的.page-container/.console-container作用域内统一定义,页面 SHALL NOT 重复 scoped 覆盖。
交互与文案
- 切换按钮文案:收起态 「展开更多」;展开态 「收起更多」(SHALL NOT 与订单 demo 历史文案「展开更多条件」混用同一产品内的新页)。
- 图标:切换按钮文字后方 SHALL 携带
<el-icon>:收起态ArrowDownBold,展开态ArrowUpBold(从@element-plus/icons-vue显式 import)。 - 无障碍:切换按钮 SHALL 设置
aria-expanded(与filterExpanded同步)及aria-controls(指向.filter-extra的id)。 - 重置行为:用户点击「重置」时,除清空表单字段外,SHALL 将
filterExpanded置为false,恢复仅首行展示。 - 条件不足一行:当筛选项总数 ≤ 4(桌面四列下一行可容纳)时,SHALL 不渲染 展开/收起按钮(无
.filter-extra亦可)。
实现模式
<p class="filter-hint">…</p>
<div class="filter-grid"><!-- 首行最多 4 项 --></div>
<div v-show="filterExpanded" id="xxx-filter-extra" class="filter-extra">
<div class="filter-grid"><!-- 其余项 --></div>
</div>
<div class="filter-actions">
<button
v-if="showFilterToggle"
type="button"
class="btn-link btn-filter-toggle"
:aria-expanded="filterExpanded"
aria-controls="xxx-filter-extra"
@click="filterExpanded = !filterExpanded"
>
{{ filterExpanded ? '收起更多' : '展开更多' }}
<el-icon>
<ArrowUpBold v-if="filterExpanded" />
<ArrowDownBold v-else />
</el-icon>
</button>
<div class="filter-actions-end">…</div>
</div>
const filterExpanded = ref(false)
const showFilterToggle = computed(() => totalFilterFieldCount > 4)
function handleReset() {
// …清空 searchForm / dateRange
filterExpanded.value = false
pagination.page = 1
loadData()
}
Scenario: CRM 客户管理筛选区
- WHEN 用户打开
#/crm/customer - THEN 默认 SHALL 仅展示首行四个条件(客户名称、客户编号、客户类型、客户级别)
- AND SHALL 显示「展开更多」按钮
- WHEN 用户点击「展开更多」
- THEN SHALL 展示客户来源、客户状态、成交状态、所在园区、创建时间等扩展条件,按钮文案变为「收起更多」
Scenario: 重置后收起扩展条件
- WHEN 用户已展开筛选扩展区并点击「重置」
- THEN 扩展区 SHALL 收起且
filterExpanded为 false
Scenario: 单行筛选无需展开按钮
- WHEN 列表页筛选区仅包含 4 个及以下
el-form-item(桌面四列下一行) - THEN SHALL NOT 渲染「展开更多 / 收起更多」按钮
Scenario: Code review 筛选区折叠
- WHEN PR 新增或修改
.filter-block,且筛选项数量大于 4 - THEN 审查者 SHALL 确认采用首行 +
.filter-extra+.filter-actions结构 - AND SHALL 确认默认折叠、重置收起、按钮文案与图标符合本 Requirement
- AND 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 页面使用独立的
dateRangeref(非 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 等业务参数。
布局与视觉
- 结构:
el-form-item(建议class="form-col-span-2"占满弹窗栅格宽)→.attachment-upload→.attachment-list(flex 行)→ 上传卡片 + 若干.attachment-item;.upload-hint在 flex 行下方独立一行。 - 上传卡片:宽 140px、高 96px;
border: 1px dashed #a7f3d0;background: #f0fdf4;border-radius: 8px;居中Plus(size 28,#10b981)+ 文案「上传附件」(.upload-card-text,12px#10b981font-weight 500)。hover:border-color: #10b981、border-style: solid、background: #ecfdf5。 - 附件卡片:宽 140px;预览区高 96px;
border: 1px solid #e2e8f0;border-radius: 8px;hover:border-color: #10b981、box-shadow: 0 2px 8px rgba(16, 185, 129, 0.1)。 - 预览区(
.attachment-preview):背景#f8fafc;图片已上传且fileUrl存在时显示缩略图(object-fit: cover),可点击预览;图片上传中无 URL 时显示Loading;视频显示.attachment-preview__icon--video(#ede9fe/#7c3aed+VideoPlay);其他文件显示.attachment-preview__icon--doc(#e0f2fe/#0284c7+Document)。 - 卡片底部(
.attachment-footer):文件名.attachment-name(11px#334155,最多 2 行省略,title为完整文件名);.attachment-actions居中排列 下载(Download)、删除(Delete,.action-btn--dangerhover 红色)。 - 上传中:
.attachment-item.is-uploading→opacity: 0.6、pointer-events: none。 - 提示:
.upload-hint— 11px#94a3b8,margin-top: 8px;默认文案:「支持图片、视频、PDF、Word、Excel等文件,单文件不超过 20MB」。 - 色板:SHALL 使用 Fresh Mint(
#10b981、#a7f3d0、#f0fdf4、#ecfdf5);SHALL NOT 使用小程序主色#0a9f6d。 - 禁止: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 的第一个子元素,且始终渲染(不因已有附件数量而隐藏)。
数据模型与上传逻辑
附件列表 SHALL 为响应式数组,元素至少包含:id、fileName、fileUrl、fileType(可选)、_uploading(可选布尔)。
- 选择文件后:校验单文件 ≤ 20MB;超限则
ElMessage.error并clearFiles()。 - 立即上传:向数组 push 占位项(
_uploading: true、fileUrl: '');FormData调用fetchUploadAttachment(@/api/business),fd.append('folder', '<业务目录>')(如protocol、storage-cert、protocol-payment);成功后回填fileUrl等字段并_uploading: false;失败则移除该项并提示。 - 收尾:
nextTick后uploadRef.clearFiles(),避免同一文件无法重复选择。 - 删除:从数组
splice移除,不单独调删除接口(除非产品另有要求)。 - 下载:使用
@/utils/download-attachment的downloadAttachmentFile(file, { fullFileUrl });fullFileUrl需拼接VITE_MINIO_BASE_URL(与参考页一致)。 - 预览:图片/视频已上传项点击预览区,打开独立
el-dialog(宽 800px,destroy-on-close);图片用<img>,视频用<video controls>。 - 提交:若存在
_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、DeleteSHALL 在<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-hintSHALL 位于卡片行下方
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: Code review 附件上传
- WHEN PR 新增或修改附件上传 UI
- THEN 审查者 SHALL 对照
ProtocolFormDialog.vue确认卡片化布局与立即上传逻辑 - THEN SHALL 拒绝
picture-card、show-file-list默认列表、与规范不一致的尺寸/色值
Requirement: 小程序拍照凭证上传(基线)
miniprogram 业务附件在当前产品范围内 仅支持拍照/相册图片凭证(如取货、送达、备货、现场确认等),SHALL NOT 实现 PDF/Word/Excel 等文档类附件上传(文档类仍以 web-admin「附件上传卡片化布局」为准)。实现与交互以叁强鲜配 sqxp-wx 配送/供应商凭证上传为权威参考(如 pages/deliverer/task/detail/detail.vue、pages/supplier/delivery/detail/detail.vue、pages/supplier/settings/avatar.vue);山峡侧后续落地时 SHALL 抽公共工具(建议 miniprogram/utils/upload.js),业务页只负责数量上限、folder 与提交字段。
范围与禁止项
- 允许来源:
uni.chooseImage(或等价的uni.chooseMedia且仅mediaType: ['image']),sourceType含album与camera,sizeType优先compressed。 - 禁止:SHALL NOT 使用
wx.chooseMessageFile/uni.chooseFile选聊天文件或任意文档;SHALL NOT 复用 Web 端el-upload、FormData+uni.request/fetch上传二进制;SHALL NOT 把图片转 Base64 塞进 JSON 业务接口(除非产品书面要求且接口单独评审)。 - 色板与触控:上传入口、删除热区 SHALL 遵守小程序 Token(品牌绿
#0a9f6d系)与最小触控 44×44;SHALL NOT 使用 Web Fresh Mint#10b981作为小程序主色。
两步流程(强制)
- 选图并立即上传:用户选择临时路径后,逐张(或受控并发)调用
uni.uploadFile上传到图片上传接口,成功后将返回的 URL 字符串 推入本地数组(如imageUrls)。 - 业务提交:用户确认业务动作(取货/送达/保存等)时,再调用业务 API,请求体携带已上传的 URL 列表(字段名建议
imageUrls,camelCase);服务端再关联业务并(如需要)写入附件表。SHALL NOT 在业务提交接口中直接接收 multipart 文件流(与叁强「先 URL、后业务」一致)。
uni.uploadFile 约定
| 项 | 要求 |
|---|---|
| 调用方式 | uni.uploadFile(微信小程序原生 multipart) |
url |
图片上传接口绝对或与 API_BASE 拼接后的完整 URL(见 api-conventions「小程序拍照凭证上传接口」) |
filePath |
chooseImage / chooseMedia 返回的临时路径 |
name |
固定为 file(与后端 UploadFile 字段名一致) |
header.Authorization |
Bearer ${token};token 来自本地存储(与登录态同一 key) |
formData.folder |
业务目录字符串(如 delivery-evidence、avatars、山峡业务自定义 kebab/短路径) |
| 响应解析 | uploadFile 的 data 多为 字符串,SHALL JSON.parse 后再读 code / data.url;成功条件与统一响应一致(code === 200 且存在 data.url) |
| Loading | 上传过程 SHALL uni.showLoading,结束 uni.hideLoading;失败 SHALL uni.showToast 提示 |
上传逻辑 SHALL NOT 走会做 AES/包体加密的通用 request() 封装(multipart 无法套 JSON 加密封装);鉴权仅靠 Header Bearer。
UI 与数量
- 展示:已上传项以缩略图网格/横向列表展示;点击预览 SHALL 使用
uni.previewImage(urls为当前列表,current为点击项)。 - 删除:未提交业务前可从本地数组移除;已随业务提交锁定的凭证按业务只读展示,SHALL NOT 在只读态提供删除。
- 数量上限:单次业务凭证默认最多 6 张(与叁强配送凭证一致);若业务需要其他上限,SHALL 在页面文案与选图
count中写死同一常量。选图前count = 上限 - 已有张数,已满则 toast「最多上传 N 张」。 - 上传入口文案:建议「+ 上传」或「选择图片并上传」;入口热区 ≥ 44×44。
参考实现骨架(工具函数形态)
// miniprogram/utils/upload.js(后续落地时抽取;业务页传入 folder / maxCount)
export async function chooseAndUploadImages({ folder, maxCount, currentUrls, token, uploadUrl }) {
const remain = maxCount - currentUrls.length
if (remain <= 0) {
uni.showToast({ title: `最多上传${maxCount}张`, icon: 'none' })
return []
}
const choose = await new Promise((resolve) => {
uni.chooseImage({
count: remain,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => resolve(res),
fail: () => resolve(null)
})
})
if (!choose?.tempFilePaths?.length) return []
const urls = []
uni.showLoading({ title: '上传中...' })
try {
for (const filePath of choose.tempFilePaths) {
const uploadRes = await new Promise((ok, fail) => {
uni.uploadFile({
url: uploadUrl,
filePath,
name: 'file',
header: { Authorization: token ? `Bearer ${token}` : '' },
formData: { folder },
success: ok,
fail
})
})
const raw = typeof uploadRes.data === 'string' ? uploadRes.data : String(uploadRes.data || '')
let body = {}
try {
body = JSON.parse(raw)
} catch {
body = {}
}
if (body.code !== 200 || !body.data?.url) {
uni.showToast({ title: body.message || '上传失败', icon: 'none' })
break
}
urls.push(String(body.data.url))
}
} finally {
uni.hideLoading()
}
return urls
}
业务页在确认动作时示例:
await request({
url: `/mini/<role>/tasks/${taskId}/pickup`,
method: 'POST',
data: { remark: '已取货', imageUrls: pickupImages.value }
})
Scenario: 配送/现场凭证上传
- WHEN 用户在小程序业务页点击「上传」且未达上限
- THEN SHALL 调起相册或拍照选图,并对每张临时文件调用
uni.uploadFile - THEN 成功后本地列表增加可预览的图片 URL,业务提交时携带
imageUrls
Scenario: 禁止文档类附件
- WHEN 需求表述为「附件」但场景为小程序端
- THEN 实现 SHALL 仅提供拍照凭证能力;若确需 PDF 等文档,SHALL 先走独立 OpenSpec 变更扩展本 Requirement,不得静默加入
chooseMessageFile
Scenario: Code review 小程序凭证上传
- WHEN PR 新增或修改
miniprogram图片/凭证上传 - THEN 审查者 SHALL 确认使用
uni.chooseImage(或仅图片的chooseMedia)+uni.uploadFile,且业务提交为 URL 列表 - THEN SHALL 拒绝
FormData+request、Base64 塞业务 JSON、以及未排除加解密的上传路径误用
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 |
筛选区「重置」按钮 |
| 展开更多 / 收起更多 | ArrowDownBold / ArrowUpBold |
筛选区 .btn-filter-toggle(默认折叠多行条件时) |
| 确定 / 确认 / 提交 / 保存 | Check |
弹窗确认类按钮 |
| 取消 / 关闭 | Close |
弹窗取消、关闭按钮 |
| 编辑 / 修改 | Edit |
工具栏「编辑」、查看弹窗 #hero-actions「编辑」、操作列「编辑」按钮 |
| 删除 / 批量删除 | Delete |
工具栏「删除」、查看弹窗 #hero-actions「删除」、操作列「删除」按钮 |
| 查看 / 详情 | View |
工具栏「查看审批流」等非列表行主入口;列表行查看由主标识列 el-link 或双击触发,操作列与工具栏不再放「查看」 |
| 审批 / 审核 | EditPen |
操作列「审批」按钮 |
| 配置 / 设置 | Setting 或 SetUp |
操作列「配置」按钮 |
| 导入 / 上传 | Upload |
工具栏「导入」、表单「上传」按钮 |
| 折叠 / 展开 | ArrowDown / ArrowUp |
工具栏「展开/收起」按钮 |
| 充值 / 发起充值 | Wallet |
渠道服务订单等预付款充值操作 |
| 退款 / 申请退款 | Sell |
退款、退费类操作 |
| 购买产品 | ShoppingCart |
购买、下单类操作 |
| 退订 | Remove |
退订、减少持有类操作 |
| 作废 | Delete |
作废流水/订单(与删除同图标,可用 op-warn 样式区分) |
| 操作流水 / 记录列表 | List |
「操作流水」等子列表入口 |
| 返回 | Close |
详情页、表单页「返回」按钮(与取消同图标) |
| 选择 / 挑选 | Select |
输入框 append「选择」、挑选合同/客户等 |
| 安排运输 | Van |
运输计划「安排运输」操作 |
| 归档 / 批量归档 | FolderChecked |
计划归档、批量归档 |
| 取消计划 | CircleClose |
取消运输计划等业务取消 |
| 二维码 | Share |
生成/查看运输二维码 |
| 提交结算 | Promotion |
运费结算「提交」 |
| 审核通过 | CircleCheck |
运费结算「审核」、证件「处理」等确认类操作 |
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: 运输计划模块(web-admin/src/views/pl)
- WHEN 实现或修改
web-admin/src/views/pl/**下运输计划菜单页面(承运商管理、车辆管理、司机管理、运输计划、运输管理、运费结算、库存台账、证件提醒及其子页 form/detail/arrange) - THEN 每个带可见文案的
el-buttonSHALL 在文字前方包含<el-icon>,并按上表选用语义图标(如搜索Search、重置RefreshLeft、新增Plus、查看View、安排运输Van、归档FolderChecked、提交Promotion、审核/处理CircleCheck、选择Select等) - THEN 图标组件 SHALL 在
<script setup>中从@element-plus/icons-vue显式import,SHALL NOT 依赖隐式全局注册 - THEN Code review SHALL 拒绝仅有文字、无
<el-icon>的按钮
Scenario: 渠道服务订单与财务确认页
- WHEN 实现或修改
web-admin/src/views/pilot-service/channel-service-order/**或channel-service-order-finance/**中的按钮(含弹窗底栏、表格操作列、行内 link 删除) - THEN 每个带文案的
el-buttonSHALL 按上表携带图标(如查看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 类型语义色映射规范
web-admin 中语义 el-tag(含列表、查看弹窗、Teleport 弹层):success / warning / danger / primary SHALL 使用 深色底 + 白字;info SHALL 使用 透明无背景 + 标准正文色(var(--mint-text) / #1e293b),SHALL NOT 使用浅底彩字或 Element 默认灰 #909399。全局样式权威实现:web-admin/src/assets/styles/core/el-ui.scss。视觉权威:docs/web-admin-ui-spec-fresh-mint.md §7.7。
| Tag Type | 背景色 | 文字 | 适用语义 | 示例 |
|---|---|---|---|---|
success |
#059669 |
#fff |
正常、启用、已完成、生效中、通过 | 正常/启用/已完成/接收/通过 |
warning |
#d97706 |
#fff |
进行中、待确认、已过期、预警 | 进行中/已排配/已过期/待确认 |
danger |
#dc2626 |
#fff |
异常、已作废、拒绝、不通过 | 异常/已作废/拒绝/不通过 |
info |
透明 | #1e293b |
待执行、待生效、未知、停用、默认 | 待执行/待生效/停用/无结论 |
primary |
#059669 |
#fff |
特殊分类、来源类型、转出 | 转科研/转细胞存储/来源类型/菜单类型 |
对于启用/停用的状态字段,SHALL 使用 success(启用)和 info(停用),SHALL NOT 使用 danger 表示停用状态。
5. 状态标签注册表(集中定义 label + tagType)
web-admin 中所有状态、结论、类型等枚举字段的 文案(label)与 Tag 语义色(tagType)SHALL 在单一注册表中集中定义,权威文件为 web-admin/src/utils/status-tag-registry.ts。
核心原则:
- 同一枚举值全局一致:同一枚举 code 在列表、查看弹窗、导出、嵌套子列表等任何展示位置,SHALL 使用相同的 label 与 tagType;SHALL NOT 在页面内用三元表达式或局部
Record重复定义映射。 - 成对导出:每个枚举域 SHALL 导出 registry 对象及配套的
xxxLabel(code)、xxxTagType(code)两个 getter;未知 code 时 label 回退为原值、tagType回退为'info'。 - 新增枚举先注册后使用:新增业务枚举时,SHALL 先在
status-tag-registry.ts追加 registry 与 getter,再在页面/组件中 import 使用;SHALL NOT 直接在.vue中写if/else或内联映射。 - 域内分组: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/xxxTagTypegetter 使用
Scenario: 复用已有枚举的状态展示
- WHEN 页面需展示已在注册表中定义的枚举(如
protocolStatus、taskStatus、enableStatus) - THEN 页面 SHALL 直接从
@/utils/status-tag-registryimport 并使用,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" 等紧凑样式。
具体规则:
- 字号与行高:表格正文 SHALL 使用规范默认 14px 字号与标准行高(与
.list-table/ 主列表一致);SHALL NOT 为弹窗内表格单独设置更小font-size或通过size="small"/size="mini"缩小表格。 - 表格类名:业务列表(含弹窗内嵌列表)SHALL 使用
.list-table(可与.inner-table等场景类叠加),以复用全局表格 Token 与行高,而非在 scoped 样式中覆盖为紧凑尺寸。 - 只读查看:查看详情、任务详情等只读场景中的子列表 SHALL 以纯文本/Tag 展示数据,SHALL NOT 保留可编辑控件;同时 SHALL NOT 隐藏操作列后改用更小表格尺寸补偿空间。
- 高度:弹窗内列表可通过
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 按序执行):
- 对照下方「功能特征 → 模式」决策表或索引文档 §二,确定模式编号(如 P01、P12)。
- 打开该模式对应的 Demo 模板页 或 业务参考页(Hash + 源码路径见索引文档 §三)。
- 复制页面骨架与交互模式,替换 API、字段、
menuCode;SHALL NOT 从零设计布局或另起一套类名习惯。 - 枚举字段 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: 金额与单价千分位输入与展示
本 Requirement 适用于价格、单价、金额、成本价、费用、总额等以「元」为业务单位的货币类字段(下称「价格类字段」)。数量、份数、排序号、百分比等非货币字段不在此列。
1. 标签须标注单位「(元)」
凡价格类字段,其可见标签文案 SHALL 在字段名后紧跟半角括号包裹的单位 (元)(全角括号,与中文排版一致),不论出现在何处:
| 场景 | 写法示例 |
|---|---|
新增/编辑弹窗 el-form-item |
label="标准单价(元)" |
筛选区 el-form-item |
label="价格区间(元)" |
列表 el-table-column |
label="销售价(元)" |
查看弹窗 KV(dt / SystemViewDialog 的 label) |
标准单价(元) |
| 合计/摘要文案中的字段名 | 应付总额(元) |
- 单位
(元)写在 label 侧,SHALL NOT 仅写在 placeholder、表头 tooltip 或单元格数值后方。 - 标签已含「元」字时仍 SHALL 使用统一后缀形式,如
金额(元),SHALL NOT 写为金额/元或金额(元)(半角括号)。 - 输入控件内(含
AmountInput)SHALL NOT 再重复显示元或¥;单位由 label 承担。
2. 只读展示须前缀人民币符号「¥」
价格类字段的只读数值展示(列表单元格、查看弹窗、详情 KV、合计行、导出前端的展示列等)SHALL:
- 使用
formatMoneyDisplay(value, 2)(@/utils/money-format.ts)做千分位与小数位格式化; - 在格式化结果前面拼接半角人民币符号
¥(U+00A5),中间不加空格; - 数值文本 SHALL 使用全局类名
money-display(web-admin/src/assets/styles/core/app.scss,色值#dc2626,font-weight: 600),使单价、价格、金额等只读数值统一红色强调。
标准模板:
<!-- 列表列 -->
<el-table-column prop="price" label="标准单价(元)" min-width="120" align="right">
<template #default="{ row }">
<span class="money-display">¥{{ formatMoneyDisplay(row.price) }}</span>
</template>
</el-table-column>
<!-- 查看弹窗 KV -->
<dt>成本价(元)</dt>
<dd><span class="money-display">¥{{ formatMoneyDisplay(detail.costPrice) }}</span></dd>
<!-- SystemViewDialog -->
{ label: '销售价(元)', value: `¥${formatMoneyDisplay(d.salePrice)}`, money: true }
formatMoneyDisplaySHALL NOT 内置¥;符号 SHALL 在模板或拼接字符串侧统一添加,便于与输入态区分。- 空值、未填:SHALL 展示为
¥0.00或业务约定的—;若用—,SHALL NOT 前缀¥。
3. 弹窗/列表表单控件同宽(栅格内满宽)
在 dialog-form-layout 弹窗、page-container / console-container 内使用 .form-grid-2~.form-grid-5(或等价栅格)时,同一行内的所有表单控件(el-input、el-select、el-tree-select、AmountInput、el-input-number 等)SHALL 占满该栅格列宽度(width: 100%),SHALL NOT 因使用 el-input-number 而保留 Element Plus 默认约 150px 窄宽导致与邻列文本框视觉不齐。
- 全局样式权威:
web-admin/src/assets/styles/core/app.scss(.el-dialog.dialog-form-layout .el-form-item .el-input-number等选择器)。 - 页面 scoped 样式 SHALL NOT 为单个弹窗重复写宽度补丁;优先依赖全局规则。
- 价格类字段 SHALL 优先使用
AmountInput(内部已为el-input且width: 100%),兼顾金额规范与同宽。
Scenario: 弹窗栅格内数字字段
- WHEN 在
form-grid-2等栅格中放置el-input-number(如排序号、数量) - THEN 控件根节点 SHALL 与同行的
el-input同宽,SHALL NOT 明显窄于邻列
Scenario: Code review 弹窗表单控件宽度
- WHEN PR 在弹窗表单中新增
el-input-number或AmountInput - THEN 审查者 SHALL 目视确认与同栅格列
el-input等宽,必要时检查是否误用未拉满的class="w-full"alone(对el-input-number无效,须全局或width: 100%作用于组件根)
4. 可编辑输入(AmountInput)
价格类可编辑字段 SHALL 使用 AmountInput(@/components/common/amount-input/index.vue):
| 时机 | 行为 |
|---|---|
| 聚焦输入中 | 纯数字展示,无千分位逗号,如 1234567.89 |
| 失焦后 | 千分位展示,如 1,234,567.89(无 ¥ 前缀) |
| 再次聚焦 | 自动去掉逗号,恢复纯数字便于修改 |
| 提交/API | number 类型,不受逗号影响 |
业务 .vue 文件 SHALL NOT 复制千分位正则或 addThousandSeparators 等函数实现;SHALL 统一引用 @/utils/money-format.ts。
Scenario: 弹窗表单价格字段
- WHEN 在新增/编辑弹窗中新增价格或单价字段
- THEN
el-form-item的labelSHALL 为「字段名(元)」 - THEN 控件 SHALL 为
<AmountInput />,SHALL NOT 使用<el-input-number>
Scenario: 列表价格列
- WHEN 在
el-table中展示价格或单价列 - THEN 列
labelSHALL 含「(元)」 - THEN 单元格展示 SHALL 为
<span class="money-display">¥{{ formatMoneyDisplay(row.xxx) }}</span>,数值列建议align="right"
Scenario: 查看弹窗价格字段
- WHEN 在查看弹窗或
SystemViewDialog中展示价格 - THEN 字段标签 SHALL 含「(元)」,数值 SHALL 以
¥+formatMoneyDisplay展示,并 SHALL 使用money-display红色样式(ViewField.money: true或模板侧class="money-display")
Scenario: Code review 价格类字段
- WHEN PR 涉及价格、单价、金额、成本价等字段
- THEN 审查者 SHALL 确认 label 含「(元)」、只读值含
¥前缀与money-display红色样式,且使用AmountInput/formatMoneyDisplay,拒绝页面内复制千分位实现或用主色绿展示金额
Requirement: Web 管理端常见错误案例(反例登记)
以下条目为已发生过的真实缺陷,用于 PR 自检与 Code Review;新增类似实现时 SHALL 对照避免重复。
错误案例 EC-UI-001:无分页列表底角被表格白底遮住
| 项 | 说明 |
|---|---|
| 现象 | 列表面板(.panel)左下、右下圆角被「削平」或露出方角白块,多见于树形/全量列表且无分页条的页面(如 #/crm/product-category、组织管理、菜单管理等)。 |
| 根因 | .list-body.list-wrap 全局为 overflow: visible(避免影响 fixed="right" 操作列);el-table 单元格白底在面板底部以直角延伸,盖住 .panel / .list-wrap 的 border-radius: 0 0 10px 10px。有分页时由 .list-pager-bar 承接底圆角,问题不明显;无分页时列表区为 panel 末段,白底直接顶到面板底边。 |
| 错误写法 | 仅给 .list-wrap 设底圆角,未裁剪末段列表;或误以为「与有分页列表页相同结构」即可自动正确。 |
| 正确做法 | 依赖全局规则(web-admin/src/assets/styles/core/app.scss):当 .list-body.list-wrap 后面没有 .pagination.list-pager-bar / .list-footer 时,对该节点设 overflow: hidden 以裁剪底圆角。新增无分页列表页 SHALL 使用 list-body list-wrap 包裹表格,SHALL NOT 在页面 scoped 样式中单独 overflow: hidden 于 .panel(会破坏固定列)。 |
| 自检 | 打开页面滚动至列表底部,确认面板左下/右下圆角与 Mint 边框一致,无白色方角溢出。 |
Scenario: EC-UI-001 无分页树形列表
- WHEN 新增 P03 类树形列表页且 SHALL NOT 使用分页条
- THEN 表格 SHALL 包在
.list-body.list-wrap内,且其后 SHALL NOT 紧跟.pagination.list-pager-bar - THEN 实现 SHALL 依赖全局
app.scss末段裁剪规则,SHALL NOT 仅复制有分页参考页而遗漏底圆角验收
Scenario: EC-UI-001 Code review
- WHEN PR 新增或改造无分页的
list-body list-wrap列表 - THEN 审查者 SHALL 在浏览器中检查面板底部圆角是否被表格白底遮挡
错误案例 EC-UI-002:一级菜单 meta.icon 未注册导致侧边栏无图标
| 项 | 说明 |
|---|---|
| 现象 | 左侧一级菜单(如「CRM 管理」)名称前无图标,仅留空槽或占位。 |
| 根因 | ArtSvgIcon 约定:图标名不含 : → 查 src/utils/ui/ep-icon-map.ts 的 Element Plus 映射;含 : → 走 Iconify。在 sqxp.ts / menu.py 中设置 meta.icon: 'goods' 时,若 ep-icon-map 未注册 goods,resolveEpIcon 返回 undefined,组件不渲染图标。workflow 等同理,须在 map 中注册(如 workflow: Connection)。 |
| 错误写法 | 路由/菜单树写入 icon: 'goods' 或任意 EP 图标名,但未在 ep-icon-map.ts 增加 import 与 epIconMap 条目;或误用无 : 的字符串指望 Iconify 自动加载。 |
| 正确做法 | 1)在 @element-plus/icons-vue 确认有对应组件(如 Goods);2)在 ep-icon-map.ts 注册 goods: Goods;3)再在 meta.icon 使用 'goods'。新增一级菜单图标 SHALL 与「图标组件使用 Element Plus 图标」Requirement 一并完成。 |
| 参考 | 修复记录:goods → Goods(CRM 管理);workflow → Connection(工作流管理)。 |
Scenario: EC-UI-002 新增一级菜单图标
- WHEN 在
src/router/modules/或backend/app/api/menu.py为一级菜单设置meta.icon - THEN 图标名 SHALL 已在
web-admin/src/utils/ui/ep-icon-map.ts注册 - THEN 刷新后侧边栏一级项 SHALL 显示图标(
SidebarSubmenu仅level === 0渲染图标)
Scenario: EC-UI-002 Code review
- WHEN PR 修改菜单
meta.icon或新增 EP 风格图标名(无:前缀) - THEN 审查者 SHALL 打开
ep-icon-map.ts确认存在对应键值,SHALL NOT 仅检查路由/菜单 JSON 是否写了icon字段
错误案例 EC-UI-003:价格字段未标「(元)」或未展示「¥」
| 项 | 说明 |
|---|---|
| 现象 | 列表列标题为「标准单价」、弹窗 label 为「成本价」,单元格或详情仅显示 1,234.56 无货币符号。 |
| 根因 | 仅使用 formatMoneyDisplay 满足千分位,未遵守「标签单位 + 展示符号」双轨约定。 |
| 正确做法 | 见本 spec「金额与单价千分位输入与展示」:label/dt/列头一律 xxx(元);只读值一律 <span class="money-display">¥{{ formatMoneyDisplay(...) }}</span>。 |
Scenario: EC-UI-003 Code review
- WHEN PR 新增或修改价格、单价、金额、成本价等字段
- THEN 审查者 SHALL 抽查列表列、弹窗表单、查看弹窗三处是否均满足「(元)」、
¥前缀与money-display红色样式
错误案例 EC-UI-003b:价格类字段未使用红色强调
| 项 | 说明 |
|---|---|
| 现象 | 列表/查看弹窗中单价、金额使用主色绿(#059669)或默认黑色展示。 |
| 根因 | 误用 .amount-text、.approval-amount 等历史局部样式,或未使用全局 .money-display。 |
| 正确做法 | 只读价格 SHALL 使用 money-display(#dc2626);SystemViewDialog 的 ViewField.money: true 自动套用。 |
错误案例 EC-UI-004:弹窗栅格内 el-input-number 宽度窄于同行文本框
| 项 | 说明 |
|---|---|
| 现象 | 新增/编辑弹窗两列栅格中,「标准单价」「成本价」「排序号」等 el-input-number 明显比「产品名称」等 el-input 窄。 |
| 根因 | Element Plus 为 el-input-number 根节点设置默认宽度(约 150px);仅加 Tailwind w-full 或未覆盖根宽度时无法与同列 el-input 对齐。 |
| 正确做法 | 价格字段用 AmountInput;其余数字字段依赖 app.scss 弹窗/页面表单内 .el-input-number { width: 100%; };SHALL NOT 在单页 scoped 零散补丁。 |
| 参考 | #/crm/product 产品新增弹窗。 |
Scenario: EC-UI-004 Code review
- WHEN PR 在
dialog-form-layout+form-grid-*弹窗中新增el-input-number - THEN 审查者 SHALL 确认数字控件与同栅格列文本输入同宽