Files
zkxw/docs/superpowers/plans/2026-05-17-protocol-view-dialog-redesign.md
T
2026-05-17 23:08:38 +08:00

862 lines
26 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.
# 采集协议单查看弹窗 方案D风格改造 实现计划
> **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:** 将 ProtocolViewDialog.vue 的查看弹窗 UI 改造为方案D风格(KPI摘要条 + 圆角卡片分区 + 🤱👨👶子卡片 + 3列网格),同时保留所有现有信息项不遗漏。
**Architecture:** 单文件改造,只改 `ProtocolViewDialog.vue` 的 `<template>` 和 `<style>` 部分。`<script>` 逻辑层不变(数据获取、格式化函数、computed 全部保留)。三个页面(采集协议单、缴费信息、采集协议单审核)复用同一个组件,改一个文件即全部生效。
**Tech Stack:** Vue 3 + Element Plus + SCSS (scoped)。组件内纯 CSS 改造,不引入新依赖。
**关键约束:**
- 所有现有信息项不可遗漏(字段对齐现有 view-kv-grid 中的每个 dt/dd)
- 保留 `showAuditHistory` prop 的条件展示逻辑
- 保留文件预览/下载功能(ProtocolAttachmentReadonly、openPreview、downloadFile)
- 保留 sticky tab 导航锚点
- 保留附件卡片样式(ProtocolAttachmentReadonly 组件不变)
- 配色严格遵循 Fresh Mint 色板:primary #10b981,dark #059669,禁止使用小程序色 #0a9f6d
---
## 文件变更清单
| 文件 | 操作 | 说明 |
|------|------|------|
| `web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue` | **修改** | template + style 全面改造,script 不动 |
---
## 参考设计对照表
### 现有字段 → 方案D映射
**Tab 1: 基本信息**
| 现有字段 | 方案D位置 |
|---------|----------|
| 协议号 | KPI摘要卡 + 基本信息卡片 |
| 协议状态 | KPI摘要卡(状态点) + 基本信息卡片(badge) |
| 登记日期 | KPI摘要卡 + 基本信息卡片 |
| 采集专员 | 基本信息卡片 |
| 创建时间 | 基本信息卡片 |
| 协议附件 | 基本信息卡片 — 附件行 |
| 备注 | 基本信息卡片 — 备注行 |
**Tab 2: 客户信息**
| 现有字段 | 方案D位置 |
|---------|----------|
| 母亲: 关联客户/姓名/电话/身份证/出生年月/民族/住址 | 🤱 母亲信息子卡片 (2列grid) |
| 父亲: 姓名/电话/身份证 | 👨 父亲信息子卡片 (2列grid) |
| 新生儿: 姓名/身份证/分娩时间/性别/分娩方式/胎次/体重/评分1min/评分5min | 👶 新生儿信息子卡片 (2列grid) |
**Tab 3: 采集信息**
| 现有字段 | 方案D位置 |
|---------|----------|
| 采集地区/采集医院/采集医护/销售人员/样本采集时间/母血采集时间 | 白色卡片 — 3列grid |
**Tab 4: 样本及运输信息**
| 现有字段 | 方案D位置 |
|---------|----------|
| 样本表格(11列) | 白色卡片 — el-table 保留,样式微调 |
**Tab 5: 缴费信息**
| 现有字段 | 方案D位置 |
|---------|----------|
| 缴费模式/合同金额/分期相关字段 | 灰色卡片 — 缴费主表指标卡 |
| 缴费明细卡片 | 灰色卡片 — 明细卡片区(含退费场景) |
| 缴费流程轨道 | 灰色卡片 — mini-flow 横向节点 |
**Tab 6: 审核记录** (条件展示)
| 现有字段 | 方案D位置 |
|---------|----------|
| 审核记录表格 | 白色卡片 — el-table 保留 |
---
### Task 1: 改造 template — 添加 KPI 摘要条 + 整体结构
**Files:**
- Modify: `web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue` (template 部分)
- [ ] **Step 1: 在 `<div class="protocol-view-body">` 内、`<template v-if="bundle">` 之前,添加 KPI 摘要条**
用 4 个 `kpi-card` 替代之前无顶部摘要的设计。KPI 条显示:协议编号、协议状态(带状态点)、母亲姓名、登记日期。所有值来自 `protocol` 和 `partyCustomer` computed。
```html
<!-- KPI Summary Strip -->
<div v-if="bundle" class="kpi-strip">
<div class="kpi-card">
<div class="kpi-label">协议编号</div>
<div class="kpi-value">{{ dash(protocol?.protocolNo) }}</div>
</div>
<div class="kpi-card">
<div class="kpi-label">协议状态</div>
<div class="kpi-value">
<span class="status-dot" :class="statusDotClass(String(protocol?.protocolStatus || ''))" />
{{ protocolStatusLabel(String(protocol?.protocolStatus || '')) }}
</div>
</div>
<div class="kpi-card">
<div class="kpi-label">母亲姓名</div>
<div class="kpi-value">{{ dash(partyCustomer?.motherName) }}</div>
</div>
<div class="kpi-card">
<div class="kpi-label">登记日期</div>
<div class="kpi-value">{{ formatYmd(protocol?.registerDate) }}</div>
</div>
</div>
```
- [ ] **Step 2: 在 script 中添加 `statusDotClass` 函数**
在现有 `protocolStatusLabel` 函数附近添加:
```typescript
function statusDotClass(s: string): string {
const m: Record<string, string> = {
dsx: 'pending',
sxz: 'active',
ygq: 'expired',
yzf: 'void',
zky: 'research'
}
return m[s] || ''
}
```
- [ ] **Step 3: 将 `<template v-if="bundle">` 内的 6 个 section 改为卡片包裹**
每个 `form-tab-pane` 改为 `section-card` 结构。保留所有 `id="view-section-*"` 用于滚动锚点。
---
### Task 2: 改造基本信息 section
**Files:**
- Modify: `web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue` (基本信息模板)
- [ ] **Step 1: 替换基本信息区域的 HTML 结构**
将现有的 `<div id="view-section-basic" class="form-tab-pane">` 替换为方案D卡片结构:
```html
<!-- 基本信息 -->
<div id="view-section-basic" class="section-card section-card--white">
<div class="section-title"><h3>基本信息</h3></div>
<div class="data-grid data-grid--3">
<div class="data-item">
<div class="data-label">协议号</div>
<div class="data-value">{{ dash(protocol?.protocolNo) }}</div>
</div>
<div class="data-item">
<div class="data-label">协议状态</div>
<div class="data-value">
<span class="badge" :class="statusBadgeClass(String(protocol?.protocolStatus || ''))">
{{ protocolStatusLabel(String(protocol?.protocolStatus || '')) }}
</span>
</div>
</div>
<div class="data-item">
<div class="data-label">登记日期</div>
<div class="data-value">{{ formatYmd(protocol?.registerDate) }}</div>
</div>
<div class="data-item">
<div class="data-label">采集专员</div>
<div class="data-value">{{ collectorDisplay }}</div>
</div>
<div class="data-item">
<div class="data-label">创建时间</div>
<div class="data-value">{{ dash(protocol?.createTime) }}</div>
</div>
</div>
<!-- 附件 -->
<div class="attachment-row" v-if="protocolAttachments.length">
<div class="data-label">协议附件</div>
<ProtocolAttachmentReadonly
:files="protocolAttachments"
@preview="openPreview"
@download="downloadFile"
/>
</div>
<!-- 备注 -->
<div class="remark-row" v-if="protocol?.remark">
<div class="data-item data-item--full">
<div class="data-label">备注</div>
<div class="data-value data-value--muted">{{ dash(protocol?.remark) }}</div>
</div>
</div>
</div>
```
- [ ] **Step 2: 在 script 中添加 `statusBadgeClass` 函数**
```typescript
function statusBadgeClass(s: string): string {
const m: Record<string, string> = {
dsx: 'badge--amber',
sxz: 'badge--green',
ygq: 'badge--gray',
yzf: 'badge--red',
zky: 'badge--purple'
}
return m[s] || 'badge--gray'
}
```
---
### Task 3: 改造客户信息 section — 🤱👨👶 子卡片
**Files:**
- Modify: `web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue` (客户信息模板)
- [ ] **Step 1: 替换客户信息区域的 HTML 结构**
将现有 `view-section-party` 的 `form-tab-pane` + 虚线分隔 + `detail-section-title` 改为绿色卡片 + 3 列子卡片:
```html
<!-- 客户信息 -->
<div id="view-section-party" class="section-card section-card--green">
<div class="section-title"><h3>客户信息</h3></div>
<div class="client-sub-cards">
<!-- 🤱 母亲信息 -->
<div class="client-sub-card">
<div class="sub-card-header">
<span class="sub-card-icon">🤱</span>
<h4>母亲信息</h4>
</div>
<div class="data-grid data-grid--2">
<div class="data-item">
<div class="data-label">关联客户</div>
<div class="data-value">{{ partyCustomer?.customerId ? dash(partyCustomer?.motherName) : '—' }}</div>
</div>
<div class="data-item">
<div class="data-label">姓名</div>
<div class="data-value">{{ dash(partyCustomer?.motherName) }}</div>
</div>
<div class="data-item">
<div class="data-label">联系电话</div>
<div class="data-value">{{ dash(partyCustomer?.motherPhone) }}</div>
</div>
<div class="data-item">
<div class="data-label">身份证号</div>
<div class="data-value">{{ dash(partyCustomer?.motherIdCard) }}</div>
</div>
<div class="data-item">
<div class="data-label">出生年月</div>
<div class="data-value">{{ formatYm(partyCustomer?.motherBirthYm) }}</div>
</div>
<div class="data-item">
<div class="data-label">民族</div>
<div class="data-value">{{ dash(partyCustomer?.motherEthnic) }}</div>
</div>
</div>
<div class="sub-card-full-row">
<div class="data-label">家庭住址</div>
<div class="data-value data-value--muted">{{ dash(partyCustomer?.motherAddress) }}</div>
</div>
</div>
<!-- 👨 父亲信息 -->
<div class="client-sub-card">
<div class="sub-card-header">
<span class="sub-card-icon">👨</span>
<h4>父亲信息</h4>
</div>
<div class="data-grid data-grid--2">
<div class="data-item">
<div class="data-label">姓名</div>
<div class="data-value">{{ dash(partyCustomer?.fatherName) }}</div>
</div>
<div class="data-item">
<div class="data-label">电话</div>
<div class="data-value">{{ dash(partyCustomer?.fatherPhone) }}</div>
</div>
<div class="data-item data-item--full">
<div class="data-label">身份证号</div>
<div class="data-value">{{ dash(partyCustomer?.fatherIdCard) }}</div>
</div>
</div>
</div>
<!-- 👶 新生儿信息 -->
<div class="client-sub-card">
<div class="sub-card-header">
<span class="sub-card-icon">👶</span>
<h4>新生儿信息</h4>
</div>
<div class="data-grid data-grid--2">
<div class="data-item">
<div class="data-label">姓名</div>
<div class="data-value">{{ dash(partyCustomer?.newbornName) }}</div>
</div>
<div class="data-item">
<div class="data-label">身份证</div>
<div class="data-value">{{ dash(partyCustomer?.newbornIdCard) }}</div>
</div>
<div class="data-item">
<div class="data-label">分娩时间</div>
<div class="data-value">{{ dash(partyCustomer?.newbornDeliveryTime) }}</div>
</div>
<div class="data-item">
<div class="data-label">性别</div>
<div class="data-value">{{ newbornSexLabel(String(partyCustomer?.newbornSex || '')) }}</div>
</div>
<div class="data-item">
<div class="data-label">分娩方式</div>
<div class="data-value">{{ deliveryModeLabel(String(partyCustomer?.deliveryMode || '')) }}</div>
</div>
<div class="data-item">
<div class="data-label">胎次</div>
<div class="data-value">{{ dash(partyCustomer?.birthOrder) }}</div>
</div>
<div class="data-item">
<div class="data-label">体重(克)</div>
<div class="data-value">{{ dash(partyCustomer?.newbornWeightGram) }}</div>
</div>
<div class="data-item">
<div class="data-label">评分1分钟</div>
<div class="data-value">{{ dash(partyCustomer?.apgar1min) }}</div>
</div>
<div class="data-item">
<div class="data-label">评分5分钟</div>
<div class="data-value">{{ dash(partyCustomer?.apgar5min) }}</div>
</div>
</div>
</div>
</div>
</div>
```
---
### Task 4: 改造采集信息 section
- [ ] **Step 1: 替换采集信息区域的 HTML 结构**
```html
<!-- 采集信息 -->
<div id="view-section-collection" class="section-card section-card--white">
<div class="section-title"><h3>采集信息</h3></div>
<div class="data-grid data-grid--3">
<div class="data-item">
<div class="data-label">采集地区</div>
<div class="data-value">{{ regionDisplay }}</div>
</div>
<div class="data-item">
<div class="data-label">采集医院</div>
<div class="data-value">{{ hospitalDisplay }}</div>
</div>
<div class="data-item">
<div class="data-label">采集医护</div>
<div class="data-value">{{ dash(collectionInfo?.medicalStaff) }}</div>
</div>
<div class="data-item">
<div class="data-label">销售人员</div>
<div class="data-value">{{ salesDisplay }}</div>
</div>
<div class="data-item">
<div class="data-label">样本采集时间</div>
<div class="data-value">{{ dash(collectionInfo?.sampleCollectTime) }}</div>
</div>
<div class="data-item">
<div class="data-label">母血采集时间</div>
<div class="data-value">{{ dash(collectionInfo?.maternalBloodCollectTime) }}</div>
</div>
</div>
</div>
```
---
### Task 5: 改造样本及运输信息 section
- [ ] **Step 1: 包裹样本区域为白色卡片**
保留 el-table 和 el-empty,只改外层容器从 `form-tab-pane` 改为 `section-card section-card--white`:
```html
<!-- 样本及运输信息 -->
<div id="view-section-sample" class="section-card section-card--white">
<div class="section-title"><h3>样本及运输信息</h3></div>
<el-table :data="samples" row-key="id" stripe border size="small" class="sub-view-table">
<!-- 保留所有现有 el-table-column 不变 -->
</el-table>
<el-empty v-if="!samples.length" description="暂无样本" :image-size="48" />
</div>
```
表格列完全保持原样(11列:样本类型/编码/采集量/单位/价格/存储年限/状态/运输类型/目的站点/预计到达/快递携带/备注),只改外层包裹。
---
### Task 6: 改造缴费信息 section — 卡片 + 明细 + 退费流程
**Files:**
- Modify: `web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue` (缴费信息模板)
- [ ] **Step 1: 替换缴费信息区域为灰色卡片 + 指标卡 + 明细卡片**
将现有的 `view-section-payment` 改造。缴费主表改为 4 个 `payment-metric` 指标卡。缴费明细卡片保留现有卡片结构但加入退费样式(红色/琥珀色左边框、退费详情区、mini-flow 横向节点)。**所有现有字段必须保留**。
```html
<!-- 缴费信息 -->
<div id="view-section-payment" class="section-card section-card--gray">
<div class="section-title"><h3>缴费信息</h3></div>
<!-- 缴费指标 -->
<div class="payment-summary">
<div class="payment-metric">
<div class="pm-label">缴费模式</div>
<div class="pm-value">{{ payModeLabel(String(payment?.payMode || '')) }}</div>
</div>
<div class="payment-metric">
<div class="pm-label">合同金额</div>
<div class="pm-value pm-value--amount">{{ dash(payment?.contractAmount) }}</div>
</div>
<template v-if="payment?.payMode === 'dj'">
<div class="payment-metric">
<div class="pm-label">趸交截止时间</div>
<div class="pm-value">{{ formatYmd(payment?.lumpSumDeadline) }}</div>
</div>
</template>
<template v-if="payment?.payMode === 'fq'">
<div class="payment-metric">
<div class="pm-label">分期期数</div>
<div class="pm-value">{{ dash(payment?.installmentCount) }}</div>
</div>
<div class="payment-metric">
<div class="pm-label">每期间隔(月)</div>
<div class="pm-value">{{ dash(payment?.installmentIntervalMonths) }}</div>
</div>
<div class="payment-metric">
<div class="pm-label">分期开始时间</div>
<div class="pm-value">{{ formatYmd(payment?.installmentStartDate) }}</div>
</div>
</template>
</div>
<!-- 缴费明细 -->
<div class="section-subtitle">缴费明细</div>
<div class="payment-details">
<div
v-for="(detail, dIdx) in paymentDetails"
:key="detail.id || dIdx"
class="payment-detail-card"
:class="{
'payment-detail-card--refunded': String(detail.payStatus) === 'ytf',
'payment-detail-card--refund-pending': String(detail.payStatus) === 'tfqrz'
}"
>
<!-- 保留现有 card header/body 结构,包含所有字段 -->
<!-- 保留 payment-flow 横向节点 -->
<!-- 保留所有附件展示 -->
</div>
</div>
<el-empty v-else description="暂无缴费明细" :image-size="48" />
</div>
```
注意:缴费明细内的每一个字段(应缴费金额、转账方式、转账金额、转账时间、缴费状态、应缴费时间、转账凭证附件)和退费流程节点(缴费财务确认、退费申请、财务退费确认)**全部保留**,只改外层容器样式。
---
### Task 7: 改造审核记录 section
- [ ] **Step 1: 包裹审核记录为白色卡片**
保留 el-table,只改外层容器:
```html
<div v-if="showAuditHistory" id="view-section-audit" class="section-card section-card--white">
<div class="section-title"><h3>审核记录</h3></div>
<el-table ...>
<!-- 保留所有现有列不变 -->
</el-table>
</div>
```
---
### Task 8: 替换整个 `<style>` 块为方案D风格 CSS
**Files:**
- Modify: `web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue` (style 部分)
- [ ] **Step 1: 删除现有 `<style scoped lang="scss">` 中的所有旧样式**
- [ ] **Step 2: 写入方案D风格的新 CSS**
以下是需要写入的完整新样式体系(从 demo-protocol-view-d.html 提取,适配 Vue scoped + Element Plus 共存):
**核心样式类:**
```scss
/* ── Dialog Body ── */
.protocol-view-body {
overflow: visible;
}
/* ── Sticky Tab Bar ── */
.detail-tabs {
position: sticky;
top: 0;
z-index: 100;
display: flex;
gap: 4px;
background: #f5f7fa;
padding: 4px;
border-radius: 8px;
margin-bottom: 16px;
width: fit-content;
isolation: isolate;
}
.detail-tab {
padding: 10px 20px;
cursor: pointer;
border-radius: 6px;
font-size: 14px;
color: #64748b;
transition: all 0.3s;
&.active {
background: #fff;
color: #059669;
font-weight: 500;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
&:hover:not(.active) {
color: #059669;
}
}
/* ═══ KPI Summary Strip ═══ */
.kpi-strip {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.kpi-card {
background: #fff;
border-radius: 12px;
padding: 18px 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
border-left: 3px solid #10b981;
transition: transform 0.2s, box-shadow 0.2s;
position: relative;
overflow: hidden;
&::after {
content: '';
position: absolute;
top: 0; right: 0;
width: 60px; height: 60px;
background: radial-gradient(circle at top right, rgba(16,185,129,0.06), transparent 70%);
pointer-events: none;
}
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(16,185,129,0.1);
}
}
.kpi-label {
font-size: 12px;
color: #94a3b8;
margin-bottom: 6px;
letter-spacing: 0.5px;
}
.kpi-value {
font-size: 16px;
font-weight: 600;
color: #1e293b;
}
.status-dot {
display: inline-block;
width: 8px; height: 8px;
border-radius: 50%;
margin-right: 6px;
vertical-align: middle;
&.active {
background: #10b981;
box-shadow: 0 0 0 3px rgba(16,185,129,0.2);
}
&.pending { background: #f59e0b; box-shadow: 0 0 0 3px rgba(245,158,11,0.2); }
&.expired { background: #94a3b8; }
&.void { background: #dc2626; box-shadow: 0 0 0 3px rgba(220,38,38,0.2); }
&.research { background: #7c3aed; box-shadow: 0 0 0 3px rgba(124,58,237,0.2); }
}
/* ═══ Section Cards ═══ */
.section-card {
border-radius: 12px;
padding: 22px 24px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
transition: transform 0.2s, box-shadow 0.2s;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
&--white { background: #fff; }
&--green { background: #f0fdf4; }
&--gray { background: #f8fafc; }
}
/* Section Title */
.section-title {
display: flex;
align-items: center;
margin-bottom: 18px;
padding-bottom: 12px;
border-bottom: 1px solid #e2e8f0;
&::before {
content: '';
width: 3px;
height: 18px;
background: #10b981;
border-radius: 2px;
margin-right: 10px;
flex-shrink: 0;
}
h3 {
font-size: 15px;
font-weight: 600;
color: #1e293b;
}
}
.section-subtitle {
font-size: 13px;
font-weight: 600;
color: #334155;
margin: 16px 0 12px;
padding-left: 10px;
border-left: 3px solid #10b981;
}
/* ── Data Grid ── */
.data-grid {
display: grid;
gap: 14px 20px;
&--3 { grid-template-columns: repeat(3, 1fr); }
&--2 { grid-template-columns: repeat(2, 1fr); }
}
.data-item {
// no special styling needed
}
.data-item--full {
grid-column: 1 / -1;
}
.data-label {
font-size: 12px;
color: #94a3b8;
margin-bottom: 3px;
}
.data-value {
font-size: 14px;
color: #1e293b;
font-weight: 500;
&--muted {
font-weight: 400;
color: #64748b;
font-size: 13px;
line-height: 1.5;
}
}
/* ── Badges ── */
.badge {
display: inline-block;
padding: 2px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
}
.badge--green { background: #ecfdf5; color: #059669; }
.badge--amber { background: #fffbeb; color: #d97706; }
.badge--gray { background: #f1f5f9; color: #64748b; }
.badge--red { background: #fef2f2; color: #dc2626; }
.badge--purple { background: #faf5ff; color: #7c3aed; }
/* ── Attachment Row ── */
.attachment-row {
margin-top: 16px;
padding-top: 14px;
border-top: 1px dashed #e2e8f0;
display: flex;
flex-direction: column;
gap: 8px;
}
/* ── Remark Row ── */
.remark-row {
margin-top: 14px;
padding-top: 14px;
border-top: 1px dashed #e2e8f0;
}
/* ═══ Client Sub Cards ═══ */
.client-sub-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.client-sub-card {
background: #fff;
border-radius: 10px;
padding: 18px 20px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
border: 1px solid rgba(0, 0, 0, 0.03);
transition: transform 0.2s, box-shadow 0.2s;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.07);
}
}
.sub-card-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 14px;
padding-bottom: 10px;
border-bottom: 1px solid #f1f5f9;
.sub-card-icon {
font-size: 22px;
line-height: 1;
}
h4 {
font-size: 14px;
font-weight: 600;
color: #1e293b;
}
}
.sub-card-full-row {
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid #f1f5f9;
}
/* ═══ Table Styles ═══ */
.sub-view-table {
margin-bottom: 8px;
}
/* ═══ Payment Section ═══ */
.payment-summary {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 18px;
}
.payment-metric {
background: #fff;
border-radius: 8px;
padding: 12px 16px;
border: 1px solid #e2e8f0;
}
.pm-label {
font-size: 11px;
color: #94a3b8;
margin-bottom: 4px;
}
.pm-value {
font-size: 16px;
font-weight: 700;
color: #1e293b;
&--amount { color: #059669; }
}
.payment-details {
display: flex;
flex-direction: column;
gap: 16px;
margin-top: 14px;
}
.payment-detail-card {
background: #fff;
border-radius: 8px;
padding: 14px 16px;
border: 1px solid #e2e8f0;
&--refunded {
border-left: 3px solid #dc2626;
}
&--refund-pending {
border-left: 3px solid #d97706;
}
}
/* 保留现有的 payment-flow 样式 */
.payment-flow { /* ...existing... */ }
/* ═══ Responsive ═══ */
@media (max-width: 1200px) {
.data-grid--3,
.data-grid--2 {
grid-template-columns: repeat(2, 1fr);
}
.client-sub-cards {
grid-template-columns: 1fr;
}
.kpi-strip {
grid-template-columns: repeat(2, 1fr);
}
.payment-summary {
grid-template-columns: repeat(2, 1fr);
}
}
```
---
### Task 9: 验证所有字段无遗漏 + 弹窗可正常工作
**Files:**
- Verify: `web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue`
- [ ] **Step 1: 字段清单核查**
逐项对照现有 `view-kv-grid` 中的 dt/dd,确保新模板中每个字段都存在:
基本信息(5+附件+备注): ✅ 协议号/状态/登记日期/采集专员/创建时间/附件/备注
客户-母亲(7): ✅ 关联客户/姓名/电话/身份证/出生年月/民族/住址
客户-父亲(3): ✅ 姓名/电话/身份证
客户-新生儿(9): ✅ 姓名/身份证/分娩时间/性别/分娩方式/胎次/体重/评分1/评分5
采集信息(6): ✅ 采集地区/医院/医护/销售/样本采集时间/母血采集时间
样本表格(11列): ✅ 类型/编码/采集量/单位/价格/存储年限/状态/运输类型/目的站点/预计到达/快递携带/备注
缴费主表(4-6): ✅ 缴费模式/合同金额/趸交截止|分期期数+间隔+开始时间
缴费明细(每条6-8): ✅ 应缴金额/转账方式/转账金额/转账时间/缴费状态/应缴费时间/转账凭证附件
缴费流程节点: ✅ 缴费财务确认/退费申请/财务退费确认(含金额/备注/附件)
审核记录(5列): ✅ 序号/审核时间/审核状态/审核备注/审核人
- [ ] **Step 2: 运行 dev 服务器验证**
```bash
cd web-admin && pnpm dev
```
打开 `/#/demo/protocol` 页面,点击「查看」按钮,验证:
1. KPI 摘要条正确显示
2. 卡片分区布局正确
3. 🤱👨👶 子卡片渲染正常
4. 所有字段数据显示正常
5. 附件可预览/下载
6. 退费状态卡片有红色/琥珀色边框
7. 审核记录 tab 正确显示(采集协议单审核页)
8. sticky tab 导航可滚动定位
- [ ] **Step 3: 在缴费信息页验证**
打开 `/#/cell-storage/payment-info` 页面,点击「查看」,确认弹窗正确渲染。
- [ ] **Step 4: 在审核页验证**
打开 `/#/cell-storage/protocol-audit` 页面,点击「查看」,确认审核记录 tab 显示。
- [ ] **Step 5: 修复 LSP 诊断**
```bash
# 检查是否有 TypeScript 错误
pnpm build:typecheck
```