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
|
<el-button
|
||||||
class="btn-ghost"
|
class="btn-ghost"
|
||||||
:disabled="!selectedSamples.length"
|
: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>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -86,25 +95,17 @@
|
|||||||
<span v-else>—</span>
|
<span v-else>—</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="运输类型" min-width="100" show-overflow-tooltip>
|
<el-table-column label="运输方式" min-width="120" show-overflow-tooltip>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">{{ freightModeLabel(row) }}</template>
|
||||||
{{
|
|
||||||
row.transport?.transportType
|
|
||||||
? transportTypeLabel(String(row.transport.transportType))
|
|
||||||
: '—'
|
|
||||||
}}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="目的站点" min-width="120" show-overflow-tooltip>
|
<el-table-column
|
||||||
<template #default="{ row }">{{
|
label="运费(元)"
|
||||||
siteName(String(row.transport?.destSiteId || ''), row.transport)
|
min-width="120"
|
||||||
}}</template>
|
align="right"
|
||||||
</el-table-column>
|
header-align="center"
|
||||||
<el-table-column label="预计到达" min-width="150" show-overflow-tooltip>
|
show-overflow-tooltip
|
||||||
<template #default="{ row }">{{ row.transport?.etaTime || '—' }}</template>
|
>
|
||||||
</el-table-column>
|
<template #default="{ row }">{{ freightAmountText(row) }}</template>
|
||||||
<el-table-column label="快递/携带" min-width="160" show-overflow-tooltip>
|
|
||||||
<template #default="{ row }">{{ sampleTransportExtra(row.transport) }}</template>
|
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
label="操作"
|
label="操作"
|
||||||
@@ -263,11 +264,11 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 配置运输信息 -->
|
<!-- 配置运费 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="batchTransportVisible"
|
v-model="freightDialogVisible"
|
||||||
title="配置运输信息"
|
title="配置运费"
|
||||||
width="1200px"
|
width="720px"
|
||||||
class="dialog-form-layout"
|
class="dialog-form-layout"
|
||||||
align-center
|
align-center
|
||||||
modal-class="protocol-dialog-mask"
|
modal-class="protocol-dialog-mask"
|
||||||
@@ -276,109 +277,72 @@
|
|||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
>
|
>
|
||||||
<p class="sample-transport-hint batch-transport-dialog-hint">
|
<p class="sample-transport-hint batch-transport-dialog-hint">
|
||||||
将以下运输信息应用到已选中的 {{ selectedSamples.length }} 个样本。
|
将运费应用到已选中的 {{ selectedSamples.length }} 个样本(可跨协议共用一笔)。
|
||||||
</p>
|
</p>
|
||||||
<el-form label-position="top" class="biz-dialog-form">
|
<el-radio-group v-model="freightMode" class="freight-mode-radio" style="margin-bottom: 12px">
|
||||||
<div
|
<el-radio-button value="create">新建运费</el-radio-button>
|
||||||
class="sample-transport-form-row"
|
<el-radio-button value="link">关联已有</el-radio-button>
|
||||||
:class="{
|
</el-radio-group>
|
||||||
'sample-transport-form-row--ryxd': batchTransport.transportType === 'ryxd'
|
<el-form v-if="freightMode === 'create'" label-position="top" class="biz-dialog-form">
|
||||||
}"
|
<div class="form-grid-2">
|
||||||
>
|
<el-form-item label="运输方式">
|
||||||
<el-form-item label="运输类型">
|
|
||||||
<el-select
|
<el-select
|
||||||
v-model="batchTransport.transportType"
|
v-model="freightForm.transportMode"
|
||||||
placeholder="请选择"
|
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="输入站点检索"
|
|
||||||
filterable
|
filterable
|
||||||
remote
|
allow-create
|
||||||
reserve-keyword
|
default-first-option
|
||||||
clearable
|
clearable
|
||||||
:loading="siteOptionsLoading"
|
|
||||||
:remote-method="handleRemoteSearchSite"
|
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@visible-change="onSiteSelectVisible"
|
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="site in siteOptions"
|
v-for="opt in transportModeOptions"
|
||||||
:key="site.id"
|
:key="opt.value"
|
||||||
:label="site.siteName"
|
:label="opt.label"
|
||||||
:value="site.id"
|
:value="opt.value"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="预计到达时间">
|
<el-form-item label="运费(元)">
|
||||||
<el-date-picker
|
<AmountInput v-model="freightForm.freightAmount" :min="0" placeholder="请输入运费" />
|
||||||
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>
|
</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>
|
</div>
|
||||||
</el-form>
|
</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>
|
<template #footer>
|
||||||
<el-button class="btn-ghost" @click="batchTransportVisible = false">
|
<el-button class="btn-ghost" @click="freightDialogVisible = false">
|
||||||
<el-icon><Close /></el-icon>
|
<el-icon><Close /></el-icon>
|
||||||
取消
|
取消
|
||||||
</el-button>
|
</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-icon><Check /></el-icon>
|
||||||
确定应用
|
确定应用
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -388,21 +352,28 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 AmountInput from '@/components/common/amount-input/index.vue'
|
||||||
import SampleCodeWithSpecialDisplay from '@/components/business/sample-code-with-special-display/index.vue'
|
import SampleCodeWithSpecialDisplay from '@/components/business/sample-code-with-special-display/index.vue'
|
||||||
import {
|
import { sampleStatusLabel, sampleStatusTagType } from '@/utils/status-tag-registry'
|
||||||
sampleStatusLabel,
|
|
||||||
sampleStatusTagType,
|
|
||||||
transportTypeLabel
|
|
||||||
} from '@/utils/status-tag-registry'
|
|
||||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
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' })
|
defineOptions({ name: 'SampleTransportFormSection' })
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
samples: Array<Record<string, any>>
|
samples: Array<Record<string, any>>
|
||||||
|
sampleSource: 'cell_storage' | 'research_donation'
|
||||||
|
protocolId?: string
|
||||||
sampleTypeOptions: Array<{
|
sampleTypeOptions: Array<{
|
||||||
id: string
|
id: string
|
||||||
typeName: string
|
typeName: string
|
||||||
@@ -413,8 +384,8 @@
|
|||||||
}>
|
}>
|
||||||
sampleCodeOptions: Api.BizSampleCode.SampleCodeItem[]
|
sampleCodeOptions: Api.BizSampleCode.SampleCodeItem[]
|
||||||
unitOptions: Array<{ id: string; unitName: string }>
|
unitOptions: Array<{ id: string; unitName: string }>
|
||||||
siteOptions: Api.Site.SiteItem[]
|
siteOptions?: Api.Site.SiteItem[]
|
||||||
expressCompanyOptions: Api.ExpressCompany.ExpressCompanyItem[]
|
expressCompanyOptions?: Api.ExpressCompany.ExpressCompanyItem[]
|
||||||
showPrice?: boolean
|
showPrice?: boolean
|
||||||
hideCollectDetailFields?: boolean
|
hideCollectDetailFields?: boolean
|
||||||
/** 添加/编辑样本弹窗内隐藏采集量、单位(不影响列表列与存储年限) */
|
/** 添加/编辑样本弹窗内隐藏采集量、单位(不影响列表列与存储年限) */
|
||||||
@@ -429,6 +400,7 @@
|
|||||||
expressCompanyOptionsLoading?: boolean
|
expressCompanyOptionsLoading?: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
|
protocolId: '',
|
||||||
showPrice: false,
|
showPrice: false,
|
||||||
hideCollectDetailFields: false,
|
hideCollectDetailFields: false,
|
||||||
hideDialogCollectAmountUnit: false,
|
hideDialogCollectAmountUnit: false,
|
||||||
@@ -452,70 +424,73 @@
|
|||||||
(e: 'search-express-company', query: string): void
|
(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 sampleTableRef = ref()
|
||||||
const selectedSamples = ref<Array<Record<string, any>>>([])
|
const selectedSamples = ref<Array<Record<string, any>>>([])
|
||||||
let sampleRowSeq = 0
|
let sampleRowSeq = 0
|
||||||
|
|
||||||
const batchTransport = reactive<SampleTransportForm>(createEmptyTransport())
|
const freightDialogVisible = ref(false)
|
||||||
const batchTransportVisible = 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 sampleDialogVisible = ref(false)
|
||||||
const sampleDialogMode = ref<'add' | 'edit'>('add')
|
const sampleDialogMode = ref<'add' | 'edit'>('add')
|
||||||
const sampleDialogIndex = ref(-1)
|
const sampleDialogIndex = ref(-1)
|
||||||
const sampleDraft = reactive<Record<string, any>>({})
|
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 {
|
function fmtMoney(v: unknown): string {
|
||||||
if (v == null || v === '') return '—'
|
if (v == null || v === '') return '—'
|
||||||
return '¥' + formatMoneyDisplay(v)
|
return '¥' + formatMoneyDisplay(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEmptyTransport(): SampleTransportForm {
|
function freightModeLabel(row: Record<string, any>) {
|
||||||
return {
|
return (
|
||||||
transportType: '',
|
row.freight?.transportModeLabel ||
|
||||||
expressCompanyId: '',
|
row.freightDraft?.transportMode ||
|
||||||
waybillNo: '',
|
row.freight?.transportMode ||
|
||||||
destSiteId: '',
|
'—'
|
||||||
expressFee: null,
|
)
|
||||||
etaTime: '',
|
}
|
||||||
carrierName: '',
|
|
||||||
carrierPhone: ''
|
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) {
|
function normalizeSampleRow(raw: Record<string, any>, _index: number) {
|
||||||
const transportRaw = raw.transport && typeof raw.transport === 'object' ? raw.transport : {}
|
|
||||||
const statusRaw = raw.sampleStatus
|
const statusRaw = raw.sampleStatus
|
||||||
const sampleStatus =
|
const sampleStatus =
|
||||||
statusRaw != null && String(statusRaw).trim() !== '' ? String(statusRaw).trim() : ''
|
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 {
|
return {
|
||||||
...raw,
|
...raw,
|
||||||
sampleStatus,
|
sampleStatus,
|
||||||
isSpecial: !!raw.isSpecial,
|
isSpecial: !!raw.isSpecial,
|
||||||
specialRemark: String(raw.specialRemark || '').trim(),
|
specialRemark: String(raw.specialRemark || '').trim(),
|
||||||
_rowKey: raw.id || raw._rowKey || `new-${++sampleRowSeq}`,
|
_rowKey: raw.id || raw._rowKey || `new-${++sampleRowSeq}`,
|
||||||
transport: {
|
freight,
|
||||||
...createEmptyTransport(),
|
freightDraft,
|
||||||
transportType: transportRaw.transportType || '',
|
freightId: freight?.id || raw.freightId || ''
|
||||||
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 || ''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,41 +533,6 @@
|
|||||||
return name
|
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) {
|
function sampleCodeOptionLabel(sc: Api.BizSampleCode.SampleCodeItem) {
|
||||||
if (sc.usedInProtocol) return `${sc.sampleCode}(已占用)`
|
if (sc.usedInProtocol) return `${sc.sampleCode}(已占用)`
|
||||||
if (sc.issued) return `${sc.sampleCode}(已下发)`
|
if (sc.issued) return `${sc.sampleCode}(已下发)`
|
||||||
@@ -638,22 +578,6 @@
|
|||||||
if (visible) emit('search-unit', '')
|
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() {
|
function refreshSampleDialogOptions() {
|
||||||
emit('search-sample-type', '')
|
emit('search-sample-type', '')
|
||||||
if (!props.autoGenerateSampleCode) {
|
if (!props.autoGenerateSampleCode) {
|
||||||
@@ -662,9 +586,68 @@
|
|||||||
emit('search-unit', '')
|
emit('search-unit', '')
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshBatchTransportOptions() {
|
async function loadTransportModeOptions() {
|
||||||
emit('search-site', '')
|
try {
|
||||||
emit('search-express-company', '')
|
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>) {
|
function onSampleCodeChange(row: Record<string, any>) {
|
||||||
@@ -709,35 +692,138 @@
|
|||||||
selectedSamples.value = rows
|
selectedSamples.value = rows
|
||||||
}
|
}
|
||||||
|
|
||||||
function openBatchTransportDialog() {
|
async function openFreightDialog() {
|
||||||
if (!selectedSamples.value.length) {
|
if (!selectedSamples.value.length) {
|
||||||
ElMessage.warning('请先勾选需要应用运输信息的样本')
|
ElMessage.warning('请先勾选需要配置运费的样本')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Object.assign(batchTransport, createEmptyTransport())
|
freightMode.value = 'create'
|
||||||
const firstWithTransport = selectedSamples.value.find((row) => row.transport?.destSiteId)
|
freightForm.transportMode = ''
|
||||||
if (firstWithTransport?.transport) {
|
freightForm.freightAmount = null
|
||||||
Object.assign(batchTransport, { ...firstWithTransport.transport })
|
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
|
freightDialogVisible.value = true
|
||||||
refreshBatchTransportOptions()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmBatchTransportApply() {
|
function onExistingFreightSelect(row: SampleFreight | null) {
|
||||||
if (!batchTransport.transportType) {
|
selectedExistingFreight.value = row
|
||||||
ElMessage.warning('请选择运输类型')
|
}
|
||||||
|
|
||||||
|
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
|
return
|
||||||
}
|
}
|
||||||
const payload = { ...batchTransport }
|
try {
|
||||||
const selectedSet = new Set(selectedSamples.value)
|
await ElMessageBox.confirm('确定取消所选样本的运费关联吗?', '取消运费关联', {
|
||||||
const next = props.samples.map((row) => {
|
type: 'warning',
|
||||||
if (!selectedSet.has(row)) return row
|
confirmButtonText: '确定',
|
||||||
const transport = { ...(row.transport || createEmptyTransport()), ...payload }
|
cancelButtonText: '取消'
|
||||||
return { ...row, transport }
|
})
|
||||||
})
|
} catch {
|
||||||
updateSamples(next)
|
return
|
||||||
batchTransportVisible.value = false
|
}
|
||||||
ElMessage.success(`已为 ${selectedSamples.value.length} 个样本应用运输信息`)
|
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) {
|
function onSpecialSampleToggle(checked: boolean | string | number) {
|
||||||
|
|||||||
@@ -469,6 +469,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<SampleTransportFormSection
|
<SampleTransportFormSection
|
||||||
v-model:samples="formData.samples"
|
v-model:samples="formData.samples"
|
||||||
|
sample-source="cell_storage"
|
||||||
|
:protocol-id="formData.id || ''"
|
||||||
hide-dialog-collect-amount-unit
|
hide-dialog-collect-amount-unit
|
||||||
:sample-type-options="sampleTypeOptions"
|
:sample-type-options="sampleTypeOptions"
|
||||||
:sample-type-options-loading="sampleTypeLoading"
|
:sample-type-options-loading="sampleTypeLoading"
|
||||||
@@ -476,17 +478,11 @@
|
|||||||
:sample-code-options-loading="sampleCodeLoading"
|
:sample-code-options-loading="sampleCodeLoading"
|
||||||
:unit-options="unitOptions"
|
:unit-options="unitOptions"
|
||||||
:unit-options-loading="unitLoading"
|
:unit-options-loading="unitLoading"
|
||||||
:site-options="siteOptions"
|
|
||||||
:site-options-loading="siteLoading"
|
|
||||||
:express-company-options="expressCompanyOptions"
|
|
||||||
:express-company-options-loading="expressCompanyLoading"
|
|
||||||
:show-price="true"
|
:show-price="true"
|
||||||
@price-change="onSamplePriceChange"
|
@price-change="onSamplePriceChange"
|
||||||
@search-sample-code="remoteSearchSampleCodes"
|
@search-sample-code="remoteSearchSampleCodes"
|
||||||
@search-sample-type="remoteSearchSampleTypes"
|
@search-sample-type="remoteSearchSampleTypes"
|
||||||
@search-unit="remoteSearchUnits"
|
@search-unit="remoteSearchUnits"
|
||||||
@search-site="remoteSearchSites"
|
|
||||||
@search-express-company="remoteSearchExpressCompanies"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -850,7 +846,8 @@
|
|||||||
class="detail-badge"
|
class="detail-badge"
|
||||||
:class="{
|
:class="{
|
||||||
'badge-green': detail.payStatus === 'yjf',
|
'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'
|
'badge-gray': detail.payStatus === 'tfqrz' || detail.payStatus === 'ytf'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
@@ -908,6 +905,7 @@
|
|||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="detail.transferTime"
|
v-model="detail.transferTime"
|
||||||
type="datetime"
|
type="datetime"
|
||||||
|
format="YYYY-MM-DD HH:mm"
|
||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
placeholder="选择时间"
|
placeholder="选择时间"
|
||||||
clearable
|
clearable
|
||||||
@@ -917,20 +915,11 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="缴费状态">
|
<el-form-item label="缴费状态">
|
||||||
<el-tag
|
<el-tag
|
||||||
v-if="isPayDetailLocked(detail)"
|
:type="payStatusTagType(String(detail.payStatus || 'dfj'))"
|
||||||
:type="payStatusTagType(String(detail.payStatus))"
|
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
{{ payStatusLabel(String(detail.payStatus)) }}
|
{{ payStatusLabel(String(detail.payStatus || 'dfj')) }}
|
||||||
</el-tag>
|
</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>
|
||||||
<el-form-item label="应缴费时间">
|
<el-form-item label="应缴费时间">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
@@ -2367,13 +2356,14 @@
|
|||||||
sampleCollectTime: formData.sampleCollectTime || undefined,
|
sampleCollectTime: formData.sampleCollectTime || undefined,
|
||||||
maternalBloodCollectTime: formData.maternalBloodCollectTime || undefined
|
maternalBloodCollectTime: formData.maternalBloodCollectTime || undefined
|
||||||
},
|
},
|
||||||
samples: formData.samples.map(({ _rowKey, transport, ...s }) => ({
|
samples: formData.samples.map(({ _rowKey, transport, freight, freightDraft, ...s }) => ({
|
||||||
...s,
|
...s,
|
||||||
sampleStatus:
|
sampleStatus:
|
||||||
s.sampleStatus != null && String(s.sampleStatus).trim() !== ''
|
s.sampleStatus != null && String(s.sampleStatus).trim() !== ''
|
||||||
? String(s.sampleStatus).trim()
|
? String(s.sampleStatus).trim()
|
||||||
: null,
|
: null,
|
||||||
transport: transport?.transportType ? { ...transport } : undefined
|
freightId: freight?.id || s.freightId || undefined,
|
||||||
|
freightDraft: freightDraft || undefined
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4238,6 +4228,11 @@
|
|||||||
color: #065f46;
|
color: #065f46;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.badge-primary {
|
||||||
|
background: #ecfdf5;
|
||||||
|
color: #047857;
|
||||||
|
}
|
||||||
|
|
||||||
&.badge-amber {
|
&.badge-amber {
|
||||||
background: #fffbeb;
|
background: #fffbeb;
|
||||||
color: #d97706;
|
color: #d97706;
|
||||||
|
|||||||
@@ -307,7 +307,16 @@
|
|||||||
>
|
>
|
||||||
<div class="section-title section-title--banner"><h3>样本及运输信息</h3></div>
|
<div class="section-title section-title--banner"><h3>样本及运输信息</h3></div>
|
||||||
<div v-if="samples.length" class="sample-cards">
|
<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__header">
|
||||||
<div class="sample-card__title">
|
<div class="sample-card__title">
|
||||||
<span class="sample-card__seq">{{ sIdx + 1 }}</span>
|
<span class="sample-card__seq">{{ sIdx + 1 }}</span>
|
||||||
@@ -355,31 +364,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="row.transport?.transportType"
|
v-if="row.freight?.id || row.freight?.transportMode"
|
||||||
class="sample-card__section sample-card__section--transport"
|
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-grid data-grid--4">
|
||||||
<div class="data-item">
|
<div class="data-item">
|
||||||
<div class="data-label">运输类型</div>
|
<div class="data-label">运输方式</div>
|
||||||
<div class="data-value">{{
|
<div class="data-value">{{
|
||||||
transportTypeLabel(String(row.transport?.transportType || ''))
|
dash(row.freight?.transportModeLabel || row.freight?.transportMode)
|
||||||
}}</div>
|
}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-item">
|
<div class="data-item">
|
||||||
<div class="data-label">目的站点</div>
|
<div class="data-label">运费(元)</div>
|
||||||
<div class="data-value">{{
|
<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>
|
</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>
|
</div>
|
||||||
<div class="sample-card__section sample-card__section--reception">
|
<div class="sample-card__section sample-card__section--reception">
|
||||||
@@ -419,6 +424,35 @@
|
|||||||
<el-empty v-else description="暂无样本" :image-size="48" />
|
<el-empty v-else description="暂无样本" :image-size="48" />
|
||||||
</div>
|
</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 id="view-section-party" class="section-card section-card--green">
|
||||||
<div class="section-title section-title--banner"><h3>客户信息</h3></div>
|
<div class="section-title section-title--banner"><h3>客户信息</h3></div>
|
||||||
@@ -926,7 +960,7 @@
|
|||||||
<div class="flow-detail-card__kv">
|
<div class="flow-detail-card__kv">
|
||||||
<span class="flow-detail-card__label">确认状态</span>
|
<span class="flow-detail-card__label">确认状态</span>
|
||||||
<span class="flow-detail-card__value"
|
<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>
|
||||||
<div v-if="detail.transferTime" class="flow-detail-card__kv">
|
<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 SampleCodeWithSpecialDisplay from '@/components/business/sample-code-with-special-display/index.vue'
|
||||||
import SampleProductionInfoCards from '@/components/business/sample-production-info-cards/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 SampleStorageReleaseInfo from '@/components/business/sample-storage-release-info/index.vue'
|
||||||
|
import ProtocolHandlePanel from '@/components/business/protocol-handle-panel/index.vue'
|
||||||
import {
|
import {
|
||||||
fetchCollectionProtocolAuditList,
|
fetchCollectionProtocolAuditList,
|
||||||
fetchCollectionProtocolDetail,
|
fetchCollectionProtocolDetail,
|
||||||
@@ -1271,8 +1306,17 @@
|
|||||||
showStorageCert?: boolean
|
showStorageCert?: boolean
|
||||||
/** 满月待办查看:客户信息下方独立展示新生儿保险信息 */
|
/** 满月待办查看:客户信息下方独立展示新生儿保险信息 */
|
||||||
showFullmoonInsurance?: 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<{
|
const emit = defineEmits<{
|
||||||
@@ -1289,6 +1333,11 @@
|
|||||||
|
|
||||||
const formTabs = computed(() => {
|
const formTabs = computed(() => {
|
||||||
const tabs: Array<{ key: string; label: string }> = [...baseFormTabs]
|
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) {
|
if (props.showFullmoonInsurance) {
|
||||||
const partyIdx = tabs.findIndex((t) => t.key === 'party')
|
const partyIdx = tabs.findIndex((t) => t.key === 'party')
|
||||||
tabs.splice(partyIdx + 1, 0, { key: 'fullmoon-insurance', label: '新生儿保险' })
|
tabs.splice(partyIdx + 1, 0, { key: 'fullmoon-insurance', label: '新生儿保险' })
|
||||||
@@ -1705,7 +1754,7 @@
|
|||||||
return {
|
return {
|
||||||
key: `ov-${i}`,
|
key: `ov-${i}`,
|
||||||
label: name,
|
label: name,
|
||||||
subLabel: '已缴费',
|
subLabel: '缴费成功',
|
||||||
dotClass: 'done',
|
dotClass: 'done',
|
||||||
dotIcon: '✓',
|
dotIcon: '✓',
|
||||||
labelClass: 'done',
|
labelClass: 'done',
|
||||||
@@ -1726,7 +1775,7 @@
|
|||||||
return {
|
return {
|
||||||
key: `ov-${i}`,
|
key: `ov-${i}`,
|
||||||
label: name,
|
label: name,
|
||||||
subLabel: '待缴费',
|
subLabel: '已缴费',
|
||||||
dotClass: 'pending',
|
dotClass: 'pending',
|
||||||
dotIcon: '○',
|
dotIcon: '○',
|
||||||
labelClass: ''
|
labelClass: ''
|
||||||
@@ -1993,6 +2042,13 @@
|
|||||||
|
|
||||||
void finishLookups(data, baseLookupsPromise)
|
void finishLookups(data, baseLookupsPromise)
|
||||||
void auditPromise
|
void auditPromise
|
||||||
|
if (props.highlightSampleCode) {
|
||||||
|
nextTick(() => {
|
||||||
|
document
|
||||||
|
.getElementById(`view-sample-${props.highlightSampleCode}`)
|
||||||
|
?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToSection(key: string) {
|
function scrollToSection(key: string) {
|
||||||
@@ -2024,6 +2080,19 @@
|
|||||||
if (visible && id) loadDetail()
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -2394,6 +2463,11 @@
|
|||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
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 {
|
.sample-card__header {
|
||||||
|
|||||||
@@ -255,6 +255,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<SampleTransportFormSection
|
<SampleTransportFormSection
|
||||||
v-model:samples="formData.samples"
|
v-model:samples="formData.samples"
|
||||||
|
sample-source="research_donation"
|
||||||
|
:protocol-id="formData.id || ''"
|
||||||
hide-collect-detail-fields
|
hide-collect-detail-fields
|
||||||
auto-generate-sample-code
|
auto-generate-sample-code
|
||||||
:reserve-sample-code="reserveDonationSampleCode"
|
:reserve-sample-code="reserveDonationSampleCode"
|
||||||
@@ -264,16 +266,10 @@
|
|||||||
:sample-code-options-loading="sampleCodeLoading"
|
:sample-code-options-loading="sampleCodeLoading"
|
||||||
:unit-options="unitOptions"
|
:unit-options="unitOptions"
|
||||||
:unit-options-loading="unitLoading"
|
:unit-options-loading="unitLoading"
|
||||||
:site-options="siteOptions"
|
|
||||||
:site-options-loading="siteLoading"
|
|
||||||
:express-company-options="expressCompanyOptions"
|
|
||||||
:express-company-options-loading="expressCompanyLoading"
|
|
||||||
:show-price="false"
|
:show-price="false"
|
||||||
@search-sample-code="remoteSearchSampleCodes"
|
@search-sample-code="remoteSearchSampleCodes"
|
||||||
@search-sample-type="remoteSearchSampleTypes"
|
@search-sample-type="remoteSearchSampleTypes"
|
||||||
@search-unit="remoteSearchUnits"
|
@search-unit="remoteSearchUnits"
|
||||||
@search-site="remoteSearchSites"
|
|
||||||
@search-express-company="remoteSearchExpressCompanies"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1411,13 +1407,14 @@
|
|||||||
maternalBloodCollectTime: formData.maternalBloodCollectTime || undefined
|
maternalBloodCollectTime: formData.maternalBloodCollectTime || undefined
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// 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,
|
...s,
|
||||||
sampleStatus:
|
sampleStatus:
|
||||||
s.sampleStatus != null && String(s.sampleStatus).trim() !== ''
|
s.sampleStatus != null && String(s.sampleStatus).trim() !== ''
|
||||||
? String(s.sampleStatus).trim()
|
? String(s.sampleStatus).trim()
|
||||||
: null,
|
: 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 id="view-section-sample" class="section-card section-card--white">
|
||||||
<div class="section-title section-title--banner"><h3>样本及运输信息</h3></div>
|
<div class="section-title section-title--banner"><h3>样本及运输信息</h3></div>
|
||||||
<div v-if="samples.length" class="sample-cards">
|
<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__header">
|
||||||
<div class="sample-card__title">
|
<div class="sample-card__title">
|
||||||
<span class="sample-card__seq">{{ sIdx + 1 }}</span>
|
<span class="sample-card__seq">{{ sIdx + 1 }}</span>
|
||||||
@@ -261,31 +270,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="row.transport?.transportType"
|
v-if="row.freight?.id || row.freight?.transportMode"
|
||||||
class="sample-card__section sample-card__section--transport"
|
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-grid data-grid--4">
|
||||||
<div class="data-item">
|
<div class="data-item">
|
||||||
<div class="data-label">运输类型</div>
|
<div class="data-label">运输方式</div>
|
||||||
<div class="data-value">{{
|
<div class="data-value">{{
|
||||||
transportTypeLabel(String(row.transport?.transportType || ''))
|
dash(row.freight?.transportModeLabel || row.freight?.transportMode)
|
||||||
}}</div>
|
}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-item">
|
<div class="data-item">
|
||||||
<div class="data-label">目的站点</div>
|
<div class="data-label">运费(元)</div>
|
||||||
<div class="data-value">{{
|
<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>
|
</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>
|
</div>
|
||||||
<div class="sample-card__section sample-card__section--reception">
|
<div class="sample-card__section sample-card__section--reception">
|
||||||
@@ -596,8 +601,9 @@
|
|||||||
listRow?: ResearchDonationRow | null
|
listRow?: ResearchDonationRow | null
|
||||||
/** 展示审核记录 Tab(科研捐赠审核页) */
|
/** 展示审核记录 Tab(科研捐赠审核页) */
|
||||||
showAuditHistory?: boolean
|
showAuditHistory?: boolean
|
||||||
|
highlightSampleCode?: string
|
||||||
}>(),
|
}>(),
|
||||||
{ showAuditHistory: false }
|
{ showAuditHistory: false, highlightSampleCode: '' }
|
||||||
)
|
)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -953,6 +959,13 @@
|
|||||||
bundle.value = data as Record<string, any>
|
bundle.value = data as Record<string, any>
|
||||||
await loadLookups(data as Record<string, any>)
|
await loadLookups(data as Record<string, any>)
|
||||||
await loadAuditRecords()
|
await loadAuditRecords()
|
||||||
|
if (props.highlightSampleCode) {
|
||||||
|
nextTick(() => {
|
||||||
|
document
|
||||||
|
.getElementById(`view-sample-${props.highlightSampleCode}`)
|
||||||
|
?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
|
})
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -982,6 +995,19 @@
|
|||||||
if (visible && id) loadDetail()
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -1311,6 +1337,11 @@
|
|||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
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 {
|
.sample-card__header {
|
||||||
|
|||||||
Reference in new issue
Block a user