提交
This commit is contained in:
1 parent
427bcb1551
commit
31b04575e2
247 files changed
+39284
-20367
No files matched your search
@@ -0,0 +1,444 @@
|
||||
<!-- 样本接收 · 卡片列表(待接收 / 已接收) -->
|
||||
<template>
|
||||
<div class="list-body list-wrap sample-reception-cards-body">
|
||||
<div v-loading="loading" class="sample-reception-cards-wrap">
|
||||
<article
|
||||
v-for="(row, idx) in rows"
|
||||
:key="String(row.protocolSampleId || row.id || idx)"
|
||||
class="sample-reception-card"
|
||||
:class="{ 'is-eta-overdue': isPendingOverdue(row) }"
|
||||
@dblclick="emit('card-dblclick', row)"
|
||||
>
|
||||
<header class="sample-reception-card__head">
|
||||
<div class="sample-reception-card__head-main">
|
||||
<div class="sample-reception-card__title-row">
|
||||
<el-checkbox
|
||||
v-if="activeTab === 'pending'"
|
||||
:model-value="isSelected(String(row.protocolSampleId || ''))"
|
||||
class="sample-reception-card__checkbox"
|
||||
@change="(val) => emit('toggle-select', row, Boolean(val))"
|
||||
@click.stop
|
||||
/>
|
||||
<span class="sample-reception-card__index">{{ cardIndex(idx) }}</span>
|
||||
<span class="sample-reception-card__code">{{ row.sampleCode || '—' }}</span>
|
||||
<el-tag
|
||||
:type="
|
||||
protocolStatusTagTypeBySource(
|
||||
String(row.protocolStatus || ''),
|
||||
String(row.sourceType || 'cell_storage')
|
||||
)
|
||||
"
|
||||
size="small"
|
||||
>
|
||||
{{
|
||||
protocolStatusLabelBySource(
|
||||
String(row.protocolStatus || ''),
|
||||
String(row.sourceType || 'cell_storage')
|
||||
)
|
||||
}}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
:type="row.sourceType === 'research_donation' ? 'warning' : 'success'"
|
||||
size="small"
|
||||
>
|
||||
{{ row.sourceTypeLabel || '细胞存储' }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="activeTab === 'received'"
|
||||
:type="row.acceptConclusion === 'js' ? 'success' : 'danger'"
|
||||
size="small"
|
||||
>
|
||||
{{ acceptConclusionLabel(String(row.acceptConclusion || '')) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<dl class="sample-reception-card__meta">
|
||||
<div class="sample-reception-card__meta-item">
|
||||
<dt>样本类型</dt>
|
||||
<dd>{{ row.sampleTypeName || '—' }}</dd>
|
||||
</div>
|
||||
<div
|
||||
v-if="activeTab === 'received'"
|
||||
class="sample-reception-card__meta-item"
|
||||
>
|
||||
<dt>实验室入库编码</dt>
|
||||
<dd>
|
||||
{{
|
||||
row.acceptConclusion === 'js' ? row.labStorageCode || '—' : '—'
|
||||
}}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="sample-reception-card__meta-item">
|
||||
<dt>母亲姓名</dt>
|
||||
<dd>{{ row.motherName || '—' }}</dd>
|
||||
</div>
|
||||
<div class="sample-reception-card__meta-item">
|
||||
<dt>新生儿姓名</dt>
|
||||
<dd>{{ row.newbornName || '—' }}</dd>
|
||||
</div>
|
||||
<div
|
||||
class="sample-reception-card__meta-item"
|
||||
:class="{ 'sample-reception-card__meta-item--key': activeTab === 'pending' }"
|
||||
>
|
||||
<dt>样本采集时间</dt>
|
||||
<dd>{{ row.sampleCollectTime || '—' }}</dd>
|
||||
</div>
|
||||
<div
|
||||
class="sample-reception-card__meta-item"
|
||||
:class="{ 'sample-reception-card__meta-item--key': activeTab === 'pending' }"
|
||||
>
|
||||
<dt>预计到达时间</dt>
|
||||
<dd>{{ row.etaTime || '—' }}</dd>
|
||||
</div>
|
||||
<div class="sample-reception-card__meta-item">
|
||||
<dt>运输方式</dt>
|
||||
<dd>{{ transportTypeLabel(String(row.transportType || '')) }}</dd>
|
||||
</div>
|
||||
<template v-if="row.transportType === 'kdys'">
|
||||
<div class="sample-reception-card__meta-item">
|
||||
<dt>快递公司</dt>
|
||||
<dd>{{ row.expressCompanyName || '—' }}</dd>
|
||||
</div>
|
||||
<div class="sample-reception-card__meta-item">
|
||||
<dt>快递单号</dt>
|
||||
<dd>{{ row.waybillNo || '—' }}</dd>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="row.transportType === 'ryxd'">
|
||||
<div class="sample-reception-card__meta-item">
|
||||
<dt>携带人</dt>
|
||||
<dd>{{ row.carrierName || '—' }}</dd>
|
||||
</div>
|
||||
<div class="sample-reception-card__meta-item">
|
||||
<dt>携带人联系电话</dt>
|
||||
<dd>{{ row.carrierPhone || '—' }}</dd>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="sample-reception-card__meta-item">
|
||||
<dt>快递单号</dt>
|
||||
<dd>{{ row.waybillNo || '—' }}</dd>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="activeTab === 'received'">
|
||||
<div class="sample-reception-card__meta-item">
|
||||
<dt>接收时间</dt>
|
||||
<dd>{{ row.receptionTime || '—' }}</dd>
|
||||
</div>
|
||||
<div class="sample-reception-card__meta-item">
|
||||
<dt>经手人</dt>
|
||||
<dd>{{ row.handlerName || '—' }}</dd>
|
||||
</div>
|
||||
</template>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="sample-reception-card__ops">
|
||||
<QcWorkOrderProtocolPaymentPanel
|
||||
v-if="showProtocolPayment(row)"
|
||||
:summary="protocolPaymentSummary(row)"
|
||||
compact
|
||||
class="sample-reception-card__ops-payment"
|
||||
/>
|
||||
<el-button
|
||||
v-if="activeTab === 'pending'"
|
||||
class="btn-op-item primary"
|
||||
type="primary"
|
||||
@click.stop="emit('receive', row)"
|
||||
>
|
||||
<el-icon><CircleCheck /></el-icon>样本接收
|
||||
</el-button>
|
||||
<template v-else>
|
||||
<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('edit', row)">
|
||||
<el-icon><EditPen /></el-icon>编辑
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</header>
|
||||
</article>
|
||||
<el-empty
|
||||
v-if="!loading && !rows.length"
|
||||
class="sample-reception-cards-empty"
|
||||
:description="activeTab === 'pending' ? '暂无待接收样本' : '暂无已接收记录'"
|
||||
:image-size="72"
|
||||
/>
|
||||
</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>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CircleCheck, View, EditPen } from '@element-plus/icons-vue'
|
||||
import {
|
||||
transportTypeLabel,
|
||||
acceptConclusionLabel,
|
||||
protocolStatusLabel,
|
||||
protocolStatusTagType,
|
||||
donationStatusLabel,
|
||||
donationStatusTagType
|
||||
} from '@/utils/status-tag-registry'
|
||||
import type { TagType } from '@/utils/status-tag-registry'
|
||||
import type { QcProtocolPaymentSummary } from '@/api/qc-work-order'
|
||||
import QcWorkOrderProtocolPaymentPanel from '@/views/quality-inspection/qc-work-order/QcWorkOrderProtocolPaymentPanel.vue'
|
||||
|
||||
defineOptions({ name: 'SampleReceptionCardList' })
|
||||
|
||||
type ReceptionTab = 'pending' | 'received'
|
||||
|
||||
const props = defineProps<{
|
||||
rows: Record<string, unknown>[]
|
||||
loading: boolean
|
||||
activeTab: ReceptionTab
|
||||
page: number
|
||||
pageSize: number
|
||||
total: number
|
||||
selectedIds: string[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
receive: [row: Record<string, unknown>]
|
||||
view: [row: Record<string, unknown>]
|
||||
edit: [row: Record<string, unknown>]
|
||||
'toggle-select': [row: Record<string, unknown>, checked: boolean]
|
||||
'card-dblclick': [row: Record<string, unknown>]
|
||||
'page-change': [page: number]
|
||||
'page-size-change': [size: number]
|
||||
}>()
|
||||
|
||||
function protocolStatusLabelBySource(status: string, source: string): string {
|
||||
return source === 'research_donation' ? donationStatusLabel(status) : protocolStatusLabel(status)
|
||||
}
|
||||
|
||||
function protocolStatusTagTypeBySource(status: string, source: string): TagType {
|
||||
return source === 'research_donation' ? donationStatusTagType(status) : protocolStatusTagType(status)
|
||||
}
|
||||
|
||||
function parseEtaLocalDate(eta: unknown): Date | null {
|
||||
const s = String(eta || '').trim()
|
||||
if (!s) return null
|
||||
const normalized = s.replace('T', ' ').slice(0, 19)
|
||||
const m = normalized.match(/^(\d{4})-(\d{2})-(\d{2})/)
|
||||
if (!m) return null
|
||||
const y = Number(m[1])
|
||||
const mo = Number(m[2]) - 1
|
||||
const d = Number(m[3])
|
||||
const dt = new Date(y, mo, d)
|
||||
if (dt.getFullYear() !== y || dt.getMonth() !== mo || dt.getDate() !== d) return null
|
||||
return dt
|
||||
}
|
||||
|
||||
function startOfLocalDay(d: Date) {
|
||||
return new Date(d.getFullYear(), d.getMonth(), d.getDate())
|
||||
}
|
||||
|
||||
function isEtaTodayOrEarlier(etaTime: unknown) {
|
||||
const etaDay = parseEtaLocalDate(etaTime)
|
||||
if (!etaDay) return false
|
||||
return etaDay.getTime() <= startOfLocalDay(new Date()).getTime()
|
||||
}
|
||||
|
||||
function isPendingOverdue(row: Record<string, unknown>) {
|
||||
return props.activeTab === 'pending' && isEtaTodayOrEarlier(row.etaTime)
|
||||
}
|
||||
|
||||
function cardIndex(i: number) {
|
||||
return (props.page - 1) * props.pageSize + i + 1
|
||||
}
|
||||
|
||||
function isSelected(id: string) {
|
||||
return props.selectedIds.includes(id)
|
||||
}
|
||||
|
||||
function showProtocolPayment(row: Record<string, unknown>) {
|
||||
const source = String(row.sourceType || 'cell_storage').trim()
|
||||
return source === 'cell_storage' || source === ''
|
||||
}
|
||||
|
||||
function protocolPaymentSummary(row: Record<string, unknown>): QcProtocolPaymentSummary | null {
|
||||
const summary = row.paymentSummary
|
||||
if (!summary || typeof summary !== 'object') return null
|
||||
return summary as QcProtocolPaymentSummary
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.sample-reception-cards-body {
|
||||
border-top: 1px solid var(--mint-border, #e2e8f0);
|
||||
}
|
||||
|
||||
.sample-reception-cards-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
min-height: 120px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.sample-reception-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
cursor: default;
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #a8d4c4;
|
||||
box-shadow: 0 2px 8px rgba(4, 120, 87, 0.08);
|
||||
}
|
||||
|
||||
&.is-eta-overdue {
|
||||
border-color: #fca5a5;
|
||||
background: #fef2f2;
|
||||
|
||||
&:hover {
|
||||
border-color: #f87171;
|
||||
box-shadow: 0 2px 8px rgba(220, 38, 38, 0.1);
|
||||
}
|
||||
|
||||
.sample-reception-card__head {
|
||||
background: #fee2e2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sample-reception-card__head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
padding: 14px 16px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.sample-reception-card__head-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sample-reception-card__title-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.sample-reception-card__checkbox {
|
||||
flex-shrink: 0;
|
||||
margin-right: -4px;
|
||||
}
|
||||
|
||||
.sample-reception-card__index {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
.sample-reception-card__code {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.sample-reception-card__meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 8px 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sample-reception-card__meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
|
||||
dt {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
color: #0f172a;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
&--key {
|
||||
dt {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
dd {
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sample-reception-card__ops {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px 12px;
|
||||
flex-shrink: 0;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.sample-reception-card__ops-payment {
|
||||
flex: 1 1 240px;
|
||||
min-width: 0;
|
||||
margin-right: auto;
|
||||
|
||||
:deep(.qc-protocol-payment) {
|
||||
border: none;
|
||||
background: #e8f4ee;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
:deep(.qc-protocol-payment--compact) {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:deep(.qc-protocol-payment__compact-details) {
|
||||
border-top: none;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
:deep(.qc-protocol-payment__detail-chip) {
|
||||
border: none;
|
||||
background: rgb(255 255 255 / 60%);
|
||||
}
|
||||
}
|
||||
|
||||
.sample-reception-cards-empty {
|
||||
padding: 24px 0;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user