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: 将出库放行审核列表卡片改成采集协议单式 protocol-card* 布局(aside + 三块 sections + foot),样式从协议页复制到出库卡片组件各自维护。
Architecture: 仅改 OutboundReleaseReviewCardList.vue:替换模板 DOM 类名与结构,删除旧 outbound-release-review-card* 样式,粘贴 ProtocolAuditTab.vue 中 protocol-card* SCSS(去掉缴费等无关块)。Props/emit 保持不变。
Tech Stack: Vue 3 + Element Plus + SCSS
Spec: docs/superpowers/specs/2026-07-29-outbound-release-card-align-protocol-design.md
Demo: docs/superpowers/demos/2026-07-29-outbound-release-card-vs-protocol-card-demo.html
Style source: web-admin/src/views/cell-storage/protocol-audit/ProtocolAuditTab.vue(约 L727–L1099)
| 文件 | 职责 |
|---|---|
web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseReviewCardList.vue |
模板 + 样式重写 |
docs/superpowers/specs/2026-07-29-outbound-release-card-align-protocol-design.md |
标「已实现」 |
不改:index.vue、API、后端。
Files:
Modify: web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseReviewCardList.vue
[ ] Step 1: 替换 <template>
用下列模板整体替换现有 template(保留外层 list-body + 分页;列表容器类名改为 protocol-card-list):
<template>
<div class="list-body list-wrap outbound-release-review-cards-body">
<div v-loading="loading" class="protocol-card-section">
<div v-if="rows.length" class="protocol-card-list">
<article
v-for="(row, idx) in rows"
:key="String(row.scheduleId || row.id || idx)"
class="protocol-card"
:class="{ 'is-selected': isSelected(row) }"
@dblclick="emit('card-dblclick', row)"
>
<div class="protocol-card__select" @click.stop>
<el-checkbox
:model-value="isSelected(row)"
@change="(val) => emit('toggle-select', row, Boolean(val))"
/>
</div>
<div class="protocol-card__aside">
<div class="protocol-card__name-row">
<span class="protocol-card__index">{{ cardIndex(idx) }}</span>
<span class="protocol-card__name">{{ asideTitle(row) }}</span>
</div>
<div class="protocol-card__contact">
<div class="protocol-card__sub">{{ row.applicationNo || '—' }}</div>
<div v-if="!showPatient" class="protocol-card__sub">
{{ row.projectTitle || '—' }}
</div>
</div>
<el-tag
v-if="row.reviewConclusion"
:type="storageReleaseConclusionTagType(row.reviewConclusion)"
size="small"
class="protocol-card__status"
>
{{ storageReleaseConclusionLabel(row.reviewConclusion) }}
</el-tag>
<el-tag v-else type="info" size="small" class="protocol-card__status">待审核</el-tag>
<el-tag
v-if="row.bizType"
:type="bizTypeTagType(row.bizType) as any"
size="small"
class="protocol-card__status"
>
{{ row.bizTypeLabel || bizTypeLabel(row.bizType) }}
</el-tag>
<div class="protocol-card__create-time">
{{ scheduleStatusLabel(row.scheduleStatus || '') || '—' }}
·
{{ qcStatusLabel(row.qcStatus || '') || '—' }}
</div>
</div>
<div class="protocol-card__sections">
<section class="protocol-card__block protocol-card__block--basic">
<div class="protocol-card__block-title">基础信息</div>
<div class="protocol-card__kv-grid">
<div class="protocol-card__kv">
<label>申请单号</label><span>{{ row.applicationNo || '—' }}</span>
</div>
<template v-if="showPatient">
<div class="protocol-card__kv">
<label>患者姓名</label><span>{{ row.patientName || '—' }}</span>
</div>
</template>
<template v-else>
<div class="protocol-card__kv">
<label>课题名称</label
><span :title="row.projectTitle || ''">{{ row.projectTitle || '—' }}</span>
</div>
<div class="protocol-card__kv">
<label>客户名称</label><span>{{ row.customerName || '—' }}</span>
</div>
</template>
<div class="protocol-card__kv">
<label>{{ pickupTimeLabel }}</label>
<span>{{ formatDisplayOrDash(row.infusionTime) }}</span>
</div>
<div class="protocol-card__kv">
<label>审核人</label><span>{{ row.reviewByName || '—' }}</span>
</div>
<div class="protocol-card__kv">
<label>审核时间</label><span>{{ formatDisplayOrDash(row.reviewTime) }}</span>
</div>
</div>
</section>
<section class="protocol-card__block protocol-card__block--cells">
<div class="protocol-card__block-title">
需求细胞
<span v-if="cellProducts(row).length" class="protocol-card__block-count">
{{ cellProducts(row).length }} 项
</span>
</div>
<div v-if="cellProducts(row).length" class="protocol-card__sample-list">
<div
v-for="(product, pi) in cellProducts(row)"
:key="`${product.projectCode || product.projectName || pi}`"
class="protocol-card__sample-item"
>
<span class="protocol-card__sample-seq">{{ pi + 1 }}</span>
<div class="protocol-card__sample-main">
<div class="protocol-card__sample-line">
<span class="protocol-card__sample-label">项目</span>
<span class="protocol-card__sample-value">{{
product.projectName || '—'
}}</span>
<el-tag
v-if="product.productStatus"
:type="productStatusTagType(product.productStatus) as any"
size="small"
class="protocol-card__sample-status"
>
{{ productStatusLabel(product.productStatus, bizType) }}
</el-tag>
</div>
<div class="protocol-card__sample-line">
<span class="protocol-card__sample-label">编码</span>
<span
class="protocol-card__sample-value protocol-card__sample-value--code"
>{{ product.projectCode || '—' }}</span>
</div>
<div class="protocol-card__sample-line">
<span class="protocol-card__sample-label">类型</span>
<span class="protocol-card__sample-value">
{{ product.cellTypeName || row.cellType || '—' }}
<template v-if="product.specText || row.specText">
· {{ product.specText || row.specText }}
</template>
<template v-if="product.quantity != null">
· ×{{ product.quantity }}
</template>
</span>
</div>
</div>
</div>
</div>
<div v-else class="protocol-card__empty">
<template v-if="row.cellType || row.specText">
<span v-if="row.cellType">细胞类型:{{ row.cellType }}</span>
<span v-if="row.cellType && row.specText"> · </span>
<span v-if="row.specText">规格:{{ row.specText }}</span>
</template>
<template v-else>暂无细胞需求明细</template>
</div>
</section>
<section class="protocol-card__block protocol-card__block--status">
<div class="protocol-card__block-title">状态摘要</div>
<div class="protocol-card__kv-grid">
<div class="protocol-card__kv">
<label>计划状态</label>
<span>
<el-tag
v-if="row.scheduleStatus"
:type="scheduleStatusTagType(row.scheduleStatus)"
size="small"
>
{{ scheduleStatusLabel(row.scheduleStatus) }}
</el-tag>
<template v-else>—</template>
</span>
</div>
<div class="protocol-card__kv">
<label>质检状态</label>
<span>
<el-tag
v-if="row.qcStatus"
:type="qcStatusTagType(row.qcStatus)"
size="small"
>
{{ qcStatusLabel(row.qcStatus) }}
</el-tag>
<template v-else>—</template>
</span>
</div>
<div class="protocol-card__kv">
<label>审核结论</label>
<span>
<el-tag
v-if="row.reviewConclusion"
:type="storageReleaseConclusionTagType(row.reviewConclusion)"
size="small"
>
{{ storageReleaseConclusionLabel(row.reviewConclusion) }}
</el-tag>
<el-tag v-else type="info" size="small">待审核</el-tag>
</span>
</div>
</div>
</section>
</div>
<div class="protocol-card__foot">
<div class="protocol-card__actions">
<el-button class="btn-op-item" @click.stop="emit('review', row)">
<el-icon><CircleCheck /></el-icon>审核
</el-button>
<el-button class="btn-op-item" @click.stop="emit('view', row)">
<el-icon><View /></el-icon>查看
</el-button>
<el-button class="btn-op-item" @click.stop="emit('material', row)">
<el-icon><Box /></el-icon>出库物料
</el-button>
</div>
</div>
</article>
</div>
<el-empty
v-else-if="!loading"
class="protocol-card-empty"
description="暂无出库放行审核记录"
:image-size="80"
/>
</div>
<div class="pagination list-pager-bar">
<el-pagination
:current-page="page"
:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="(v) => emit('page-size-change', v)"
@current-change="(v) => emit('page-change', v)"
/>
</div>
</div>
</template>
asideTitle在现有 script 中保留 props/emit/cellProducts/isSelected/cardIndex 等;新增:
function asideTitle(row: OutboundReleaseReview) {
if (showPatient.value) return row.patientName || '—'
return row.customerName || '—'
}
scheduleStatusLabel / qcStatusLabel 在空字符串时可能返回空或原码——aside 底部用 || '—' 已兜底。
<style>.outbound-release-review-card* 规则。ProtocolAuditTab.vue 复制以下选择器整块到本组件 scoped SCSS(含 media query):.protocol-card-section、.protocol-card-list、.protocol-card(含 hover/is-selected)、__select、__aside、__status、__create-time、__name-row、__index、__name、__contact、__sub、__sections、__block(含 --basic/--newborn/--samples 可改名或保留)、__block-title、__block-count、__kv-grid、__kv、__empty、__sample-list、__sample-item、__sample-seq、__sample-main、__sample-line、__sample-label、__sample-value、__sample-status、__foot、__actions、__protocol-card-empty、以及 @media (max-width: 1200px) / 900px 中与 card 相关的规则。__block--payment 与缴费 :deep 规则。 .outbound-release-review-cards-body {
border-top: 1px solid var(--mint-border, #e2e8f0);
}
.protocol-card__block--cells {
flex: 1.35;
min-width: 220px;
}
.protocol-card__block--status {
flex: 0.9;
min-width: 140px;
}
.protocol-card__block--basic {
flex: 1.1;
min-width: 180px;
}
若复制了 --samples,可将 --cells 与 --samples 设相同 flex,或模板只用 --cells + 上列修饰。
打开:
http://localhost:3006/#/market/pilot-service/outbound-release-reviewhttp://localhost:3006/#/market/basic-experiment/outbound-release-review(路径以路由为准)对照 Demo「提议」态:单列、aside 主标题、三块、右侧操作。勾选/审核/查看/出库物料仍可用。
git add web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseReviewCardList.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): align outbound release cards with protocol-card layout
Reuse protocol-card class names and structure; map outbound fields into aside and three sections.
EOF
)"
(勿提交 graphify 噪音。)
Files:
Modify: docs/superpowers/specs/2026-07-29-outbound-release-card-align-protocol-design.md
[ ] Step 1: > 状态:已确认 → > 状态:已实现
[ ] Step 2: Commit
git add docs/superpowers/specs/2026-07-29-outbound-release-card-align-protocol-design.md
git commit -m "$(cat <<'EOF'
docs: mark outbound release card align-protocol design as implemented
EOF
)"
| Spec | Task |
|---|---|
| protocol-card 结构 | Task 1 template |
| 复制 SCSS、删旧样式 | Task 1 style |
| aside 主标题映射 | Task 1 asideTitle |
| 三块 sections | Task 1 |
| 操作顺序审核→查看→出库物料 | Task 1 |
| 不改 API/index | 无其它文件 |
| 规格状态 | Task 2 |
无 TBD。实现时以 ProtocolAuditTab.vue 源码粘贴为准,勿手抄残缺 media query。