## 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: 列表面板主标题装饰 `web-admin` 中列表面板 `panel-top` 下的主标题(`h2.title-bar-heading`)SHALL 在标题文案前展示 **主色竖条**(与全局 `app.scss` 中 `h2.title-bar-heading::before` 一致);含「租户」「用户」等易歧义文案的标题 SHALL 通过 `App.vue` 中 `panelTitleBarTextRules` 显式纳入竖条样式,避免子串误匹配为其它图标。 **重要**:`App.vue` 中 `panelTitleBarTextRules` 是标题竖条样式的唯一触发入口。新增任何包含 `panel-top > h2` 标题的页面时,开发者 SHALL 将该标题文案追加到 `panelTitleBarTextRules` 正则表达式的 `|` 分隔列表中。遗漏将导致标题无绿色竖条而显示为无样式或错误图标。 #### Scenario: 租户列表标题 - **WHEN** 打开 `#/system/tenant` 租户管理页 - **THEN** 「租户列表」标题 SHALL 显示绿色竖条装饰,且 SHALL NOT 因「租户」含「用户」子串而显示用户类图标 #### Scenario: 新增页面标题竖条 - **WHEN** 新增包含 `panel-top > h2` 标题的列表页(如标题为"XXX列表") - **THEN** 开发者 SHALL 在 `App.vue` 的 `panelTitleBarTextRules` 正则中追加该标题文案(用 `|` 分隔) - **THEN** 标题 SHALL 显示绿色竖条装饰 #### Scenario: Code review 新页面标题 - **WHEN** PR 包含新增页面的 `panel-top > h2` 标题 - **THEN** 审查者 SHALL 确认 `App.vue` 的 `panelTitleBarTextRules` 已同步更新 ### Requirement: 弹窗与面板内子区域标题装饰 `web-admin` 中弹窗(`el-dialog`)和面板内的子区域标题(如「选择部门」「已选用户」「基本信息」「参数配置」等区块标题)SHALL 在文字左侧展示主色竖条装饰(`border-left: 3px solid #10b981`,`padding-left: 10px`),与列表面板主标题的竖条风格保持一致。子区域标题与字段标签(`el-form-item` label 或 `dt`)SHALL 形成明显视觉区分:子区域标题使用深色文字 + 竖条,字段标签使用辅助灰色、无竖条。 此规则适用于以下场景的区块标题: 1. **弹窗内分组标题**:如编辑弹窗中的「基本信息」「参数配置」等将表单字段分组的标题。 2. **多栏选择器的列标题**:如「选择部门」「选择用户」等 `data-scope-col-title` 类标题。 3. **已选摘要的分组标题**:如「已选部门」「已选用户」等 `data-scope-summary-heading` 类标题。 4. **查看弹窗的区块标题**:`SystemViewDialog` 的 section title(`h3`)。 5. **设计器/工具面板的分组标题**:如「节点类型」「属性」等 `panel-section-title` 类标题。 #### Scenario: 新增弹窗包含子区域分组 - **WHEN** 新增编辑弹窗内使用 `section-title`、`col-title`、`summary-heading` 等类名作为区块标题 - **THEN** 标题 SHALL 使用 `border-left: 3px solid #10b981` + `padding-left: 10px` 展示主色竖条 #### Scenario: Code review 弹窗子区域标题 - **WHEN** PR 包含弹窗内新增或修改的区块标题样式 - **THEN** 审查者 SHALL 确认标题包含主色竖条装饰,且与字段标签有明确视觉区分 ### Requirement: 新增与编辑弹窗布局与视口留白 使用 Element Plus `el-dialog` 承载新增/编辑表单时,系统 SHALL 满足: 1. **水平栅格**:默认使用 **两列** 栅格(`.form-grid-2`,`grid-template-columns: repeat(2, minmax(0, 1fr))`,`gap: 12px 20px`);字段较多时 SHALL 通过加宽 `width` 并选用 `.form-grid-3`、`.form-grid-4` 或 `.form-grid-5` 扩展列数,**列数上限为 5**;跨整行长字段 SHALL 使用 `.form-col-span-2`(`grid-column: 1 / -1`)。 2. **窄屏回退**:视口或表单区过窄时,栅格 SHALL 回退为更少列或单列(与全局 `app.scss` 中 `@media (max-width: 900px)`、`(max-width: 560px)` 规则一致)。 3. **定位**:弹窗 SHALL 相对视口水平与垂直居中(依赖 `ElConfigProvider` 的 `dialog.alignCenter` 及 Element Plus 默认遮罩层居中行为)。 4. **高度与滚动**:使用 `.dialog-form-layout` 类名的弹窗壳层 SHALL `max-height: calc(100vh - 100px)`,保证相对视口上下各 **至少约 50px** 可视留白;弹窗 SHALL NOT 以固定高度占满视口;当内容超出时 **仅** `.el-dialog__body` 区域出现纵向滚动条,页头与底栏固定不滚动。 #### Scenario: 租户新增弹窗 - **WHEN** 在租户管理页打开新增或编辑租户弹窗 - **THEN** 表单 SHALL 以两列栅格展示常规字段、备注跨两列;弹窗 SHALL 带 `dialog-form-layout` 且正文区在内容过多时可滚动 #### Scenario: 高字段数弹窗 - **WHEN** 某业务弹窗字段超过两列舒适排布 - **THEN** 实现 SHALL 增加 `el-dialog` 的 `width` 并选用 `form-grid-3` 至 `form-grid-5` 之一,且 SHALL NOT 使用超过 5 列的栅格类 ### Requirement: 图标组件使用 Element Plus 图标 `web-admin` 中的图标 SHALL 优先使用 Element Plus 内置图标(`@element-plus/icons-vue`),通过 `ArtSvgIcon` 组件的 EP 图标映射(`src/utils/ui/ep-icon-map.ts`)渲染;SHALL NOT 在新增代码中直接使用 Iconify 远程图标名(如 `ri:xxx`、`fluent:xxx`)。仅在 Element Plus 无等价图标时,方可保留 Iconify 图标名作为例外。 #### Scenario: 新增菜单路由 - **WHEN** 在 `src/router/modules/` 中新增路由并设置 `meta.icon` - **THEN** `icon` 值 SHALL 使用 `ep-icon-map.ts` 中已注册的名称(如 `setting`、`grid`),SHALL NOT 使用 `ri:` 前缀的 Iconify 名 #### Scenario: 新增界面图标 - **WHEN** 在页面或组件中新增图标 - **THEN** SHALL 使用 `` 或 ``,SHALL NOT 使用 `` 除非 Element Plus 无等价图标 #### Scenario: 新增 Element Plus 图标 - **WHEN** Element Plus 提供了新的所需图标但 `ep-icon-map.ts` 尚未注册 - **THEN** 开发者 SHALL 先在 `ep-icon-map.ts` 中注册映射条目,再在业务代码中使用 ### Requirement: 表格操作列固定在右侧 `web-admin` 中所有业务列表页(系统管理、业务模块等,不含 Demo 参考页)的 `el-table` 操作列 SHALL 使用 `fixed="right"` 固定在表格右侧。宽度规则:2 个按钮时 `min-width="260"`,3 个按钮时 `min-width="320"`,4 个及以上按钮时 `min-width="400"`。全局 CSS 通过 `.el-table-fixed-column--right` 选择器(注意:Element Plus 使用单短横线,非双下划线)提供固定列样式。 #### Scenario: 新增系统管理列表页 - **WHEN** 在系统管理模块下新增列表页面(如用户管理、角色管理等) - **THEN** 表格操作列 SHALL 设置 `fixed="right"`,宽度按按钮数设置:≤ 2 按钮用 `min-width="260"`,3 按钮用 `min-width="320"`,≥ 4 按钮用 `min-width="400"` #### Scenario: Code review 表格列 - **WHEN** PR 包含新增或修改的 `el-table-column` 且 `label="操作"` - **THEN** 审查者 SHALL 确认该列包含 `fixed="right"` 属性,且宽度与按钮数量匹配(2 按钮 260px,3+ 按钮 320px) ### Requirement: 表格列宽使用 min-width 实现等比缩放 `web-admin` 中所有 `el-table` 的列宽属性 SHALL 使用 `min-width` 替代 `width`,使浏览器视口宽度变化时各列按基础比例等比伸缩。 **原理**:Element Plus 的布局逻辑是,所有 `min-width` 列按各自 `min-width` 值的比例分配容器可用宽度;当容器宽度小于所有 `min-width` 之和时,触发横向滚动。若部分列使用 `width`(固定宽度),则多余空间全部由 `min-width` 列吸收,导致视口加宽时只有 `min-width` 列变宽而 `width` 列不变。 **规则**: 1. 所有 `el-table-column`(包括序号列、数据列、操作列)SHALL 使用 `min-width` 设定基础宽度,SHALL NOT 使用 `width`。 2. `min-width` 值即为该列的最小可读宽度,也是等比分配的权重基数。 3. `el-table` 本身的 `width` 属性(如弹窗内嵌表格的固定总宽)不受此规则约束。 #### Scenario: 新增列表页 - **WHEN** 新增列表页包含 `el-table` - **THEN** 所有 `el-table-column` 的宽度属性 SHALL 使用 `min-width`,SHALL NOT 使用 `width` #### Scenario: 浏览器窗口宽度变化 - **WHEN** 用户调整浏览器窗口宽度 - **THEN** 各列宽度 SHALL 按 `min-width` 设定的基础比例等比伸缩 #### Scenario: Code review 表格列宽 - **WHEN** PR 包含新增或修改的 `el-table-column` 且使用 `width` 属性 - **THEN** 审查者 SHALL 要求改为 `min-width` ### Requirement: 侧边栏菜单图标层级规则 `web-admin` 左侧菜单区域的图标展示 SHALL 遵循以下层级规则: 1. **一级菜单**(顶层菜单项)SHALL 在菜单名称前展示图标(`meta.icon`)。 2. **二级及以下菜单**(子菜单项)SHALL NOT 展示图标,仅展示文字名称。 3. 双列菜单模式(`DualMenu`)左侧一级图标栏 SHALL 为每个一级菜单项渲染图标,与侧边栏一级规则一致。 4. 水平菜单(`HorizontalMenu`)和混合菜单(`MixedMenu`)如使用图标,SHALL 与侧边栏保持相同的层级规则。 #### Scenario: 侧边栏一级菜单图标 - **WHEN** 用户打开 `web-admin` 主页,查看左侧菜单区域的一级菜单项 - **THEN** 每个一级菜单项 SHALL 在文字名称左侧展示其 `meta.icon` 对应的图标 #### Scenario: 侧边栏二级菜单无图标 - **WHEN** 用户展开某个一级菜单,查看其下的二级(及更深层级)菜单项 - **THEN** 二级及以下菜单项 SHALL NOT 展示任何图标,仅显示菜单文字名称 #### Scenario: Code review 菜单图标 - **WHEN** PR 修改了 `SidebarSubmenu.vue` 或其他菜单组件中图标渲染逻辑 - **THEN** 审查者 SHALL 确认 `level === 0` 条件判断保持不变,仅一级菜单渲染图标 ### Requirement: 标签页(WorkTab)不展示图标 `web-admin` 顶部标签页区域(WorkTab strip)的每个标签 SHALL 仅展示文字标题,SHALL NOT 展示图标。路由 `meta.icon` 仅用于侧边栏菜单,SHALL NOT 传递到标签页渲染。 #### Scenario: 标签页仅显示文字 - **WHEN** 用户打开任意页面,顶部标签页区域出现对应标签 - **THEN** 标签 SHALL 仅展示页面标题文字,SHALL NOT 在标题前展示任何图标 #### Scenario: Code review 标签页图标 - **WHEN** PR 修改了 `art-work-tab/index.vue` 或 `utils/navigation/worktab.ts` - **THEN** 审查者 SHALL 确认标签页组件不渲染图标,且 `setWorktab` 不向 `openTab` 传递 `icon` 字段 ### Requirement: 表单字段注释格式 `web-admin` 中弹窗表单(`el-form-item`)如需对某字段添加辅助说明文字(如"数值越小越靠前"),该注释 SHALL 以小字形式直接显示在 label 文字后方,SHALL NOT 放置在输入控件旁边或下方。实现方式为使用 `el-form-item` 的 `#label` 插槽,在 label 文字后追加 `注释内容`。样式类 `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 使用 `` 格式,SHALL NOT 将注释放在输入控件旁 ### Requirement: 列表行查看弹窗(按钮 + 双击) `web-admin` 中**所有** `el-table` 组件——包括主列表页(标准列表、树形列表)、弹窗内嵌列表(如编辑弹窗/查看弹窗/选择弹窗中的子表格)、侧栏面板内的列表——若操作列包含「查看」「查看详情」「查看确认信息」等只读查看类按钮,SHALL 同时提供行双击触发查看的能力,实现规则如下: 1. **操作列「查看」按钮**:操作列 SHALL 在首位包含一个「查看」按钮(`查看`),点击后弹出只读查看弹窗。 2. **行双击触发**:`el-table` SHALL 监听 `@row-dblclick` 事件,双击任意行时触发与「查看」按钮相同的弹窗(即调用同一 `handleView(row)` 函数)。 3. **条件按钮场景**:当操作列按钮随状态/Tab 变化(如待确认 Tab 显示「确认收款」,已确认 Tab 显示「查看确认信息」),`@row-dblclick` handler SHALL 仅在「查看」类按钮可见时触发对应查看函数,其余状态下可忽略或调用对应主操作函数。 3. **弹窗样式**:查看弹窗 SHALL 使用 `SystemViewDialog` 共享组件(`src/components/business/system-view-dialog/index.vue`),样式对齐待办中心「查看详情」弹窗(无标题栏、hero 渐变头图 + 图标、KV grid 字段展示、关闭按钮)。 4. **字段内容**:查看弹窗 SHALL 展示该行记录的所有业务字段(含状态映射、字典翻译),不展示 `id`、`del_flag` 等技术字段。长文本/备注字段 SHALL 使用 `span: true` 跨两列展示。 5. **区块标题样式**:查看弹窗中各区块标题(`SystemViewDialog` 的 section title,渲染为 `h3`)SHALL 与字段标签(`dt`)形成明显视觉区分。区块标题 SHALL 使用深色文字(`#1e293b`)配合左侧主色竖条装饰(`border-left: 3px solid #10b981`),字号 14px、`font-weight: 600`;字段标签 SHALL 保持为辅助灰色(`#64748b`)、`font-weight: 500`。区块标题样式 SHALL NOT 与字段标签使用相同颜色或相近字重。 #### Scenario: 新增系统管理列表页 - **WHEN** 在系统管理模块下新增标准列表页面 - **THEN** 操作列首位 SHALL 包含「查看」按钮,且 `el-table` SHALL 绑定 `@row-dblclick="handleView"` 事件 - **THEN** 点击「查看」按钮或双击行 SHALL 打开同一 `SystemViewDialog` 弹窗 #### Scenario: 现有页面补充查看弹窗 - **WHEN** 为现有列表页补充查看弹窗 - **THEN** SHALL 同时添加操作列「查看」按钮与 `@row-dblclick` 双击事件,两者调用同一个 `handleView` 函数 #### Scenario: Code review 查看弹窗 - **WHEN** PR 修改列表页面且页面包含 `el-table` - **THEN** 审查者 SHALL 确认 `el-table` 绑定了 `@row-dblclick` 事件,且操作列包含「查看」按钮 ### Requirement: 查看弹窗中编码字段展示为可读标签 `web-admin` 中所有只读查看弹窗(`SystemViewDialog` 及类似组件)中,存储为编码(code / value)的字段 SHALL 展示为对应的人类可读标签(label),SHALL NOT 直接展示原始编码值。这包括但不限于:字典编码→字典标签、状态编码→状态文本、站点类型编码→类型名称等。 对于多值编码字段(如逗号分隔的编码串),SHALL 逐个编码转换为标签后以顿号(`、`)连接展示;无匹配标签时 SHALL 回退展示原始编码值。 #### Scenario: 站点管理查看弹窗 - **WHEN** 用户点击站点管理列表中某行的「查看」按钮 - **THEN** 「经营范围」字段 SHALL 展示为编码对应的标签文本(如"蔬菜、水果"),SHALL NOT 展示原始编码串(如"1,2") #### Scenario: 新增包含字典编码字段的查看弹窗 - **WHEN** 新增列表页的查看弹窗中包含字典编码、状态编码等字段 - **THEN** 实现 SHALL 使用对应字典选项(`businessScopeOptions`、`statusOptions` 等)将编码翻译为标签后展示 #### Scenario: Code review 查看弹窗编码展示 - **WHEN** PR 修改查看弹窗中涉及编码类字段的展示 - **THEN** 审查者 SHALL 确认字段值已翻译为可读标签,SHALL NOT 出现原始编码值 ### Requirement: 排序字段不在界面默认展示 `web-admin` 中所有列表页的表格列、新增弹窗表单、编辑弹窗表单 SHALL NOT 默认包含排序字段(`sortOrder`、`orderNum`、`排序`等)。排序字段仅在产品明确要求时按需添加,不应作为默认 UI 元素。 #### Scenario: 新增标准列表页 - **WHEN** 新增标准 CRUD 列表页(含新增/编辑弹窗) - **THEN** 表格列和弹窗表单中 SHALL NOT 包含排序相关字段,除非产品需求明确要求 #### Scenario: Code review 排序字段 - **WHEN** PR 新增列表页或弹窗中包含排序字段 - **THEN** 审查者 SHALL 确认产品需求中有明确排序要求,否则 SHALL 要求移除 ### Requirement: 筛选区关键词字段排第一 `web-admin` 列表页筛选区(`.filter-block` 内 `.filter-grid`)若包含**关键词**模糊检索字段(`label` 为「关键词」或等效语义、`el-input` 绑定 `keyword` 等),该字段 SHALL 作为 `.filter-grid` 中**第一个** `el-form-item`,位于下拉筛选、状态筛选、日期范围等条件字段之前。落地参考:`#/demo/unit`、`#/demo/user` 及质检模块「入库放行审核」页。 #### Scenario: 新增含关键词的列表页 - **WHEN** 在列表页筛选区同时提供关键词输入与下拉/日期等筛选条件 - **THEN** 「关键词」对应的 `el-form-item` SHALL 位于 `.filter-grid` 首位 #### Scenario: 入库放行审核筛选区 - **WHEN** 用户打开质检模块「入库放行审核」列表页 - **THEN** 筛选区 `.filter-grid` 中第一个字段 SHALL 为「关键词」,其后依次为协议类别、审核结论、入库状态等条件 #### Scenario: Code review 筛选区字段顺序 - **WHEN** PR 新增或修改列表页 `.filter-block` 筛选字段 - **THEN** 审查者 SHALL 确认存在关键词字段时其位于 `.filter-grid` 第一位 ### Requirement: 列表筛选自动触发搜索 `web-admin` 所有列表页的筛选表单(`.filter-block` 内 `searchForm` / `filterForm`)SHALL 使用 `watchDebounced`(来自 `@vueuse/core`,已 auto-import)对筛选表单做 deep watch,当任意字段值变更后 **300ms** 防抖自动触发搜索(重置页码为 1 并调用数据加载函数),无需用户手动点击「搜索」按钮。「搜索」按钮 SHALL 保留作为手动触发入口。 #### 实现模式 ```typescript watchDebounced( searchForm, // 或 filterForm () => { pagination.page = 1 loadData() }, { debounce: 300, deep: true } ) ``` 对于含有**独立日期范围选择器**(`dateRange` ref,不属于 searchForm)的页面(如 `material-stock-log`、`qc-material-usage`),SHALL 额外添加 `watch(dateRange, ...)` 监听,在日期变更时同样触发搜索。 #### Scenario: 单字段筛选自动搜索 - **WHEN** 用户在筛选区关键词输入框中键入内容 - **THEN** 停止输入 300ms 后 SHALL 自动执行搜索,无需点击「搜索」按钮 #### Scenario: 多字段筛选自动搜索 - **WHEN** 列表页筛选区包含关键词、下拉选择、日期选择等多个筛选控件 - **AND** 用户修改其中任意一个筛选字段的值 - **THEN** 变更后 300ms SHALL 自动执行搜索 #### Scenario: 日期范围筛选 - **WHEN** 页面使用独立的 `dateRange` ref(非 searchForm 内字段)进行日期范围筛选 - **THEN** SHALL 额外使用 `watch(dateRange, ...)` 监听变更并触发搜索 #### Scenario: 保留手动搜索 - **WHEN** 用户点击筛选区的「搜索」按钮 - **THEN** SHALL 立即执行搜索(无防抖),与自动搜索行为并行可用 #### Scenario: Code review 自动搜索 - **WHEN** PR 新增或修改列表页筛选区 - **THEN** 审查者 SHALL 确认筛选表单已添加 `watchDebounced` 自动搜索,且 `debounce` 不超过 500ms ### Requirement: 附件上传卡片化布局 `web-admin` 弹窗表单中的附件上传与展示 SHALL 采用**卡片化布局**,实现与交互以 `web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue` 中「协议附件」为权威参考;储存证等业务弹窗(如 `web-admin/src/views/cell-storage/storage-cert/index.vue`)SHALL 复用同一套 DOM 结构、类名与样式规则,仅调整 `el-form-item` 的 `label` 与上传 `folder` 等业务参数。 #### 布局与视觉 1. **结构**:`el-form-item`(建议 `class="form-col-span-2"` 占满弹窗栅格宽)→ `.attachment-upload` → `.attachment-list`(flex 行)→ 上传卡片 + 若干 `.attachment-item`;`.upload-hint` 在 flex 行**下方**独立一行。 2. **上传卡片**:宽 **140px**、高 **96px**;`border: 1px dashed #a7f3d0`;`background: #f0fdf4`;`border-radius: 8px`;居中 `Plus`(size 28,`#10b981`)+ 文案「上传附件」(`.upload-card-text`,12px `#10b981` font-weight 500)。hover:`border-color: #10b981`、`border-style: solid`、`background: #ecfdf5`。 3. **附件卡片**:宽 **140px**;预览区高 **96px**;`border: 1px solid #e2e8f0`;`border-radius: 8px`;hover:`border-color: #10b981`、`box-shadow: 0 2px 8px rgba(16, 185, 129, 0.1)`。 4. **预览区**(`.attachment-preview`):背景 `#f8fafc`;图片已上传且 `fileUrl` 存在时显示缩略图(`object-fit: cover`),可点击预览;图片上传中无 URL 时显示 `Loading`;视频显示 `.attachment-preview__icon--video`(`#ede9fe` / `#7c3aed` + `VideoPlay`);其他文件显示 `.attachment-preview__icon--doc`(`#e0f2fe` / `#0284c7` + `Document`)。 5. **卡片底部**(`.attachment-footer`):文件名 `.attachment-name`(11px `#334155`,最多 2 行省略,`title` 为完整文件名);`.attachment-actions` 居中排列 **下载**(`Download`)、**删除**(`Delete`,`.action-btn--danger` hover 红色)。 6. **上传中**:`.attachment-item.is-uploading` → `opacity: 0.6`、`pointer-events: none`。 7. **提示**:`.upload-hint` — 11px `#94a3b8`,`margin-top: 8px`;默认文案:「支持图片、视频、PDF、Word、Excel等文件,单文件不超过 20MB」。 8. **色板**:SHALL 使用 Fresh Mint(`#10b981`、`#a7f3d0`、`#f0fdf4`、`#ecfdf5`);SHALL NOT 使用小程序主色 `#0a9f6d`。 9. **禁止**:SHALL NOT 使用 `el-upload` 的 `list-type="picture-card"`、默认 `file-list` 展示,或单独一行纯文字「选择文件」按钮替代上传卡片。 #### `el-upload` 配置 | 属性 | 值 | |------|-----| | `:auto-upload` | `false` | | `:show-file-list` | `false` | | `:multiple` | `true` | | `accept` | `image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar` | | `@change` | 选中后立即走上传逻辑(见下) | 上传卡片 SHALL 作为 `.attachment-list` 的**第一个子元素**,且始终渲染(不因已有附件数量而隐藏)。 #### 数据模型与上传逻辑 附件列表 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`);图片用 ``,视频用 `