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