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 改造,不引入新依赖。
关键约束:
showAuditHistory prop 的条件展示逻辑| 文件 | 操作 | 说明 |
|---|---|---|
web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue |
修改 | template + style 全面改造,script 不动 |
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 保留 |
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>
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] || ''
}
<template v-if="bundle"> 内的 6 个 section 改为卡片包裹每个 form-tab-pane 改为 section-card 结构。保留所有 id="view-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>
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'
}
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>
<!-- 采集信息 -->
<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>
保留 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列:样本类型/编码/采集量/单位/价格/存储年限/状态/运输类型/目的站点/预计到达/快递携带/备注),只改外层包裹。
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>
注意:缴费明细内的每一个字段(应缴费金额、转账方式、转账金额、转账时间、缴费状态、应缴费时间、转账凭证附件)和退费流程节点(缴费财务确认、退费申请、财务退费确认)全部保留,只改外层容器样式。
保留 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>
<style> 块为方案D风格 CSSFiles:
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);
}
}
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列): ✅ 序号/审核时间/审核状态/审核备注/审核人
cd web-admin && pnpm dev
打开 /#/demo/protocol 页面,点击「查看」按钮,验证:
打开 /#/cell-storage/payment-info 页面,点击「查看」,确认弹窗正确渲染。
打开 /#/cell-storage/protocol-audit 页面,点击「查看」,确认审核记录 tab 显示。
# 检查是否有 TypeScript 错误
pnpm build:typecheck