Files
zkxw/docs/superpowers/specs/2025-05-25-pilot-application-dialog-refactor-design.md
T
2026-05-26 12:32:19 +08:00

436 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 中试服务申请单 — 详情页改弹窗 + 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 包裹** | `<DialogFormBody :loading="loading" body-class="application-dialog-body min-h-[200px]">` |
| 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"`(确定),都带 `<el-icon>` |
### 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
<el-dialog
:model-value="modelValue"
@update:model-value="emit('update:modelValue', $event)"
:title="dialogTitle"
width="1600px"
class="dialog-form-layout application-detail-dialog"
align-center
destroy-on-close
:close-on-click-modal="false"
>
<div
class="application-detail-body"
:class="{ 'is-loading': detailLoading }"
v-loading="detailLoading"
element-loading-text="加载中…"
>
<!-- 照抄 ProtocolViewDialog 的 detail-tabs sticky 导航 -->
<div class="detail-tabs">
<div
v-for="tab in detailTabs"
:key="tab.key"
class="detail-tab"
:class="{ active: activeTab === tab.key }"
@click="scrollToTab(tab.key)"
>
{{ tab.label }}
</div>
</div>
<template v-if="applicationDetail">
<!-- KPI 摘要条(照抄 ProtocolViewDialog) -->
<div class="kpi-strip">...</div>
<!-- 6个Tab内容区域 -->
...
</template>
</div>
</el-dialog>
```
- `width="1600px"` — 与 ProtocolViewDialog 查看弹窗一致(查看类弹窗用更宽尺寸)
- `application-detail-dialog` — 弹窗级样式
- `application-detail-body` — 与 `protocol-view-body` 一致的灰底滚动容器
- 直接用 `div + v-loading` 替代 `DialogFormBody`(与 ProtocolViewDialog 保持一致)
### 3.4 顶部区域改造
**移除**:原详情页的 `<header class="page-head">` 返回按钮和标题栏(弹窗自带关闭按钮)
**保留**:状态操作按钮(开始/完成/作废),移入弹窗 `#header` 自定义 slot 右侧
```vue
<!-- 照抄 ProtocolFormDialog 的 header 布局风格 -->
<template #header>
<div class="detail-dialog-header">
<div class="detail-dialog-header-left">
<h3>申请单详情</h3>
<span v-if="applicationDetail.applicationNo" class="detail-code">
{{ applicationDetail.applicationNo }}
</span>
<el-tag
v-if="applicationDetail.applicationStatus"
:type="applicationStatusTagType(applicationDetail.applicationStatus)"
size="large"
>
{{ applicationStatusLabel(applicationDetail.applicationStatus) }}
</el-tag>
</div>
<div class="detail-dialog-header-right">
<el-button v-if="..." class="btn-main" type="primary" @click="handleStart">
<el-icon><VideoPlay /></el-icon>开始
</el-button>
<el-button v-if="..." class="btn-main" type="primary" @click="handleComplete">
<el-icon><CircleCheck /></el-icon>完成
</el-button>
<el-button v-if="..." class="btn-op-item op-warn" type="danger" @click="handleVoid">
<el-icon><CircleClose /></el-icon>作废
</el-button>
</div>
</div>
</template>
```
### 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
<ApplicationDetailDialog
v-model="detailDialogVisible"
:application-id="detailApplicationId"
@status-changed="onDetailStatusChanged"
/>
```
---
## 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 | 响应式断点 |