fix(web-admin): 样本接收展示协议知情同意书等附件

质检样本接收列表与弹窗需查看采集协议侧附件。后端待接收/已接收列表补齐协议附件字段;前端列表增加知情同意书缩略图,接收弹窗右侧改为上运输、下协议附件分组展示。

关联 #43

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-11-05 21:53:56 +08:00
1 parent 9cf1110495
commit c40feab76a
6 files changed
+362 -34

No files matched your search

@@ -0,0 +1,62 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import {
getConsentProtocolAttachment,
getSourceProtocolAttachment,
getSourceProtocolAttachmentGroups
} from './protocol-source-attachment'
describe('protocol-source-attachment', () => {
it('returns health survey as primary for cell storage', () => {
const meta = getSourceProtocolAttachment({
sourceType: 'cell_storage',
healthSurveyAttachmentFiles: [{ id: '1', fileUrl: '/a.png' }]
})
assert.equal(meta.label, '孕妇健康调查表')
assert.equal(meta.files.length, 1)
})
it('returns infectious disease report as primary for research donation', () => {
const meta = getSourceProtocolAttachment({
sourceType: 'research_donation',
infectiousDiseaseReportAttachmentFiles: [{ id: '2', fileUrl: '/b.png' }]
})
assert.equal(meta.label, '传染病检测报告')
assert.equal(meta.files.length, 1)
})
it('returns consent attachment from shared field', () => {
const meta = getConsentProtocolAttachment({
consentAttachmentFiles: [{ id: '3', fileUrl: '/c.png' }]
})
assert.equal(meta.label, '知情同意书')
assert.equal(meta.files.length, 1)
})
it('groups cell storage attachments for reception dialog', () => {
const groups = getSourceProtocolAttachmentGroups({
sourceType: 'cell_storage',
protocolAttachmentFiles: [{ id: 'p' }],
consentAttachmentFiles: [{ id: 'c' }],
healthSurveyAttachmentFiles: [{ id: 'h' }]
})
assert.deepEqual(
groups.map((g) => g.label),
['协议附件', '知情同意书', '孕妇健康调查表']
)
assert.ok(groups.every((g) => g.files.length === 1))
})
it('groups research donation attachments for reception dialog', () => {
const groups = getSourceProtocolAttachmentGroups({
sourceType: 'research_donation',
consentAttachmentFiles: [{ id: 'c' }],
infectiousDiseaseReportAttachmentFiles: [{ id: 'i' }],
attachmentFiles: [{ id: 'o' }]
})
assert.deepEqual(
groups.map((g) => g.label),
['知情同意书', '传染病检测报告', '其他附件']
)
})
})
@@ -8,7 +8,7 @@ function normalizeAttachmentFiles(raw: unknown): Array<Record<string, unknown>>
return raw.filter((item) => item && typeof item === 'object') as Array<Record<string, unknown>>
}
/** 按来源返回协议级附件标签与文件列表(细胞存储=孕妇健康调查表,科研捐赠=传染病检测报告) */
/** 按来源返回协议级主附件标签与文件列表(细胞存储=孕妇健康调查表,科研捐赠=传染病检测报告) */
export function getSourceProtocolAttachment(
row: Record<string, unknown>
): ProtocolSourceAttachmentMeta {
@@ -24,3 +24,54 @@ export function getSourceProtocolAttachment(
files: normalizeAttachmentFiles(row.healthSurveyAttachmentFiles)
}
}
/** 知情同意书(细胞存储与科研捐赠共用字段) */
export function getConsentProtocolAttachment(
row: Record<string, unknown>
): ProtocolSourceAttachmentMeta {
return {
label: '知情同意书',
files: normalizeAttachmentFiles(row.consentAttachmentFiles)
}
}
/**
* 样本接收弹窗:按来源返回质检需查看的协议附件分组。
* 细胞存储:协议附件 / 知情同意书 / 孕妇健康调查表
* 科研捐赠:知情同意书 / 传染病检测报告 / 其他附件
*/
export function getSourceProtocolAttachmentGroups(
row: Record<string, unknown>
): ProtocolSourceAttachmentMeta[] {
const source = String(row.sourceType || 'cell_storage').trim()
if (source === 'research_donation') {
return [
{
label: '知情同意书',
files: normalizeAttachmentFiles(row.consentAttachmentFiles)
},
{
label: '传染病检测报告',
files: normalizeAttachmentFiles(row.infectiousDiseaseReportAttachmentFiles)
},
{
label: '其他附件',
files: normalizeAttachmentFiles(row.attachmentFiles)
}
]
}
return [
{
label: '协议附件',
files: normalizeAttachmentFiles(row.protocolAttachmentFiles)
},
{
label: '知情同意书',
files: normalizeAttachmentFiles(row.consentAttachmentFiles)
},
{
label: '孕妇健康调查表',
files: normalizeAttachmentFiles(row.healthSurveyAttachmentFiles)
}
]
}
@@ -80,6 +80,16 @@
/>
</dd>
</div>
<div class="sample-reception-card__meta-item sample-reception-card__meta-item--attachment">
<dt>{{ consentAttachmentMeta(row).label }}</dt>
<dd>
<ProtocolAttachmentMiniReadonly
:files="consentAttachmentMeta(row).files"
@preview="handleAttachmentPreview"
@download="handleAttachmentDownload"
/>
</dd>
</div>
<div v-if="activeTab === 'received'" class="sample-reception-card__meta-item">
<dt>实验室入库编码</dt>
<dd>
@@ -242,7 +252,10 @@
import SampleCodeWithSpecialDisplay from '@/components/business/sample-code-with-special-display/index.vue'
import QcWorkOrderProtocolPaymentPanel from '@/views/quality-inspection/qc-work-order/QcWorkOrderProtocolPaymentPanel.vue'
import ProtocolAttachmentMiniReadonly from '@/components/business/protocol-attachment-mini-readonly/index.vue'
import { getSourceProtocolAttachment } from '@/utils/protocol-source-attachment'
import {
getConsentProtocolAttachment,
getSourceProtocolAttachment
} from '@/utils/protocol-source-attachment'
import { useAttachmentActions } from '@/composables/useAttachmentActions'
defineOptions({ name: 'SampleReceptionCardList' })
@@ -284,6 +297,10 @@
return getSourceProtocolAttachment(row)
}
function consentAttachmentMeta(row: Record<string, unknown>) {
return getConsentProtocolAttachment(row)
}
function protocolStatusLabelBySource(status: string, source: string, voidType?: string): string {
return source === 'research_donation'
? donationStatusDisplayLabel(status, voidType)
@@ -204,41 +204,62 @@
</div>
</div>
</div>
<div id="form-section-transport" class="sample-transport-col">
<div class="section-title section-title--compact">
<div class="section-title-left"><h3>运输信息</h3></div>
<div id="form-section-transport" class="sample-transport-col sample-transport-col--stack">
<div class="sample-transport-stack-block">
<div class="section-title section-title--compact">
<div class="section-title-left"><h3>运输信息</h3></div>
</div>
<div class="data-grid data-grid--2">
<div class="data-item">
<div class="data-label">运输方式</div>
<div class="data-value">
{{ transportTypeLabel(String(currentRow.transportType || '')) }}
</div>
</div>
<div class="data-item">
<div class="data-label">预计到达</div>
<div class="data-value">{{ formatDisplayOrDash(currentRow.etaTime) }}</div>
</div>
<template v-if="currentRow.transportType === 'kdys'">
<div class="data-item">
<div class="data-label">快递公司</div>
<div class="data-value">{{ currentRow.expressCompanyName || '-' }}</div>
</div>
<div class="data-item">
<div class="data-label">快递单号</div>
<div class="data-value">{{ currentRow.waybillNo || '-' }}</div>
</div>
</template>
<template v-else-if="currentRow.transportType === 'ryxd'">
<div class="data-item">
<div class="data-label">携带人</div>
<div class="data-value">{{ currentRow.carrierName || '-' }}</div>
</div>
<div class="data-item">
<div class="data-label">携带人电话</div>
<div class="data-value">{{ currentRow.carrierPhone || '-' }}</div>
</div>
</template>
</div>
</div>
<div class="data-grid data-grid--2">
<div class="data-item">
<div class="data-label">运输方式</div>
<div class="data-value">
{{ transportTypeLabel(String(currentRow.transportType || '')) }}
<div class="sample-transport-stack-block sample-protocol-attachments">
<div class="section-title section-title--compact">
<div class="section-title-left"><h3>协议附件</h3></div>
</div>
<div class="sample-protocol-attachments__list">
<div
v-for="group in receiveProtocolAttachmentGroups"
:key="group.label"
class="sample-protocol-attachments__item"
>
<div class="data-label">{{ group.label }}</div>
<ProtocolAttachmentReadonly
:files="group.files"
@preview="handleAttachmentPreview"
@download="handleAttachmentDownload"
/>
</div>
</div>
<div class="data-item">
<div class="data-label">预计到达</div>
<div class="data-value">{{ formatDisplayOrDash(currentRow.etaTime) }}</div>
</div>
<template v-if="currentRow.transportType === 'kdys'">
<div class="data-item">
<div class="data-label">快递公司</div>
<div class="data-value">{{ currentRow.expressCompanyName || '-' }}</div>
</div>
<div class="data-item">
<div class="data-label">快递单号</div>
<div class="data-value">{{ currentRow.waybillNo || '-' }}</div>
</div>
</template>
<template v-else-if="currentRow.transportType === 'ryxd'">
<div class="data-item">
<div class="data-label">携带人</div>
<div class="data-value">{{ currentRow.carrierName || '-' }}</div>
</div>
<div class="data-item">
<div class="data-label">携带人电话</div>
<div class="data-value">{{ currentRow.carrierPhone || '-' }}</div>
</div>
</template>
</div>
</div>
</div>
@@ -745,6 +766,7 @@
buildPrefilledLabStorageCode,
parseLabStorageReferenceDate
} from '@/utils/sample-reception-lab-storage-code'
import { getSourceProtocolAttachmentGroups } from '@/utils/protocol-source-attachment'
import type { ProductionReceptionRecord } from '@/utils/sample-production-reception-fields'
import {
formatProductionFieldValue,
@@ -955,6 +977,12 @@
.filter(Boolean) as Array<Record<string, unknown>>
})
const receiveProtocolAttachmentGroups = computed(() => {
const row = currentRow.value
if (!row) return []
return getSourceProtocolAttachmentGroups(row)
})
function formatMetric(v: unknown) {
if (v === null || v === undefined || v === '') return '-'
const n = Number(v)
@@ -1653,6 +1681,35 @@
padding-left: 20px;
border-left: 1px solid #e2e8f0;
}
&--stack {
display: flex;
flex-direction: column;
gap: 14px;
}
}
.sample-transport-stack-block {
min-width: 0;
}
.sample-protocol-attachments {
background: #ecfdf5;
border-radius: 8px;
padding: 12px 14px;
border: 1px solid #a7f3d0;
}
.sample-protocol-attachments__list {
display: flex;
flex-direction: column;
gap: 12px;
}
.sample-protocol-attachments__item {
.data-label {
margin-bottom: 6px;
}
}
.section-title {