862 lines
26 KiB
Markdown
862 lines
26 KiB
Markdown
# 采集协议单查看弹窗 方案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
|
||
```
|