2026-05-17-protocol-view-dialog-redesign.md 26 KB

采集协议单查看弹窗 方案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。

<!-- 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 函数附近添加:

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卡片结构:

<!-- 基本信息 -->
<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 函数
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 列子卡片:

<!-- 客户信息 -->
<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 结构
<!-- 采集信息 -->
<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:

<!-- 样本及运输信息 -->
<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 横向节点)。所有现有字段必须保留。

<!-- 缴费信息 -->
<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,只改外层容器:

<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 共存):

核心样式类:

/* ── 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 服务器验证
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 诊断
# 检查是否有 TypeScript 错误
pnpm build:typecheck