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

17 KiB
Raw Blame History

中试服务申请单 — 详情页改弹窗 + 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 的标准模式:

// 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 自动递增,不受影响)

具体照抄细节:

  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) + 立即加载

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 响应式断点