feat(web-admin): wire protocol sample freight batch UI
Replace per-sample transport fee entry with shared freight batch config in storage and donation protocol dialogs, and show mode/amount in view dialogs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
468a7c5e12
commit
5dd545ba5c
6 files changed
+551
-293
No files matched your search
@@ -0,0 +1,75 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export type SampleFreightItem = {
|
||||
sampleSource: 'cell_storage' | 'research_donation'
|
||||
sampleId: string
|
||||
protocolId?: string
|
||||
sampleCode?: string
|
||||
}
|
||||
|
||||
export type SampleFreight = {
|
||||
id: string
|
||||
transportMode: string
|
||||
transportModeLabel?: string
|
||||
freightAmount: number
|
||||
linkedSampleCount?: number
|
||||
createTime?: string
|
||||
items?: Array<SampleFreightItem & { protocolNo?: string }>
|
||||
deleted?: boolean
|
||||
}
|
||||
|
||||
type PageList<T> = { list: T[]; total: number; page: number; pageSize: number }
|
||||
|
||||
export function fetchSampleFreightList(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
}) {
|
||||
return request.get<PageList<SampleFreight>>({
|
||||
url: '/web/admin/sample-freights',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchSampleFreightDetail(id: string) {
|
||||
return request.get<SampleFreight>({ url: `/web/admin/sample-freights/${id}` })
|
||||
}
|
||||
|
||||
export function fetchCreateSampleFreight(data: {
|
||||
transportMode: string
|
||||
freightAmount: number
|
||||
items: SampleFreightItem[]
|
||||
}) {
|
||||
return request.post<SampleFreight>({
|
||||
url: '/web/admin/sample-freights',
|
||||
data,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchUpdateSampleFreight(
|
||||
id: string,
|
||||
data: { transportMode?: string; freightAmount?: number }
|
||||
) {
|
||||
return request.put<SampleFreight>({
|
||||
url: `/web/admin/sample-freights/${id}`,
|
||||
data,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchLinkSampleFreight(id: string, items: SampleFreightItem[]) {
|
||||
return request.post<SampleFreight>({
|
||||
url: `/web/admin/sample-freights/${id}/link`,
|
||||
data: { items },
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchUnlinkSampleFreight(id: string, items: SampleFreightItem[]) {
|
||||
return request.post<SampleFreight>({
|
||||
url: `/web/admin/sample-freights/${id}/unlink`,
|
||||
data: { items },
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
@@ -9,9 +9,18 @@
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="!selectedSamples.length"
|
||||
@click="openBatchTransportDialog"
|
||||
@click="openFreightDialog"
|
||||
>
|
||||
配置运输信息
|
||||
<el-icon><Van /></el-icon>
|
||||
配置运费
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="!canUnlinkFreight"
|
||||
@click="unlinkSelectedFreight"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
取消运费关联
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
@@ -86,25 +95,17 @@
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="运输类型" min-width="100" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{
|
||||
row.transport?.transportType
|
||||
? transportTypeLabel(String(row.transport.transportType))
|
||||
: '—'
|
||||
}}
|
||||
</template>
|
||||
<el-table-column label="运输方式" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ freightModeLabel(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="目的站点" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{
|
||||
siteName(String(row.transport?.destSiteId || ''), row.transport)
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预计到达" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.transport?.etaTime || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="快递/携带" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ sampleTransportExtra(row.transport) }}</template>
|
||||
<el-table-column
|
||||
label="运费(元)"
|
||||
min-width="120"
|
||||
align="right"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ freightAmountText(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
@@ -263,11 +264,11 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 配置运输信息 -->
|
||||
<!-- 配置运费 -->
|
||||
<el-dialog
|
||||
v-model="batchTransportVisible"
|
||||
title="配置运输信息"
|
||||
width="1200px"
|
||||
v-model="freightDialogVisible"
|
||||
title="配置运费"
|
||||
width="720px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
modal-class="protocol-dialog-mask"
|
||||
@@ -276,109 +277,72 @@
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<p class="sample-transport-hint batch-transport-dialog-hint">
|
||||
将以下运输信息应用到已选中的 {{ selectedSamples.length }} 个样本。
|
||||
将运费应用到已选中的 {{ selectedSamples.length }} 个样本(可跨协议共用一笔)。
|
||||
</p>
|
||||
<el-form label-position="top" class="biz-dialog-form">
|
||||
<div
|
||||
class="sample-transport-form-row"
|
||||
:class="{
|
||||
'sample-transport-form-row--ryxd': batchTransport.transportType === 'ryxd'
|
||||
}"
|
||||
>
|
||||
<el-form-item label="运输类型">
|
||||
<el-radio-group v-model="freightMode" class="freight-mode-radio" style="margin-bottom: 12px">
|
||||
<el-radio-button value="create">新建运费</el-radio-button>
|
||||
<el-radio-button value="link">关联已有</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-form v-if="freightMode === 'create'" label-position="top" class="biz-dialog-form">
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="运输方式">
|
||||
<el-select
|
||||
v-model="batchTransport.transportType"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="快递运输" value="kdys" />
|
||||
<el-option label="人员携带" value="ryxd" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="目的站点">
|
||||
<el-select
|
||||
v-model="batchTransport.destSiteId"
|
||||
placeholder="输入站点检索"
|
||||
v-model="freightForm.transportMode"
|
||||
placeholder="选择或输入运输方式"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
allow-create
|
||||
default-first-option
|
||||
clearable
|
||||
:loading="siteOptionsLoading"
|
||||
:remote-method="handleRemoteSearchSite"
|
||||
style="width: 100%"
|
||||
@visible-change="onSiteSelectVisible"
|
||||
>
|
||||
<el-option
|
||||
v-for="site in siteOptions"
|
||||
:key="site.id"
|
||||
:label="site.siteName"
|
||||
:value="site.id"
|
||||
v-for="opt in transportModeOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="预计到达时间">
|
||||
<el-date-picker
|
||||
v-model="batchTransport.etaTime"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
placeholder="选择预计到达时间"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
/>
|
||||
<el-form-item label="运费(元)">
|
||||
<AmountInput v-model="freightForm.freightAmount" :min="0" placeholder="请输入运费" />
|
||||
</el-form-item>
|
||||
<template v-if="batchTransport.transportType === 'kdys'">
|
||||
<el-form-item label="快递公司">
|
||||
<el-select
|
||||
v-model="batchTransport.expressCompanyId"
|
||||
placeholder="输入快递公司检索"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
clearable
|
||||
:loading="expressCompanyOptionsLoading"
|
||||
:remote-method="handleRemoteSearchExpressCompany"
|
||||
style="width: 100%"
|
||||
@visible-change="onExpressCompanySelectVisible"
|
||||
>
|
||||
<el-option
|
||||
v-for="ec in expressCompanyOptions"
|
||||
:key="ec.id"
|
||||
:label="ec.expressName"
|
||||
:value="ec.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="快递单号">
|
||||
<el-input v-model="batchTransport.waybillNo" placeholder="请输入快递单号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="快递费用(元)">
|
||||
<el-input
|
||||
v-model="batchTransport.expressFee"
|
||||
type="number"
|
||||
step="any"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-if="batchTransport.transportType === 'ryxd'">
|
||||
<el-form-item label="携带人员姓名">
|
||||
<el-input v-model="batchTransport.carrierName" placeholder="请输入姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="携带人员电话">
|
||||
<el-input v-model="batchTransport.carrierPhone" placeholder="请输入电话" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</div>
|
||||
</el-form>
|
||||
<div v-else class="list-body list-wrap">
|
||||
<el-table
|
||||
:data="existingFreightList"
|
||||
stripe
|
||||
class="list-table inner-table"
|
||||
highlight-current-row
|
||||
@current-change="onExistingFreightSelect"
|
||||
>
|
||||
<el-table-column label="运输方式" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.transportModeLabel || row.transportMode || '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="运费(元)" min-width="100" align="right" header-align="center">
|
||||
<template #default="{ row }">{{ fmtMoney(row.freightAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="关联样本数"
|
||||
min-width="96"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ row.linkedSampleCount ?? 0 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.createTime || '—' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="batchTransportVisible = false">
|
||||
<el-button class="btn-ghost" @click="freightDialogVisible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="confirmBatchTransportApply">
|
||||
<el-button class="btn-main" type="primary" :loading="freightSubmitting" @click="confirmFreight">
|
||||
<el-icon><Check /></el-icon>
|
||||
确定应用
|
||||
</el-button>
|
||||
@@ -388,21 +352,28 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Plus, Edit, Delete, Close, Check } from '@element-plus/icons-vue'
|
||||
import { Plus, Edit, Delete, Close, Check, Van } from '@element-plus/icons-vue'
|
||||
import AmountInput from '@/components/common/amount-input/index.vue'
|
||||
import SampleCodeWithSpecialDisplay from '@/components/business/sample-code-with-special-display/index.vue'
|
||||
import {
|
||||
sampleStatusLabel,
|
||||
sampleStatusTagType,
|
||||
transportTypeLabel
|
||||
} from '@/utils/status-tag-registry'
|
||||
import { sampleStatusLabel, sampleStatusTagType } from '@/utils/status-tag-registry'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
import { fetchDictDataOptions } from '@/api/admin'
|
||||
import {
|
||||
fetchCreateSampleFreight,
|
||||
fetchLinkSampleFreight,
|
||||
fetchSampleFreightList,
|
||||
fetchUnlinkSampleFreight,
|
||||
type SampleFreight,
|
||||
type SampleFreightItem
|
||||
} from '@/api/sample-freight'
|
||||
|
||||
defineOptions({ name: 'SampleTransportFormSection' })
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
samples: Array<Record<string, any>>
|
||||
sampleSource: 'cell_storage' | 'research_donation'
|
||||
protocolId?: string
|
||||
sampleTypeOptions: Array<{
|
||||
id: string
|
||||
typeName: string
|
||||
@@ -413,8 +384,8 @@
|
||||
}>
|
||||
sampleCodeOptions: Api.BizSampleCode.SampleCodeItem[]
|
||||
unitOptions: Array<{ id: string; unitName: string }>
|
||||
siteOptions: Api.Site.SiteItem[]
|
||||
expressCompanyOptions: Api.ExpressCompany.ExpressCompanyItem[]
|
||||
siteOptions?: Api.Site.SiteItem[]
|
||||
expressCompanyOptions?: Api.ExpressCompany.ExpressCompanyItem[]
|
||||
showPrice?: boolean
|
||||
hideCollectDetailFields?: boolean
|
||||
/** 添加/编辑样本弹窗内隐藏采集量、单位(不影响列表列与存储年限) */
|
||||
@@ -429,6 +400,7 @@
|
||||
expressCompanyOptionsLoading?: boolean
|
||||
}>(),
|
||||
{
|
||||
protocolId: '',
|
||||
showPrice: false,
|
||||
hideCollectDetailFields: false,
|
||||
hideDialogCollectAmountUnit: false,
|
||||
@@ -452,70 +424,73 @@
|
||||
(e: 'search-express-company', query: string): void
|
||||
}>()
|
||||
|
||||
type SampleTransportForm = {
|
||||
transportType: string
|
||||
expressCompanyId: string
|
||||
waybillNo: string
|
||||
destSiteId: string
|
||||
expressFee: number | null
|
||||
etaTime: string
|
||||
carrierName: string
|
||||
carrierPhone: string
|
||||
}
|
||||
|
||||
const sampleTableRef = ref()
|
||||
const selectedSamples = ref<Array<Record<string, any>>>([])
|
||||
let sampleRowSeq = 0
|
||||
|
||||
const batchTransport = reactive<SampleTransportForm>(createEmptyTransport())
|
||||
const batchTransportVisible = ref(false)
|
||||
const freightDialogVisible = ref(false)
|
||||
const freightMode = ref<'create' | 'link'>('create')
|
||||
const freightSubmitting = ref(false)
|
||||
const freightForm = reactive({
|
||||
transportMode: '' as string,
|
||||
freightAmount: null as number | null
|
||||
})
|
||||
const transportModeOptions = ref<Array<{ label: string; value: string }>>([])
|
||||
const existingFreightList = ref<SampleFreight[]>([])
|
||||
const selectedExistingFreight = ref<SampleFreight | null>(null)
|
||||
|
||||
const sampleDialogVisible = ref(false)
|
||||
const sampleDialogMode = ref<'add' | 'edit'>('add')
|
||||
const sampleDialogIndex = ref(-1)
|
||||
const sampleDraft = reactive<Record<string, any>>({})
|
||||
|
||||
const canUnlinkFreight = computed(() =>
|
||||
selectedSamples.value.some((r) => r.freight?.id || r.freightDraft || r.freightId)
|
||||
)
|
||||
|
||||
function fmtMoney(v: unknown): string {
|
||||
if (v == null || v === '') return '—'
|
||||
return '¥' + formatMoneyDisplay(v)
|
||||
}
|
||||
|
||||
function createEmptyTransport(): SampleTransportForm {
|
||||
return {
|
||||
transportType: '',
|
||||
expressCompanyId: '',
|
||||
waybillNo: '',
|
||||
destSiteId: '',
|
||||
expressFee: null,
|
||||
etaTime: '',
|
||||
carrierName: '',
|
||||
carrierPhone: ''
|
||||
}
|
||||
function freightModeLabel(row: Record<string, any>) {
|
||||
return (
|
||||
row.freight?.transportModeLabel ||
|
||||
row.freightDraft?.transportMode ||
|
||||
row.freight?.transportMode ||
|
||||
'—'
|
||||
)
|
||||
}
|
||||
|
||||
function freightAmountText(row: Record<string, any>) {
|
||||
const amount = row.freight?.freightAmount ?? row.freightDraft?.freightAmount
|
||||
if (amount == null || amount === '') return '—'
|
||||
const base = fmtMoney(amount)
|
||||
const n = Number(row.freight?.linkedSampleCount || 0)
|
||||
return n > 1 ? `${base}(共用)` : base
|
||||
}
|
||||
|
||||
function normalizeSampleRow(raw: Record<string, any>, _index: number) {
|
||||
const transportRaw = raw.transport && typeof raw.transport === 'object' ? raw.transport : {}
|
||||
const statusRaw = raw.sampleStatus
|
||||
const sampleStatus =
|
||||
statusRaw != null && String(statusRaw).trim() !== '' ? String(statusRaw).trim() : ''
|
||||
const freight =
|
||||
raw.freight && typeof raw.freight === 'object'
|
||||
? { ...raw.freight }
|
||||
: raw.freightId
|
||||
? { id: raw.freightId }
|
||||
: null
|
||||
const freightDraft =
|
||||
raw.freightDraft && typeof raw.freightDraft === 'object' ? { ...raw.freightDraft } : undefined
|
||||
return {
|
||||
...raw,
|
||||
sampleStatus,
|
||||
isSpecial: !!raw.isSpecial,
|
||||
specialRemark: String(raw.specialRemark || '').trim(),
|
||||
_rowKey: raw.id || raw._rowKey || `new-${++sampleRowSeq}`,
|
||||
transport: {
|
||||
...createEmptyTransport(),
|
||||
transportType: transportRaw.transportType || '',
|
||||
expressCompanyId: transportRaw.expressCompanyId || '',
|
||||
waybillNo: transportRaw.waybillNo || '',
|
||||
destSiteId: transportRaw.destSiteId || '',
|
||||
destSiteName: transportRaw.destSiteName || '',
|
||||
expressFee: transportRaw.expressFee ?? null,
|
||||
etaTime: transportRaw.etaTime || '',
|
||||
carrierName: transportRaw.carrierName || '',
|
||||
carrierPhone: transportRaw.carrierPhone || ''
|
||||
}
|
||||
freight,
|
||||
freightDraft,
|
||||
freightId: freight?.id || raw.freightId || ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,41 +533,6 @@
|
||||
return name
|
||||
}
|
||||
|
||||
function siteName(id: string, transport?: Record<string, any>) {
|
||||
if (!id) return '—'
|
||||
const fromOpt = props.siteOptions.find((x) => x.id === id)?.siteName
|
||||
if (fromOpt && fromOpt !== id) return fromOpt
|
||||
const fromTransport = String(transport?.destSiteName || '').trim()
|
||||
if (fromTransport && fromTransport !== id) return fromTransport
|
||||
return '—'
|
||||
}
|
||||
|
||||
function expressName(id: string) {
|
||||
if (!id) return '—'
|
||||
return props.expressCompanyOptions.find((x) => x.id === id)?.expressName || '—'
|
||||
}
|
||||
|
||||
function sampleTransportExtra(transport: Record<string, any> | undefined) {
|
||||
if (!transport?.transportType) return '—'
|
||||
if (transport.transportType === 'kdys') {
|
||||
const parts: string[] = []
|
||||
const ec = expressName(String(transport.expressCompanyId || ''))
|
||||
if (ec !== '—') parts.push(ec)
|
||||
if (transport.waybillNo) parts.push(String(transport.waybillNo))
|
||||
if (transport.expressFee != null && transport.expressFee !== '') {
|
||||
parts.push(`¥${transport.expressFee}`)
|
||||
}
|
||||
return parts.length ? parts.join(' · ') : '—'
|
||||
}
|
||||
if (transport.transportType === 'ryxd') {
|
||||
const parts: string[] = []
|
||||
if (transport.carrierName) parts.push(String(transport.carrierName))
|
||||
if (transport.carrierPhone) parts.push(String(transport.carrierPhone))
|
||||
return parts.length ? parts.join(' · ') : '—'
|
||||
}
|
||||
return '—'
|
||||
}
|
||||
|
||||
function sampleCodeOptionLabel(sc: Api.BizSampleCode.SampleCodeItem) {
|
||||
if (sc.usedInProtocol) return `${sc.sampleCode}(已占用)`
|
||||
if (sc.issued) return `${sc.sampleCode}(已下发)`
|
||||
@@ -638,22 +578,6 @@
|
||||
if (visible) emit('search-unit', '')
|
||||
}
|
||||
|
||||
function handleRemoteSearchSite(query: string) {
|
||||
emit('search-site', query)
|
||||
}
|
||||
|
||||
function onSiteSelectVisible(visible: boolean) {
|
||||
if (visible) emit('search-site', '')
|
||||
}
|
||||
|
||||
function handleRemoteSearchExpressCompany(query: string) {
|
||||
emit('search-express-company', query)
|
||||
}
|
||||
|
||||
function onExpressCompanySelectVisible(visible: boolean) {
|
||||
if (visible) emit('search-express-company', '')
|
||||
}
|
||||
|
||||
function refreshSampleDialogOptions() {
|
||||
emit('search-sample-type', '')
|
||||
if (!props.autoGenerateSampleCode) {
|
||||
@@ -662,9 +586,68 @@
|
||||
emit('search-unit', '')
|
||||
}
|
||||
|
||||
function refreshBatchTransportOptions() {
|
||||
emit('search-site', '')
|
||||
emit('search-express-company', '')
|
||||
async function loadTransportModeOptions() {
|
||||
try {
|
||||
const res = await fetchDictDataOptions('sample_transport_mode')
|
||||
const list = Array.isArray(res) ? res : (res as any)?.data || []
|
||||
transportModeOptions.value = list.map((x: any) => ({
|
||||
label: x.label || x.dictLabel || x.value || x.dictValue,
|
||||
value: x.value || x.dictValue
|
||||
}))
|
||||
} catch {
|
||||
transportModeOptions.value = [
|
||||
{ label: '顺丰快递', value: 'sfkd' },
|
||||
{ label: '专线车', value: 'zxc' }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function transportModeLabelOf(mode: string) {
|
||||
const hit = transportModeOptions.value.find((x) => x.value === mode || x.label === mode)
|
||||
return hit?.label || mode
|
||||
}
|
||||
|
||||
function toFreightItems(rows: Array<Record<string, any>>): SampleFreightItem[] {
|
||||
return rows
|
||||
.filter((r) => r.id)
|
||||
.map((r) => ({
|
||||
sampleSource: props.sampleSource,
|
||||
sampleId: String(r.id),
|
||||
protocolId: props.protocolId || undefined,
|
||||
sampleCode: r.sampleCode || undefined
|
||||
}))
|
||||
}
|
||||
|
||||
function applyFreightToRows(
|
||||
rows: Array<Record<string, any>>,
|
||||
freight: Partial<SampleFreight> & { freightDraft?: any }
|
||||
) {
|
||||
const selectedSet = new Set(rows)
|
||||
const next = props.samples.map((row) => {
|
||||
if (!selectedSet.has(row)) return row
|
||||
return {
|
||||
...row,
|
||||
freight: freight.id
|
||||
? {
|
||||
id: freight.id,
|
||||
transportMode: freight.transportMode,
|
||||
transportModeLabel: freight.transportModeLabel || transportModeLabelOf(String(freight.transportMode || '')),
|
||||
freightAmount: freight.freightAmount,
|
||||
linkedSampleCount: freight.linkedSampleCount
|
||||
}
|
||||
: {
|
||||
id: '',
|
||||
transportMode: freight.transportMode,
|
||||
transportModeLabel:
|
||||
freight.transportModeLabel || transportModeLabelOf(String(freight.transportMode || '')),
|
||||
freightAmount: freight.freightAmount,
|
||||
linkedSampleCount: freight.linkedSampleCount
|
||||
},
|
||||
freightId: freight.id || '',
|
||||
freightDraft: freight.freightDraft
|
||||
}
|
||||
})
|
||||
updateSamples(next)
|
||||
}
|
||||
|
||||
function onSampleCodeChange(row: Record<string, any>) {
|
||||
@@ -709,35 +692,138 @@
|
||||
selectedSamples.value = rows
|
||||
}
|
||||
|
||||
function openBatchTransportDialog() {
|
||||
async function openFreightDialog() {
|
||||
if (!selectedSamples.value.length) {
|
||||
ElMessage.warning('请先勾选需要应用运输信息的样本')
|
||||
ElMessage.warning('请先勾选需要配置运费的样本')
|
||||
return
|
||||
}
|
||||
Object.assign(batchTransport, createEmptyTransport())
|
||||
const firstWithTransport = selectedSamples.value.find((row) => row.transport?.destSiteId)
|
||||
if (firstWithTransport?.transport) {
|
||||
Object.assign(batchTransport, { ...firstWithTransport.transport })
|
||||
freightMode.value = 'create'
|
||||
freightForm.transportMode = ''
|
||||
freightForm.freightAmount = null
|
||||
selectedExistingFreight.value = null
|
||||
await loadTransportModeOptions()
|
||||
try {
|
||||
const res = await fetchSampleFreightList({ page: 1, pageSize: 50 })
|
||||
const data = (res as any)?.data || res
|
||||
existingFreightList.value = data?.list || []
|
||||
} catch {
|
||||
existingFreightList.value = []
|
||||
}
|
||||
batchTransportVisible.value = true
|
||||
refreshBatchTransportOptions()
|
||||
freightDialogVisible.value = true
|
||||
}
|
||||
|
||||
function confirmBatchTransportApply() {
|
||||
if (!batchTransport.transportType) {
|
||||
ElMessage.warning('请选择运输类型')
|
||||
function onExistingFreightSelect(row: SampleFreight | null) {
|
||||
selectedExistingFreight.value = row
|
||||
}
|
||||
|
||||
async function confirmFreight() {
|
||||
const selected = selectedSamples.value
|
||||
if (!selected.length) return
|
||||
const persisted = selected.filter((r) => r.id)
|
||||
const drafts = selected.filter((r) => !r.id)
|
||||
|
||||
freightSubmitting.value = true
|
||||
try {
|
||||
if (freightMode.value === 'create') {
|
||||
if (!freightForm.transportMode || freightForm.freightAmount == null) {
|
||||
ElMessage.warning('请填写运输方式与运费')
|
||||
return
|
||||
}
|
||||
const amount = Number(freightForm.freightAmount)
|
||||
if (Number.isNaN(amount) || amount < 0) {
|
||||
ElMessage.warning('运费须为不小于 0 的数字')
|
||||
return
|
||||
}
|
||||
let created: SampleFreight | null = null
|
||||
if (persisted.length) {
|
||||
const res = await fetchCreateSampleFreight({
|
||||
transportMode: freightForm.transportMode,
|
||||
freightAmount: amount,
|
||||
items: toFreightItems(persisted)
|
||||
})
|
||||
created = ((res as any)?.data || res) as SampleFreight
|
||||
applyFreightToRows(persisted, created)
|
||||
}
|
||||
if (drafts.length) {
|
||||
const clientKey = `fd_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
||||
applyFreightToRows(drafts, {
|
||||
id: created?.id || '',
|
||||
transportMode: freightForm.transportMode,
|
||||
transportModeLabel: transportModeLabelOf(freightForm.transportMode),
|
||||
freightAmount: amount,
|
||||
linkedSampleCount: (created?.linkedSampleCount || 0) + drafts.length,
|
||||
freightDraft: created?.id
|
||||
? undefined
|
||||
: {
|
||||
clientKey,
|
||||
transportMode: freightForm.transportMode,
|
||||
freightAmount: amount
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (!selectedExistingFreight.value?.id) {
|
||||
ElMessage.warning('请选择一笔已有运费')
|
||||
return
|
||||
}
|
||||
const target = selectedExistingFreight.value
|
||||
if (persisted.length) {
|
||||
const res = await fetchLinkSampleFreight(target.id, toFreightItems(persisted))
|
||||
const linked = ((res as any)?.data || res) as SampleFreight
|
||||
applyFreightToRows(persisted, linked)
|
||||
}
|
||||
if (drafts.length) {
|
||||
applyFreightToRows(drafts, {
|
||||
...target,
|
||||
freightId: target.id,
|
||||
freightDraft: undefined
|
||||
} as any)
|
||||
}
|
||||
}
|
||||
freightDialogVisible.value = false
|
||||
ElMessage.success(`已为 ${selected.length} 个样本配置运费`)
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '配置运费失败')
|
||||
} finally {
|
||||
freightSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function unlinkSelectedFreight() {
|
||||
const selected = selectedSamples.value.filter(
|
||||
(r) => r.freight?.id || r.freightDraft || r.freightId
|
||||
)
|
||||
if (!selected.length) {
|
||||
ElMessage.warning('请勾选已关联运费的样本')
|
||||
return
|
||||
}
|
||||
const payload = { ...batchTransport }
|
||||
const selectedSet = new Set(selectedSamples.value)
|
||||
const next = props.samples.map((row) => {
|
||||
if (!selectedSet.has(row)) return row
|
||||
const transport = { ...(row.transport || createEmptyTransport()), ...payload }
|
||||
return { ...row, transport }
|
||||
})
|
||||
updateSamples(next)
|
||||
batchTransportVisible.value = false
|
||||
ElMessage.success(`已为 ${selectedSamples.value.length} 个样本应用运输信息`)
|
||||
try {
|
||||
await ElMessageBox.confirm('确定取消所选样本的运费关联吗?', '取消运费关联', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
const byFreight = new Map<string, Array<Record<string, any>>>()
|
||||
for (const row of selected) {
|
||||
const fid = String(row.freight?.id || row.freightId || '')
|
||||
if (!fid) continue
|
||||
if (!byFreight.has(fid)) byFreight.set(fid, [])
|
||||
byFreight.get(fid)!.push(row)
|
||||
}
|
||||
for (const [fid, rows] of byFreight) {
|
||||
await fetchUnlinkSampleFreight(fid, toFreightItems(rows))
|
||||
}
|
||||
const selectedSet = new Set(selected)
|
||||
updateSamples(
|
||||
props.samples.map((row) => {
|
||||
if (!selectedSet.has(row)) return row
|
||||
return { ...row, freight: null, freightId: '', freightDraft: undefined }
|
||||
})
|
||||
)
|
||||
ElMessage.success('已取消运费关联')
|
||||
}
|
||||
|
||||
function onSpecialSampleToggle(checked: boolean | string | number) {
|
||||
|
||||
@@ -469,6 +469,8 @@
|
||||
</div>
|
||||
<SampleTransportFormSection
|
||||
v-model:samples="formData.samples"
|
||||
sample-source="cell_storage"
|
||||
:protocol-id="formData.id || ''"
|
||||
hide-dialog-collect-amount-unit
|
||||
:sample-type-options="sampleTypeOptions"
|
||||
:sample-type-options-loading="sampleTypeLoading"
|
||||
@@ -476,17 +478,11 @@
|
||||
:sample-code-options-loading="sampleCodeLoading"
|
||||
:unit-options="unitOptions"
|
||||
:unit-options-loading="unitLoading"
|
||||
:site-options="siteOptions"
|
||||
:site-options-loading="siteLoading"
|
||||
:express-company-options="expressCompanyOptions"
|
||||
:express-company-options-loading="expressCompanyLoading"
|
||||
:show-price="true"
|
||||
@price-change="onSamplePriceChange"
|
||||
@search-sample-code="remoteSearchSampleCodes"
|
||||
@search-sample-type="remoteSearchSampleTypes"
|
||||
@search-unit="remoteSearchUnits"
|
||||
@search-site="remoteSearchSites"
|
||||
@search-express-company="remoteSearchExpressCompanies"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -850,7 +846,8 @@
|
||||
class="detail-badge"
|
||||
:class="{
|
||||
'badge-green': detail.payStatus === 'yjf',
|
||||
'badge-amber': detail.payStatus === 'dfj' || detail.payStatus === 'jfqrz',
|
||||
'badge-primary': detail.payStatus === 'dfj',
|
||||
'badge-amber': detail.payStatus === 'jfqrz',
|
||||
'badge-gray': detail.payStatus === 'tfqrz' || detail.payStatus === 'ytf'
|
||||
}"
|
||||
>
|
||||
@@ -908,6 +905,7 @@
|
||||
<el-date-picker
|
||||
v-model="detail.transferTime"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择时间"
|
||||
clearable
|
||||
@@ -917,20 +915,11 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="缴费状态">
|
||||
<el-tag
|
||||
v-if="isPayDetailLocked(detail)"
|
||||
:type="payStatusTagType(String(detail.payStatus))"
|
||||
:type="payStatusTagType(String(detail.payStatus || 'dfj'))"
|
||||
size="small"
|
||||
>
|
||||
{{ payStatusLabel(String(detail.payStatus)) }}
|
||||
{{ payStatusLabel(String(detail.payStatus || 'dfj')) }}
|
||||
</el-tag>
|
||||
<el-select
|
||||
v-else
|
||||
v-model="detail.payStatus"
|
||||
placeholder="状态"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="待缴费" value="dfj" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="应缴费时间">
|
||||
<el-date-picker
|
||||
@@ -2367,13 +2356,14 @@
|
||||
sampleCollectTime: formData.sampleCollectTime || undefined,
|
||||
maternalBloodCollectTime: formData.maternalBloodCollectTime || undefined
|
||||
},
|
||||
samples: formData.samples.map(({ _rowKey, transport, ...s }) => ({
|
||||
samples: formData.samples.map(({ _rowKey, transport, freight, freightDraft, ...s }) => ({
|
||||
...s,
|
||||
sampleStatus:
|
||||
s.sampleStatus != null && String(s.sampleStatus).trim() !== ''
|
||||
? String(s.sampleStatus).trim()
|
||||
: null,
|
||||
transport: transport?.transportType ? { ...transport } : undefined
|
||||
freightId: freight?.id || s.freightId || undefined,
|
||||
freightDraft: freightDraft || undefined
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -4238,6 +4228,11 @@
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
&.badge-primary {
|
||||
background: #ecfdf5;
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
&.badge-amber {
|
||||
background: #fffbeb;
|
||||
color: #d97706;
|
||||
|
||||
@@ -307,7 +307,16 @@
|
||||
>
|
||||
<div class="section-title section-title--banner"><h3>样本及运输信息</h3></div>
|
||||
<div v-if="samples.length" class="sample-cards">
|
||||
<div v-for="(row, sIdx) in samples" :key="row.id || sIdx" class="sample-card">
|
||||
<div
|
||||
v-for="(row, sIdx) in samples"
|
||||
:key="row.id || sIdx"
|
||||
class="sample-card"
|
||||
:class="{
|
||||
'is-highlight':
|
||||
!!highlightSampleCode && String(row.sampleCode || '') === highlightSampleCode
|
||||
}"
|
||||
:id="row.sampleCode ? `view-sample-${row.sampleCode}` : undefined"
|
||||
>
|
||||
<div class="sample-card__header">
|
||||
<div class="sample-card__title">
|
||||
<span class="sample-card__seq">{{ sIdx + 1 }}</span>
|
||||
@@ -355,31 +364,27 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="row.transport?.transportType"
|
||||
v-if="row.freight?.id || row.freight?.transportMode"
|
||||
class="sample-card__section sample-card__section--transport"
|
||||
>
|
||||
<div class="sample-card__section-label">运输信息</div>
|
||||
<div class="sample-card__section-label">运费信息</div>
|
||||
<div class="data-grid data-grid--4">
|
||||
<div class="data-item">
|
||||
<div class="data-label">运输类型</div>
|
||||
<div class="data-label">运输方式</div>
|
||||
<div class="data-value">{{
|
||||
transportTypeLabel(String(row.transport?.transportType || ''))
|
||||
dash(row.freight?.transportModeLabel || row.freight?.transportMode)
|
||||
}}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">目的站点</div>
|
||||
<div class="data-label">运费(元)</div>
|
||||
<div class="data-value">{{
|
||||
siteName(String(row.transport?.destSiteId || ''))
|
||||
row.freight?.freightAmount != null
|
||||
? '¥' + formatMoneyDisplay(row.freight.freightAmount)
|
||||
: '—'
|
||||
}}{{
|
||||
Number(row.freight?.linkedSampleCount || 0) > 1 ? '(共用)' : ''
|
||||
}}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">预计到达</div>
|
||||
<div class="data-value">{{ dash(row.transport?.etaTime) }}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">快递/携带</div>
|
||||
<div class="data-value">{{ sampleTransportExtra(row.transport) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sample-card__section sample-card__section--reception">
|
||||
@@ -419,6 +424,35 @@
|
||||
<el-empty v-else description="暂无样本" :image-size="48" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="showHandlePanels"
|
||||
id="view-section-storage-cert-handle"
|
||||
class="section-card section-card--white"
|
||||
>
|
||||
<div class="section-title section-title--banner"><h3>储存证办理</h3></div>
|
||||
<ProtocolHandlePanel
|
||||
v-if="protocolId"
|
||||
mode="storage-cert"
|
||||
:protocol-id="protocolId"
|
||||
:initial="storageCertInfo"
|
||||
@saved="loadDetail"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="showHandlePanels"
|
||||
id="view-section-insurance-handle"
|
||||
class="section-card section-card--white"
|
||||
>
|
||||
<div class="section-title section-title--banner"><h3>保险办理</h3></div>
|
||||
<ProtocolHandlePanel
|
||||
v-if="protocolId"
|
||||
mode="insurance"
|
||||
:protocol-id="protocolId"
|
||||
:initial="fullmoonInsuranceInfo"
|
||||
@saved="loadDetail"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 客户信息 -->
|
||||
<div id="view-section-party" class="section-card section-card--green">
|
||||
<div class="section-title section-title--banner"><h3>客户信息</h3></div>
|
||||
@@ -926,7 +960,7 @@
|
||||
<div class="flow-detail-card__kv">
|
||||
<span class="flow-detail-card__label">确认状态</span>
|
||||
<span class="flow-detail-card__value"
|
||||
><el-tag type="success" size="small">已缴费</el-tag></span
|
||||
><el-tag type="success" size="small">缴费成功</el-tag></span
|
||||
>
|
||||
</div>
|
||||
<div v-if="detail.transferTime" class="flow-detail-card__kv">
|
||||
@@ -1225,6 +1259,7 @@
|
||||
import SampleCodeWithSpecialDisplay from '@/components/business/sample-code-with-special-display/index.vue'
|
||||
import SampleProductionInfoCards from '@/components/business/sample-production-info-cards/index.vue'
|
||||
import SampleStorageReleaseInfo from '@/components/business/sample-storage-release-info/index.vue'
|
||||
import ProtocolHandlePanel from '@/components/business/protocol-handle-panel/index.vue'
|
||||
import {
|
||||
fetchCollectionProtocolAuditList,
|
||||
fetchCollectionProtocolDetail,
|
||||
@@ -1271,8 +1306,17 @@
|
||||
showStorageCert?: boolean
|
||||
/** 满月待办查看:客户信息下方独立展示新生儿保险信息 */
|
||||
showFullmoonInsurance?: boolean
|
||||
/** 样本信息管理:储存证/保险轻量办理 */
|
||||
showHandlePanels?: boolean
|
||||
highlightSampleCode?: string
|
||||
}>(),
|
||||
{ showAuditHistory: false, showStorageCert: false, showFullmoonInsurance: false }
|
||||
{
|
||||
showAuditHistory: false,
|
||||
showStorageCert: false,
|
||||
showFullmoonInsurance: false,
|
||||
showHandlePanels: false,
|
||||
highlightSampleCode: ''
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -1289,6 +1333,11 @@
|
||||
|
||||
const formTabs = computed(() => {
|
||||
const tabs: Array<{ key: string; label: string }> = [...baseFormTabs]
|
||||
if (props.showHandlePanels) {
|
||||
const sampleIdx = tabs.findIndex((t) => t.key === 'sample')
|
||||
tabs.splice(sampleIdx + 1, 0, { key: 'storage-cert-handle', label: '储存证办理' })
|
||||
tabs.splice(sampleIdx + 2, 0, { key: 'insurance-handle', label: '保险办理' })
|
||||
}
|
||||
if (props.showFullmoonInsurance) {
|
||||
const partyIdx = tabs.findIndex((t) => t.key === 'party')
|
||||
tabs.splice(partyIdx + 1, 0, { key: 'fullmoon-insurance', label: '新生儿保险' })
|
||||
@@ -1705,7 +1754,7 @@
|
||||
return {
|
||||
key: `ov-${i}`,
|
||||
label: name,
|
||||
subLabel: '已缴费',
|
||||
subLabel: '缴费成功',
|
||||
dotClass: 'done',
|
||||
dotIcon: '✓',
|
||||
labelClass: 'done',
|
||||
@@ -1726,7 +1775,7 @@
|
||||
return {
|
||||
key: `ov-${i}`,
|
||||
label: name,
|
||||
subLabel: '待缴费',
|
||||
subLabel: '已缴费',
|
||||
dotClass: 'pending',
|
||||
dotIcon: '○',
|
||||
labelClass: ''
|
||||
@@ -1993,6 +2042,13 @@
|
||||
|
||||
void finishLookups(data, baseLookupsPromise)
|
||||
void auditPromise
|
||||
if (props.highlightSampleCode) {
|
||||
nextTick(() => {
|
||||
document
|
||||
.getElementById(`view-sample-${props.highlightSampleCode}`)
|
||||
?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToSection(key: string) {
|
||||
@@ -2024,6 +2080,19 @@
|
||||
if (visible && id) loadDetail()
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.highlightSampleCode, bundle.value] as const,
|
||||
([visible, code]) => {
|
||||
if (!visible || !code || !bundle.value) return
|
||||
nextTick(() => {
|
||||
document.getElementById(`view-sample-${code}`)?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -2394,6 +2463,11 @@
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
&.is-highlight {
|
||||
border-color: var(--primary, #10b981);
|
||||
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.18);
|
||||
}
|
||||
}
|
||||
|
||||
.sample-card__header {
|
||||
|
||||
@@ -255,6 +255,8 @@
|
||||
</div>
|
||||
<SampleTransportFormSection
|
||||
v-model:samples="formData.samples"
|
||||
sample-source="research_donation"
|
||||
:protocol-id="formData.id || ''"
|
||||
hide-collect-detail-fields
|
||||
auto-generate-sample-code
|
||||
:reserve-sample-code="reserveDonationSampleCode"
|
||||
@@ -264,16 +266,10 @@
|
||||
:sample-code-options-loading="sampleCodeLoading"
|
||||
:unit-options="unitOptions"
|
||||
:unit-options-loading="unitLoading"
|
||||
:site-options="siteOptions"
|
||||
:site-options-loading="siteLoading"
|
||||
:express-company-options="expressCompanyOptions"
|
||||
:express-company-options-loading="expressCompanyLoading"
|
||||
:show-price="false"
|
||||
@search-sample-code="remoteSearchSampleCodes"
|
||||
@search-sample-type="remoteSearchSampleTypes"
|
||||
@search-unit="remoteSearchUnits"
|
||||
@search-site="remoteSearchSites"
|
||||
@search-express-company="remoteSearchExpressCompanies"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1411,13 +1407,14 @@
|
||||
maternalBloodCollectTime: formData.maternalBloodCollectTime || undefined
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
samples: formData.samples.map(({ _rowKey: _, transport, ...s }) => ({
|
||||
samples: formData.samples.map(({ _rowKey: _, transport, freight, freightDraft, ...s }) => ({
|
||||
...s,
|
||||
sampleStatus:
|
||||
s.sampleStatus != null && String(s.sampleStatus).trim() !== ''
|
||||
? String(s.sampleStatus).trim()
|
||||
: null,
|
||||
transport: transport?.transportType ? { ...transport } : undefined
|
||||
freightId: freight?.id || s.freightId || undefined,
|
||||
freightDraft: freightDraft || undefined
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -225,7 +225,16 @@
|
||||
<div id="view-section-sample" class="section-card section-card--white">
|
||||
<div class="section-title section-title--banner"><h3>样本及运输信息</h3></div>
|
||||
<div v-if="samples.length" class="sample-cards">
|
||||
<div v-for="(row, sIdx) in samples" :key="row.id || sIdx" class="sample-card">
|
||||
<div
|
||||
v-for="(row, sIdx) in samples"
|
||||
:key="row.id || sIdx"
|
||||
class="sample-card"
|
||||
:class="{
|
||||
'is-highlight':
|
||||
!!highlightSampleCode && String(row.sampleCode || '') === highlightSampleCode
|
||||
}"
|
||||
:id="row.sampleCode ? `view-sample-${row.sampleCode}` : undefined"
|
||||
>
|
||||
<div class="sample-card__header">
|
||||
<div class="sample-card__title">
|
||||
<span class="sample-card__seq">{{ sIdx + 1 }}</span>
|
||||
@@ -261,31 +270,27 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="row.transport?.transportType"
|
||||
v-if="row.freight?.id || row.freight?.transportMode"
|
||||
class="sample-card__section sample-card__section--transport"
|
||||
>
|
||||
<div class="sample-card__section-label">运输信息</div>
|
||||
<div class="sample-card__section-label">运费信息</div>
|
||||
<div class="data-grid data-grid--4">
|
||||
<div class="data-item">
|
||||
<div class="data-label">运输类型</div>
|
||||
<div class="data-label">运输方式</div>
|
||||
<div class="data-value">{{
|
||||
transportTypeLabel(String(row.transport?.transportType || ''))
|
||||
dash(row.freight?.transportModeLabel || row.freight?.transportMode)
|
||||
}}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">目的站点</div>
|
||||
<div class="data-label">运费(元)</div>
|
||||
<div class="data-value">{{
|
||||
siteName(String(row.transport?.destSiteId || ''))
|
||||
row.freight?.freightAmount != null
|
||||
? '¥' + formatMoneyDisplay(row.freight.freightAmount)
|
||||
: '—'
|
||||
}}{{
|
||||
Number(row.freight?.linkedSampleCount || 0) > 1 ? '(共用)' : ''
|
||||
}}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">预计到达</div>
|
||||
<div class="data-value">{{ dash(row.transport?.etaTime) }}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">快递/携带</div>
|
||||
<div class="data-value">{{ sampleTransportExtra(row.transport) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sample-card__section sample-card__section--reception">
|
||||
@@ -596,8 +601,9 @@
|
||||
listRow?: ResearchDonationRow | null
|
||||
/** 展示审核记录 Tab(科研捐赠审核页) */
|
||||
showAuditHistory?: boolean
|
||||
highlightSampleCode?: string
|
||||
}>(),
|
||||
{ showAuditHistory: false }
|
||||
{ showAuditHistory: false, highlightSampleCode: '' }
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -953,6 +959,13 @@
|
||||
bundle.value = data as Record<string, any>
|
||||
await loadLookups(data as Record<string, any>)
|
||||
await loadAuditRecords()
|
||||
if (props.highlightSampleCode) {
|
||||
nextTick(() => {
|
||||
document
|
||||
.getElementById(`view-sample-${props.highlightSampleCode}`)
|
||||
?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -982,6 +995,19 @@
|
||||
if (visible && id) loadDetail()
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.highlightSampleCode, bundle.value] as const,
|
||||
([visible, code]) => {
|
||||
if (!visible || !code || !bundle.value) return
|
||||
nextTick(() => {
|
||||
document.getElementById(`view-sample-${code}`)?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -1311,6 +1337,11 @@
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
&.is-highlight {
|
||||
border-color: var(--primary, #10b981);
|
||||
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.18);
|
||||
}
|
||||
}
|
||||
|
||||
.sample-card__header {
|
||||
|
||||
Reference in new issue
Block a user