2026-07-29-outbound-release-card-align-protocol.md 15 KB

出库放行审核卡片对齐协议卡 Implementation Plan

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)


File Map

文件 职责
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、后端。


Task 1: 重写 OutboundReleaseReviewCardList 模板与样式

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>
  • Step 2: 脚本增加 asideTitle

在现有 script 中保留 props/emit/cellProducts/isSelected/cardIndex 等;新增:

  function asideTitle(row: OutboundReleaseReview) {
    if (showPatient.value) return row.patientName || '—'
    return row.customerName || '—'
  }

scheduleStatusLabel / qcStatusLabel 在空字符串时可能返回空或原码——aside 底部用 || '—' 已兜底。

  • Step 3: 替换 <style>
  1. 删除全部 .outbound-release-review-card* 规则。
  2. 从 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 相关的规则。
  3. 不要复制 __block--payment 与缴费 :deep 规则。
  4. 增加出库修饰:
  .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 + 上列修饰。

  • Step 4: 本地目视验收

打开:

  • http://localhost:3006/#/market/pilot-service/outbound-release-review
  • http://localhost:3006/#/market/basic-experiment/outbound-release-review(路径以路由为准)

对照 Demo「提议」态:单列、aside 主标题、三块、右侧操作。勾选/审核/查看/出库物料仍可用。

  • Step 5: Commit
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 噪音。)


Task 2: 规格标已实现

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 coverage

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。