# 中试服务申请单 — 详情页改弹窗 + UI合规修复 > 日期: 2025-05-25 > 状态: 设计待审 > 影响: web-admin 前端 only --- ## 1. 背景与目标 当前【中试服务申请单】模块有两个界面: - **列表页** `pilot-application/index.vue`(644行)— 菜单可见路由 - **详情页** `pilot-application/ApplicationDetail.vue`(2635行)— `hidden: true` 路由,通过 `router.push` 跳转 **问题**: 1. 详情页作为独立路由页面,用户需通过浏览器后退返回列表,体验割裂 2. 列表页操作列按钮缺少 `el-icon`,不符合 openspec 按钮须携带图标规范 **目标**: 1. 将详情页从独立路由改为弹窗形式,**同时参考两个参考页面**: - **查看弹窗风格**:照抄 `ProtocolViewDialog.vue`(采集协议单查看弹窗,1600px 宽,KPI 摘要条 + data-grid 只读数据展示) - **编辑/新增弹窗风格**:照抄 `ProtocolFormDialog.vue`(采集协议单新增/编辑弹窗,1380px 宽,section-card + form-grid 表单布局) 2. 一并修复列表页所有操作按钮的 `el-icon` 缺失 --- ## 2. 变更范围 | 文件 | 操作 | 说明 | |------|------|------| | `src/views/pilot-service/pilot-application/ApplicationDetailDialog.vue` | **新建** | 详情弹窗主体(照抄 ProtocolViewDialog 的 KPI + data-grid 只读展示风格) | | `src/views/pilot-service/pilot-application/index.vue` | **修改** | 引入弹窗组件,替换路由跳转逻辑,修复按钮图标 | | `src/views/pilot-service/pilot-application/ApplicationDetail.vue` | **删除** | 被 ApplicationDetailDialog.vue 完全替代 | | `src/router/modules/sqxp.ts` | **修改** | 删除 PilotApplicationDetail 路由定义(第650-656行) | **不变更**: - API 层(`src/api/admin.ts`)— 无需修改 - 状态标签工具(`src/utils/status-tag-registry.ts`)— 无需修改 - 后端代码 — 无需修改 --- ## 3. 设计方案:详情页改弹窗(照抄 ProtocolViewDialog + ProtocolFormDialog 风格) > **核心原则**:详情弹窗 = **查看模式照抄 ProtocolViewDialog** + **编辑区域照抄 ProtocolFormDialog**。 > > - **查看展示**(申请单只读信息、各Tab中的只读数据)→ 照抄 `ProtocolViewDialog.vue`(1600px宽,KPI 摘要条 + `.data-grid` + `.section-card`) > - **表单编辑**(患者信息可编辑表单、嵌套弹窗中的表单)→ 照抄 `ProtocolFormDialog.vue`(`.form-grid` + `.section-card`) > - **公共元素**(弹窗壳层、Sticky Tab 导航、状态操作按钮)→ 两者共用 ### 3.1 照抄清单:两个参考弹窗的关键风格要素 #### A. 来自 ProtocolViewDialog(查看弹窗,只读展示风格) | # | 风格要素 | ProtocolViewDialog 中的实现 | 申请单详情弹窗中的对应 | |---|----------|---------------------------|----------------------| | A1 | **弹窗宽度** | `width="1600px"` + `class="dialog-form-layout protocol-view-dialog"` | 照抄 1600px(查看类弹窗用更宽尺寸,与 ProtocolViewDialog 一致) | | A2 | **KPI 摘要条** | `.kpi-strip` 4列网格 + `.kpi-card`(渐变绿背景 #ecfdf5、图标徽章、label+value) | 用于展示:申请单号、状态、渠道名称、创建时间 等关键指标 | | A3 | **只读数据网格** | `.data-grid .data-grid--4` + `.data-item`(`.data-label` + `.data-value`) | 用于"申请单信息"只读区域(替代原 `el-descriptions`) | | A4 | **绿色背景分区** | `.section-card--green`(#f0fdf4 背景) | 可用于患者信息区(区分只读和可编辑) | | A5 | **白色背景分区** | `.section-card--white`(默认白底) | 用于申请单信息、输注方案等标准分区 | #### B. 来自 ProtocolFormDialog(编辑弹窗,表单风格) | # | 风格要素 | ProtocolFormDialog 中的实现 | 申请单详情弹窗中的对应 | |---|----------|---------------------------|----------------------| | B1 | **表单网格** | `.form-grid`(默认3列)、`.form-grid--col2`、`.form-grid--col4`、`.form-col-span-2` | 用于患者信息可编辑表单 | | B2 | **表单标签位置** | `label-position="top"` + `scroll-to-error` | 照抄 | | B3 | **附件上传卡片** | `.upload-card`(140×96px 虚线框 + Plus 图标)+ `.attachment-item` 卡片列表 | 用于诊断附件上传区域 | | B4 | **弹窗背景色** | `.protocol-dialog-body { background: #f5f7fa; overflow: visible }` | 照抄 | #### C. 公共元素(两个弹窗共用) | # | 风格要素 | 实现 | |---|----------|------| | C1 | **DialogFormBody 包裹** | `` | | C2 | **Sticky Tab 导航** | `.detail-tabs` + `.detail-tab`,绿色背景 pill 样式,`position: sticky; top: 0` | | C3 | **分区卡片** | `.section-card`(白底圆角12px、阴影、hover 效果)+ `.section-title`(绿色竖线 `::before`) | | C4 | **Footer 按钮** | 嵌套弹窗:`btn-ghost`(取消)+ `btn-main type="primary"`(确定),都带 `` | ### 3.2 弹窗组件接口 参照 `ProtocolFormDialog.vue` 的标准模式: ```typescript // ApplicationDetailDialog.vue const props = defineProps<{ modelValue: boolean // v-model 控制显隐 applicationId: string // 申请单 ID(替代 route.query.id) listRow?: any // 可选:列表行数据(用于预填标题/状态) }>() const emit = defineEmits<{ (e: 'update:modelValue', val: boolean): void (e: 'status-changed'): void // 状态操作后通知父组件刷新列表 }>() ``` ### 3.3 弹窗壳层配置(照抄 ProtocolViewDialog 查看弹窗尺寸) ```vue
{{ tab.label }}
``` - `width="1600px"` — 与 ProtocolViewDialog 查看弹窗一致(查看类弹窗用更宽尺寸) - `application-detail-dialog` — 弹窗级样式 - `application-detail-body` — 与 `protocol-view-body` 一致的灰底滚动容器 - 直接用 `div + v-loading` 替代 `DialogFormBody`(与 ProtocolViewDialog 保持一致) ### 3.4 顶部区域改造 **移除**:原详情页的 `
` 返回按钮和标题栏(弹窗自带关闭按钮) **保留**:状态操作按钮(开始/完成/作废),移入弹窗 `#header` 自定义 slot 右侧 ```vue ``` ### 3.5 正文结构:照抄两个参考弹窗的混合模式 **ProtocolViewDialog 的正文层次**(查看模式,只读数据展示): ``` div.protocol-view-body (灰底 #f5f7fa, v-loading) └─ .detail-tabs (sticky 绿色 pill 导航) └─ .kpi-strip (4列 KPI 摘要条:协议号 / 状态 / 母亲姓名 / 登记日期) └─ #view-section-basic (.section-card > .data-grid--4 > .data-item) └─ #view-section-party (.section-card--green > .data-grid--4) └─ #view-section-collection (.section-card > .data-grid--4) └─ #view-section-sample (.section-card > 样本卡片) └─ #view-section-payment (.section-card > 缴费卡片) ``` **申请单详情弹窗照抄后的正文层次**: ``` div.application-detail-body (灰底, v-loading) └─ .detail-tabs (sticky 绿色 pill 导航) 基本信息 | 输注方案 | 体检信息 | 交付信息 | 放行审核 | 应用确认 └─ .kpi-strip (4列 KPI 摘要条,照抄 ProtocolViewDialog) 申请单号 | 状态 | 渠道名称 | 创建时间 └─ #tab-basic (.section-card) ├─ 申请单信息(.data-grid--4 只读,照抄 ProtocolViewDialog) └─ 患者信息(.form-grid--col2 表单,照抄 ProtocolFormDialog) └─ #tab-infusion (.section-card) └─ 方案表格 + 展开计划列表 └─ #tab-checkup (.section-card) └─ 体检详情 + 预约/实际项目 └─ #tab-delivery (.section-card) └─ 交付列表 └─ #tab-release (.section-card) └─ 审核列表 └─ #tab-confirm (.section-card) └─ 确认列表 └─ 8个嵌套弹窗 (el-dialog z-index 自动递增,不受影响) ``` **具体照抄细节**: 1. **KPI 摘要条**(照抄 ProtocolViewDialog `.kpi-strip`) - `.kpi-strip`: `display: grid; grid-template-columns: repeat(4, 1fr); margin-bottom: 24px; padding: 6px; background: linear-gradient(135deg, #ecfdf5 0%, #f8fafc 52%, #ecfdf5 100%); border: 1px solid #a7f3d0; border-radius: 12px` - `.kpi-card`: `display: flex; align-items: flex-start; gap: 12px; padding: 14px 18px` - `.kpi-card__icon`: 40×40px 圆角图标徽章(白底 + 绿色边框 + 绿色图标) - `.kpi-label`: 11px 灰色标签 - `.kpi-value`: 15px 黑色值(强调值 `.kpi-value--emphasis`: 16px + 绿色 + 加粗) - 展示内容:申请单号、状态(el-tag)、渠道名称、创建时间 2. **只读数据展示**(照抄 ProtocolViewDialog `.data-grid`) - `.data-grid--4`: `display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px 20px` - `.data-item` > `.data-label`(12px 灰色)+ `.data-value`(14px 黑色) - **替代原详情页的 `el-descriptions` 组件**,用 `.data-grid` 做只读展示 3. **Tab 导航用 `.detail-tabs` + `.detail-tab`**(自定义样式),不用 `el-tabs` 组件 - 与 ProtocolViewDialog / ProtocolFormDialog 完全一致的 sticky 绿色 pill 导航 4. **每个 Tab 内容用 `.section-card` 包裹** - 白色区域 `.section-card--white`(默认)、绿色区域 `.section-card--green` 5. **分区标题用 `.section-title`** - `.section-title::before`: 绿色竖线 `3px × 18px; background: #10b981` 6. **表单区域**(照抄 ProtocolFormDialog `.form-grid`) - 患者信息编辑表单:`.form-grid--col2` + `label-position="top"` - `.form-col-span-2`: 跨整行(临床诊断、诊断附件) 7. **附件上传**(照抄 ProtocolFormDialog `.upload-card` 卡片化布局) - 上传触发器:140×96px 虚线框 + Plus 图标 + "上传附件" 文字 ### 3.6 数据加载方式变更 **原方式**:`onMounted` + `route.query.id` **新方式**:`watch(applicationId)` + 立即加载 ```typescript watch(() => props.applicationId, (newId) => { if (newId && props.modelValue) { loadApplicationDetail(newId) } }, { immediate: true }) ``` 弹窗关闭时(`destroy-on-close`)自动清理状态。 ### 3.7 列表页 index.vue 改造 **替换导航逻辑**: ```typescript // 移除 const router = useRouter() const handleView = (row) => router.push({ name: 'PilotApplicationDetail', query: { id: row.id } }) // 新增 const detailDialogVisible = ref(false) const detailApplicationId = ref('') const handleView = (row: any) => { detailApplicationId.value = row.id detailDialogVisible.value = true } const handleEdit = (row: any) => { detailApplicationId.value = row.id detailDialogVisible.value = true } // 创建成功后不再跳转路由,改为打开详情弹窗 const handleSubmit = async () => { // ...提交逻辑... const newId = data?.id || data?.pilotApplicationId || data if (newId) { detailApplicationId.value = newId detailDialogVisible.value = true } loadData() } // 状态变更后刷新列表 const onDetailStatusChanged = () => { loadData() } ``` **模板引入弹窗组件**: ```vue ``` --- ## 4. 设计方案:列表页 UI 合规修复 ### 4.1 操作列按钮补充 el-icon 当前操作列按钮缺少图标,需按 openspec 规范补充: | 按钮 | 图标组件 | 适用状态 | |------|----------|----------| | 查看 | `View` | 始终显示 | | 编辑 | `Edit` | 仅 `dks`(待开始) | | 删除 | `Delete` | 仅 `dks`(popconfirm 内) | | 开始 | `VideoPlay` | 仅 `dks` | | 完成 | `CircleCheck` | 仅 `jxz`(进行中) | | 作废 | `CircleClose` | 仅 `dks` / `jxz` | **新增 import**: ```typescript import { Plus, Search, RefreshLeft, Close, Check, View, Edit, Delete, VideoPlay, CircleCheck, CircleClose } from '@element-plus/icons-vue' ``` ### 4.2 详情弹窗内按钮图标 详情弹窗顶部状态操作按钮(开始/完成/作废)也需补充 el-icon: - 开始 → `VideoPlay` - 完成 → `CircleCheck` - 作废 → `CircleClose` 弹窗内各 Tab 的操作按钮(新增输注方案、编辑、删除等)也需补充 el-icon。 ### 4.3 其他合规项 | 检查项 | 当前状态 | 操作 | |--------|----------|------| | 状态字段用 el-tag | ✅ 已合规 | 无需修改 | | 操作列 fixed="right" | ✅ 已合规 | 无需修改 | | min-width 而非 width | ✅ 已合规 | 无需修改 | | 筛选区搜索/重置有图标 | ✅ 已合规 | 无需修改 | | 弹窗 footer 有图标 | ✅ 已合规 | 无需修改 | | 排序字段默认不展示 | ✅ 无排序字段 | 无需修改 | --- ## 5. 路由清理 删除 `src/router/modules/sqxp.ts` 中的 PilotApplicationDetail 路由定义: ```typescript // 删除以下路由(第650-656行) { id: '80000000000000000000000000000011', name: 'PilotApplicationDetail', path: 'pilot-application/detail', component: '/pilot-service/pilot-application/ApplicationDetail', meta: { title: '申请单详情', hidden: true } } ``` --- ## 6. 不在范围内 - 后端 API 变更 - 数据库变更 - 新增业务功能 - 其他页面的 UI 合规修复 - 状态标签注册表变更 --- ## 7. 风险与缓解 | 风险 | 缓解措施 | |------|----------| | 2635行组件改造量大 | 保留原有逻辑,仅改 props 接口和容器层,不重构内部 | | 嵌套弹窗 z-index 层级 | el-dialog 自动管理 z-index 递增,不受影响;验证点:打开详情弹窗后逐个测试嵌套弹窗 | | `destroy-on-close` 导致状态丢失 | 符合预期:每次打开弹窗重新加载数据,与原来路由跳转行为一致 | | 列表页操作列按钮变多导致换行 | 操作列已设置 `min-width="280"`,验证各状态下的按钮宽度是否足够 | --- ## 8. 照抄样式速查表(从两个参考弹窗复制) ### 来自 ProtocolViewDialog.vue(查看弹窗 — 只读展示风格) | CSS 类/块 | ProtocolViewDialog 行号 | 用途 | |-----------|------------------------|------| | `.protocol-view-body` | ~L15-19 | 弹窗 body 灰底 + v-loading 容器 | | `.kpi-strip` / `.kpi-card` / `.kpi-label` / `.kpi-value` | L2073-2143 | KPI 摘要条(4列绿渐变背景 + 图标徽章) | | `.data-grid` / `--2` / `--3` / `--4` | L2197-2212 | 只读数据网格 | | `.data-item` / `.data-label` / `.data-value` | L2214-2238 | 只读数据项(label + value) | | `.section-card` / `--white` / `--green` | L2146-2168 | 分区卡片 | | `.section-title` + `::before` + `h3` | L2170-2194 | 分区标题(绿色竖线) | | `.detail-tabs` / `.detail-tab` | 同 ProtocolFormDialog | Sticky 绿色 pill Tab 导航 | ### 来自 ProtocolFormDialog.vue(编辑弹窗 — 表单风格) | CSS 类/块 | ProtocolFormDialog 行号 | 用途 | |-----------|------------------------|------| | `.protocol-dialog-body` | L2867-2870 | 弹窗 body 灰底背景 | | `.form-tab-pane` | L2917-2924 | Tab 内容区容器 | | `.form-grid` / `--col2` / `--col4` / `.form-col-span-2` | L2995-3023 | 表单网格布局 | | `.upload-card` / `.attachment-item` | ~L3100+ | 附件上传卡片 | | `@media (max-width: 900px)` | L3520-3543 | 响应式断点 |