436 lines
17 KiB
Markdown
436 lines
17 KiB
Markdown
# 中试服务申请单 — 详情页改弹窗 + 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 | 响应式断点 |
|