17 KiB
中试服务申请单 — 详情页改弹窗 + UI合规修复
日期: 2025-05-25 状态: 设计待审 影响: web-admin 前端 only
1. 背景与目标
当前【中试服务申请单】模块有两个界面:
- 列表页
pilot-application/index.vue(644行)— 菜单可见路由 - 详情页
pilot-application/ApplicationDetail.vue(2635行)—hidden: true路由,通过router.push跳转
问题:
- 详情页作为独立路由页面,用户需通过浏览器后退返回列表,体验割裂
- 列表页操作列按钮缺少
el-icon,不符合 openspec 按钮须携带图标规范
目标:
- 将详情页从独立路由改为弹窗形式,同时参考两个参考页面:
- 查看弹窗风格:照抄
ProtocolViewDialog.vue(采集协议单查看弹窗,1600px 宽,KPI 摘要条 + data-grid 只读数据展示) - 编辑/新增弹窗风格:照抄
ProtocolFormDialog.vue(采集协议单新增/编辑弹窗,1380px 宽,section-card + form-grid 表单布局)
- 查看弹窗风格:照抄
- 一并修复列表页所有操作按钮的
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 的标准模式:
// 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 查看弹窗尺寸)
<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 右侧
<!-- 照抄 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 自动递增,不受影响)
具体照抄细节:
-
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)、渠道名称、创建时间
-
只读数据展示(照抄 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做只读展示
-
Tab 导航用
.detail-tabs+.detail-tab(自定义样式),不用el-tabs组件- 与 ProtocolViewDialog / ProtocolFormDialog 完全一致的 sticky 绿色 pill 导航
-
每个 Tab 内容用
.section-card包裹- 白色区域
.section-card--white(默认)、绿色区域.section-card--green
- 白色区域
-
分区标题用
.section-title.section-title::before: 绿色竖线3px × 18px; background: #10b981
-
表单区域(照抄 ProtocolFormDialog
.form-grid)- 患者信息编辑表单:
.form-grid--col2+label-position="top" .form-col-span-2: 跨整行(临床诊断、诊断附件)
- 患者信息编辑表单:
-
附件上传(照抄 ProtocolFormDialog
.upload-card卡片化布局)- 上传触发器:140×96px 虚线框 + Plus 图标 + "上传附件" 文字
3.6 数据加载方式变更
原方式:onMounted + route.query.id
新方式:watch(applicationId) + 立即加载
watch(() => props.applicationId, (newId) => {
if (newId && props.modelValue) {
loadApplicationDetail(newId)
}
}, { immediate: true })
弹窗关闭时(destroy-on-close)自动清理状态。
3.7 列表页 index.vue 改造
替换导航逻辑:
// 移除
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()
}
模板引入弹窗组件:
<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:
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 路由定义:
// 删除以下路由(第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 | 响应式断点 |