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

479 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合规修复 实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将中试服务申请单详情页从独立路由改为弹窗形式(照抄采集协议单的查看/编辑弹窗风格),并修复列表页按钮图标缺失。
**Architecture:** 新建 `ApplicationDetailDialog.vue`,从 `ApplicationDetail.vue` 复制全部业务逻辑,改 props-driven 弹窗接口。列表页 `index.vue` 引入弹窗组件替换路由跳转。照抄 `ProtocolViewDialog`(KPI+data-grid)和 `ProtocolFormDialog`(form-grid+section-card)的 CSS 风格。
**Tech Stack:** Vue 3 + Element Plus + TypeScript + SCSS
**设计规格:** `docs/superpowers/specs/2025-05-25-pilot-application-dialog-refactor-design.md`
---
## 文件结构
| 文件 | 操作 | 职责 |
|------|------|------|
| `src/views/pilot-service/pilot-application/ApplicationDetailDialog.vue` | **新建** | 详情弹窗组件,props-driven,照抄 ProtocolViewDialog + ProtocolFormDialog 风格 |
| `src/views/pilot-service/pilot-application/index.vue` | **修改** | 列表页:引入弹窗组件、替换路由跳转、补按钮图标 |
| `src/views/pilot-service/pilot-application/ApplicationDetail.vue` | **删除** | 被新组件完全替代 |
| `src/router/modules/sqxp.ts` | **修改** | 删除 PilotApplicationDetail 路由定义 |
**参考文件(照抄风格源,只读不修改):**
- `src/views/cell-storage/protocol/ProtocolViewDialog.vue` — KPI 摘要条、data-grid、section-card CSS
- `src/views/cell-storage/protocol/ProtocolFormDialog.vue` — form-grid、upload-card CSS
---
## Task 1: 创建 ApplicationDetailDialog.vue 弹窗壳层 + 基本信息Tab
**Files:**
- Create: `src/views/pilot-service/pilot-application/ApplicationDetailDialog.vue`
- Read: `src/views/pilot-service/pilot-application/ApplicationDetail.vue`(源文件,复制逻辑)
- Read: `src/views/cell-storage/protocol/ProtocolViewDialog.vue`(照抄 KPI + data-grid + section-card CSS)
- [ ] **Step 1: 复制 ApplicationDetail.vue 为 ApplicationDetailDialog.vue 并改造弹窗壳层**
从 `ApplicationDetail.vue` 复制全部内容到 `ApplicationDetailDialog.vue`,然后改造:
1. **替换 `<template>` 最外层**:移除 `<div class="page-container">` 和 `<header class="page-head">`(返回按钮+标题),改为 `el-dialog` 弹窗壳:
```vue
<template>
<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="加载中…"
>
<!-- 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 摘要条 -->
<div class="kpi-strip">
<div class="kpi-card">
<div class="kpi-card__icon"><el-icon><Document /></el-icon></div>
<div class="kpi-card__main">
<div class="kpi-label">申请单号</div>
<div class="kpi-value kpi-value--emphasis">{{ displayValue(applicationDetail.applicationNo) }}</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-card__icon"><el-icon><CircleCheck /></el-icon></div>
<div class="kpi-card__main">
<div class="kpi-label">状态</div>
<div class="kpi-value">
<el-tag :type="applicationStatusTagType(applicationDetail.applicationStatus)" size="small">
{{ applicationStatusLabel(applicationDetail.applicationStatus) }}
</el-tag>
</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-card__icon"><el-icon><User /></el-icon></div>
<div class="kpi-card__main">
<div class="kpi-label">渠道名称</div>
<div class="kpi-value">{{ displayValue(applicationDetail.channelName) }}</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-card__icon"><el-icon><Calendar /></el-icon></div>
<div class="kpi-card__main">
<div class="kpi-label">创建时间</div>
<div class="kpi-value">{{ displayValue(applicationDetail.createTime) }}</div>
</div>
</div>
</div>
<!-- 原 el-tabs 内容迁移到这里 -->
<div id="tab-basic" class="form-tab-pane">
<!-- 申请单信息(只读,用 data-grid 替代 el-descriptions) -->
<div class="section-card section-card--white">
<div class="section-title"><h3>申请单信息</h3></div>
<div class="data-grid data-grid--4">
<div class="data-item"><div class="data-label">申请单号</div><div class="data-value">{{ displayValue(applicationDetail.applicationNo) }}</div></div>
<div class="data-item"><div class="data-label">渠道名称</div><div class="data-value">{{ displayValue(applicationDetail.channelName) }}</div></div>
<div class="data-item"><div class="data-label">服务订单名称</div><div class="data-value">{{ displayValue(applicationDetail.serviceOrderNo) }}</div></div>
<div class="data-item"><div class="data-label">状态</div><div class="data-value"><el-tag :type="applicationStatusTagType(applicationDetail.applicationStatus)" size="small">{{ applicationStatusLabel(applicationDetail.applicationStatus) }}</el-tag></div></div>
<div class="data-item"><div class="data-label">创建时间</div><div class="data-value">{{ displayValue(applicationDetail.createTime) }}</div></div>
<div class="data-item"><div class="data-label">备注</div><div class="data-value">{{ displayValue(applicationDetail.remark) }}</div></div>
</div>
</div>
<!-- 患者信息(可编辑表单,保留原 form-grid-2 布局) -->
<!-- 直接复制原 ApplicationDetail.vue 中 Tab1 的患者信息部分 -->
</div>
<!-- 其余5个Tab内容照搬原文件,每个用 form-tab-pane 包裹 -->
<!-- ... -->
</template>
</div>
<!-- 状态操作按钮(照抄原 header 中的按钮) -->
<template #header>
<div class="detail-dialog-header">
<div class="detail-dialog-header-left">
<span class="detail-dialog-title">申请单详情</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="applicationDetail.applicationStatus === 'dks'" class="btn-main" type="primary" @click="handleStart"><el-icon><VideoPlay /></el-icon>开始</el-button>
<el-button v-if="applicationDetail.applicationStatus === 'jxz'" class="btn-main" type="primary" @click="handleComplete"><el-icon><CircleCheck /></el-icon>完成</el-button>
<el-button v-if="applicationDetail.applicationStatus === 'dks' || applicationDetail.applicationStatus === 'jxz'" class="btn-op-item op-warn" type="danger" @click="handleVoid"><el-icon><CircleClose /></el-icon>作废</el-button>
</div>
</div>
</template>
</el-dialog>
<!-- 8个嵌套弹窗照搬 -->
</template>
```
2. **替换 `<script>` 的 props 接口**:
```typescript
defineOptions({ name: 'ApplicationDetailDialog' })
const props = defineProps<{
modelValue: boolean
applicationId: string
listRow?: any
}>()
const emit = defineEmits<{
(e: 'update:modelValue', val: boolean): void
(e: 'status-changed'): void
}>()
```
3. **替换数据加载方式**:移除 `onMounted` + `route.query.id`,改为:
```typescript
// 移除
// const route = useRoute()
// onMounted(() => { loadApplicationDetail(route.query.id) })
// 新增
const detailTabs = [
{ key: 'basic', label: '基本信息' },
{ key: 'infusion', label: '输注方案' },
{ key: 'checkup', label: '体检信息' },
{ key: 'delivery', label: '交付信息' },
{ key: 'release', label: '放行审核' },
{ key: 'confirm', label: '应用确认' },
]
const activeTab = ref('basic')
const scrollToTab = (key: string) => {
activeTab.value = key
const el = document.getElementById(`tab-${key}`)
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
watch(() => props.applicationId, (newId) => {
if (newId && props.modelValue) {
loadApplicationDetail(newId)
}
}, { immediate: true })
```
4. **状态操作后发射事件**:在 `handleStart`、`handleComplete`、`handleVoid` 成功后追加 `emit('status-changed')`
5. **新增图标 import**:`VideoPlay`, `CircleCheck`, `CircleClose`, `Document`, `Calendar` 等
- [ ] **Step 2: 照抄 CSS 样式**
从 `ProtocolViewDialog.vue` 的 `<style scoped>` 中复制以下 CSS 块到 `ApplicationDetailDialog.vue`:
- `.kpi-strip` / `.kpi-card` / `.kpi-card__icon` / `.kpi-card__main` / `.kpi-label` / `.kpi-value`(L2073-2143)
- `.data-grid` / `--2` / `--3` / `--4`(L2197-2212)
- `.data-item` / `.data-label` / `.data-value`(L2214-2238)
- `.section-card` / `--white` / `--green`(L2146-2168)
- `.section-title` + `::before` + `h3`(L2170-2194)
从 `ProtocolFormDialog.vue` 的 `<style scoped>` 中复制:
- `.detail-tabs` / `.detail-tab`(L2878-2912)
- `.form-tab-pane`(L2917-2924)
- `.form-grid` / `--col2` / `--col4` / `.form-col-span-2`(L2995-3023)
新增弹窗 header 样式:
```scss
.detail-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.detail-dialog-header-left {
display: flex;
align-items: center;
gap: 12px;
}
.detail-dialog-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
}
.detail-dialog-header-right {
display: flex;
align-items: center;
gap: 8px;
}
.application-detail-body {
background: #f5f7fa;
overflow: visible;
}
```
- [ ] **Step 3: 验证无编译错误**
Run: `cd web-admin && pnpm build:typecheck 2>&1 | head -50`
Expected: 无与 ApplicationDetailDialog.vue 相关的类型错误
---
## Task 2: 改造列表页 index.vue — 引入弹窗 + 补按钮图标
**Files:**
- Modify: `src/views/pilot-service/pilot-application/index.vue`
- [ ] **Step 1: 引入弹窗组件,替换路由跳转逻辑**
在 `<script>` 区域:
1. 移除 `const router = useRouter()`(如果其他地方还用到 router 则保留)
2. 新增弹窗状态变量和事件处理:
```typescript
import ApplicationDetailDialog from './ApplicationDetailDialog.vue'
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
}
// handleSubmit 中:创建成功后打开弹窗而非跳转路由
// 将 router.push({ name: 'PilotApplicationDetail', ... }) 替换为:
// detailApplicationId.value = newId
// detailDialogVisible.value = true
const onDetailStatusChanged = () => {
loadData()
}
```
3. 在 `<template>` 底部(新增申请单弹窗之后)添加弹窗组件:
```vue
<ApplicationDetailDialog
v-model="detailDialogVisible"
:application-id="detailApplicationId"
@status-changed="onDetailStatusChanged"
/>
```
- [ ] **Step 2: 补充操作列按钮的 el-icon**
在 `<template>` 操作列区域,为所有 `el-button` 补充 `<el-icon>` 前缀:
```vue
<!-- 查看 -->
<el-button class="btn-op-item primary" @click="handleView(row)">
<el-icon><View /></el-icon>查看
</el-button>
<!-- 编辑 -->
<el-button v-if="row.applicationStatus === 'dks'" class="btn-op-item" @click="handleEdit(row)">
<el-icon><Edit /></el-icon>编辑
</el-button>
<!-- 删除(popconfirm 内) -->
<template #reference>
<el-button class="btn-op-item op-warn"><el-icon><Delete /></el-icon>删除</el-button>
</template>
<!-- 开始 -->
<el-button v-if="row.applicationStatus === 'dks'" class="btn-op-item primary" type="primary" @click="handleStart(row)">
<el-icon><VideoPlay /></el-icon>开始
</el-button>
<!-- 完成 -->
<el-button v-if="row.applicationStatus === 'jxz'" class="btn-op-item primary" type="primary" @click="handleComplete(row)">
<el-icon><CircleCheck /></el-icon>完成
</el-button>
<!-- 作废 -->
<el-button v-if="..." class="btn-op-item op-warn" type="danger" @click="handleVoid(row)">
<el-icon><CircleClose /></el-icon>作废
</el-button>
```
更新 import 语句:
```typescript
import {
Plus, Search, RefreshLeft, Close, Check,
View, Edit, Delete, VideoPlay, CircleCheck, CircleClose
} from '@element-plus/icons-vue'
```
- [ ] **Step 3: 验证列表页无编译错误**
Run: `cd web-admin && pnpm build:typecheck 2>&1 | head -50`
Expected: 无与 index.vue 相关的类型错误
---
## Task 3: 清理路由 + 删除旧详情页
**Files:**
- Modify: `src/router/modules/sqxp.ts`
- Delete: `src/views/pilot-service/pilot-application/ApplicationDetail.vue`
- [ ] **Step 1: 删除 PilotApplicationDetail 路由定义**
在 `src/router/modules/sqxp.ts` 第650-656行,删除:
```typescript
{
id: '80000000000000000000000000000011',
name: 'PilotApplicationDetail',
path: 'pilot-application/detail',
component: '/pilot-service/pilot-application/ApplicationDetail',
meta: { title: '申请单详情', hidden: true }
}
```
注意保留上一行的逗号处理——删除该对象后,`PilotApplication` 路由对象后不应有尾随逗号。
- [ ] **Step 2: 删除 ApplicationDetail.vue**
```bash
rm src/views/pilot-service/pilot-application/ApplicationDetail.vue
```
- [ ] **Step 3: 全局搜索确认无其他引用**
Run: `grep -r "PilotApplicationDetail\|ApplicationDetail" web-admin/src/ --include="*.vue" --include="*.ts"`
Expected: 0 结果(所有引用已清理)
---
## Task 4: 弹窗内嵌套弹窗按钮图标补充
**Files:**
- Modify: `src/views/pilot-service/pilot-application/ApplicationDetailDialog.vue`
- [ ] **Step 1: 检查并补充弹窗内所有操作按钮的 el-icon**
在 `ApplicationDetailDialog.vue` 中,搜索所有 `el-button` 标签(不含 `el-icon` 子元素的),按 openspec 规范补充图标:
- "新增输注方案" → `Plus`
- "手动添加计划" → `Plus`
- "选择客户" → `User`
- "保存患者信息" → `Check`
- 各 Tab 中的"编辑" → `Edit`
- 各 Tab 中的"删除" → `Delete`
- 嵌套弹窗 footer 的"取消" → `Close`,"确定" → `Check`
- 其他操作按钮逐一检查
确保所有图标已在 `<script>` 中从 `@element-plus/icons-vue` import。
- [ ] **Step 2: 验证无遗漏**
Run: `grep -n '<el-button' web-admin/src/views/pilot-service/pilot-application/ApplicationDetailDialog.vue | head -50`
Expected: 手动检查每个 el-button 是否包含 `<el-icon>` 子元素
---
## Task 5: 端到端验证
**Files:** 无新增修改
- [ ] **Step 1: 构建检查**
Run: `cd web-admin && pnpm build 2>&1 | tail -20`
Expected: 构建成功,无错误
- [ ] **Step 2: Lint 检查**
Run: `cd web-admin && pnpm lint 2>&1 | tail -30`
Expected: 无 ESLint 错误(或仅预存在的无关错误)
- [ ] **Step 3: 更新知识图谱**
Run: `./scripts/update-graph.sh frontend`
Expected: 前端知识图谱更新成功
---
## Self-Review
### 1. Spec Coverage
| Spec Section | Task |
|---|---|
| 3.1 照抄清单 A1-A5 (ViewDialog) | Task 1 (弹窗壳层+CSS) |
| 3.1 照抄清单 B1-B4 (FormDialog) | Task 1 (表单CSS) |
| 3.1 照抄清单 C1-C4 (公共) | Task 1 (detail-tabs, section-card) |
| 3.2 弹窗组件接口 | Task 1 (defineProps/defineEmits) |
| 3.3 弹窗壳层配置 | Task 1 (el-dialog 1600px) |
| 3.4 顶部区域改造 | Task 1 (自定义 #header) |
| 3.5 正文结构 | Task 1 (KPI + data-grid + form-tab-pane) |
| 3.6 数据加载变更 | Task 1 (watch) |
| 3.7 列表页改造 | Task 2 (引入弹窗+替换路由) |
| 4.1 操作列按钮图标 | Task 2 (补充 el-icon) |
| 4.2 弹窗内按钮图标 | Task 4 |
| 5. 路由清理 | Task 3 |
**Gap check:** 无遗漏。
### 2. Placeholder Scan
无 TBD/TODO/占位符。
### 3. Type Consistency
- `ApplicationDetailDialog` props: `modelValue: boolean`, `applicationId: string` — 与 `index.vue` 中的使用 `v-model="detailDialogVisible"` 和 `:application-id="detailApplicationId"` 一致。
- Event `status-changed` — 与 `@status-changed="onDetailStatusChanged"` 一致。