499 lines
17 KiB
TypeScript
499 lines
17 KiB
TypeScript
import { ref, computed } from 'vue'
|
||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||
import { fetchQcWorkOrderDetail, type QcWorkOrder, type QcWorkOrderDetail } from '@/api/qc-work-order'
|
||
import type { QcTask } from '@/api/qc-task'
|
||
import {
|
||
fetchQcTaskResults,
|
||
fetchQcTaskMaterialUsages,
|
||
type QcTaskResultItem,
|
||
type QcTaskMaterialUsageItem
|
||
} from '@/api/qc-task-result'
|
||
import {
|
||
workOrderStatusLabel,
|
||
receptionSourceLabel,
|
||
acceptConclusionLabel,
|
||
transportTypeLabel,
|
||
taskStatusLabel,
|
||
qcTaskConclusionLabel,
|
||
qcBizTypeLabel
|
||
} from '@/utils/qc-labels'
|
||
import {
|
||
protocolStatusLabel,
|
||
protocolStatusTagType,
|
||
donationStatusLabel,
|
||
donationStatusTagType
|
||
} from '@/utils/status-tag-registry'
|
||
import type { TagType } from '@/utils/status-tag-registry'
|
||
import type { ViewField, ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||
|
||
type ViewMaterialUsageRow = {
|
||
stockInNo: string
|
||
materialName: string
|
||
batchNo: string
|
||
receiverName: string
|
||
unit: string
|
||
qty: number | undefined
|
||
remark: string
|
||
}
|
||
|
||
type QcWorkOrderTaskView = QcTask & {
|
||
results?: QcTaskResultItem[]
|
||
materialUsages?: ViewMaterialUsageRow[]
|
||
}
|
||
|
||
function protocolStatusLabelBySource(status: string, source: string): string {
|
||
return source === 'research_donation' ? donationStatusLabel(status) : protocolStatusLabel(status)
|
||
}
|
||
|
||
function protocolStatusTagTypeBySource(status: string, source: string): TagType {
|
||
return source === 'research_donation' ? donationStatusTagType(status) : protocolStatusTagType(status)
|
||
}
|
||
|
||
function materialRowsFromApi(items: QcTaskMaterialUsageItem[]): ViewMaterialUsageRow[] {
|
||
return (items || [])
|
||
.filter((u) => (u.requisitionUsageId || u.parentUsageId || '').trim())
|
||
.map((u) => ({
|
||
stockInNo: u.stockInNo || '',
|
||
materialName: u.materialName || '',
|
||
batchNo: u.batchNo || '',
|
||
receiverName: u.requisitionReceiverName || u.receiverName || '',
|
||
unit: (u.usageUnit || '').trim(),
|
||
qty: Math.abs(Number(u.changeQty) || 0) || undefined,
|
||
remark: u.remark || ''
|
||
}))
|
||
}
|
||
|
||
export function useQcWorkOrderViewDialog() {
|
||
const viewVisible = ref(false)
|
||
const viewLoading = ref(false)
|
||
const detail = ref<QcWorkOrderDetail | null>(null)
|
||
const viewTasks = ref<QcWorkOrderTaskView[]>([])
|
||
const outboundViewVisible = ref(false)
|
||
const outboundViewId = ref('')
|
||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||
|
||
const viewTasksWithAttachments = computed(() =>
|
||
viewTasks.value.filter((t) => (t.attachmentFiles?.length || 0) > 0)
|
||
)
|
||
const viewLabTasks = computed(() => viewTasks.value.filter((t) => t.taskType === 'sysz'))
|
||
|
||
function taskAttachmentSlotName(taskId: string) {
|
||
return `task-att-${taskId}`
|
||
}
|
||
|
||
function taskMaterialSlotName(taskId: string) {
|
||
return `task-mat-${taskId}`
|
||
}
|
||
|
||
const viewReceptionAttachmentFiles = computed(() => {
|
||
const r = detail.value?.reception
|
||
if (!r) return []
|
||
if (Array.isArray(r.receiveAttachmentFiles) && r.receiveAttachmentFiles.length) {
|
||
return r.receiveAttachmentFiles as Array<Record<string, unknown>>
|
||
}
|
||
const urls = String(r.receiveAttachments || '')
|
||
if (!urls.trim()) return []
|
||
return urls
|
||
.split(',')
|
||
.map((u, i) => {
|
||
const url = u.trim()
|
||
if (!url) return null
|
||
const name = url.includes('/') ? url.split('/').pop() || `附件${i + 1}` : url
|
||
return { id: `url_${i}`, fileUrl: url, fileName: name }
|
||
})
|
||
.filter(Boolean) as Array<Record<string, unknown>>
|
||
})
|
||
|
||
function attachmentFullUrl(url: string) {
|
||
const u = String(url || '').trim()
|
||
if (!u) return ''
|
||
if (/^https?:\/\//i.test(u)) return u
|
||
return minioBaseUrl ? `${minioBaseUrl}/${u.replace(/^\//, '')}` : u
|
||
}
|
||
|
||
function handleReceptionAttachmentPreview(file: Record<string, unknown>) {
|
||
const url = attachmentFullUrl(String(file.fileUrl || ''))
|
||
if (url) window.open(url, '_blank')
|
||
}
|
||
|
||
function handleReceptionAttachmentDownload(file: Record<string, unknown>) {
|
||
downloadAttachmentFile(file, { fullFileUrl: attachmentFullUrl(String(file.fileUrl || '')) })
|
||
}
|
||
|
||
function handleTaskAttachmentPreview(file: Record<string, unknown>) {
|
||
const url = attachmentFullUrl(String(file.fileUrl || ''))
|
||
if (url) window.open(url, '_blank')
|
||
}
|
||
|
||
function handleTaskAttachmentDownload(file: Record<string, unknown>) {
|
||
downloadAttachmentFile(file, { fullFileUrl: attachmentFullUrl(String(file.fileUrl || '')) })
|
||
}
|
||
|
||
function conclusionTagType(conclusion?: string) {
|
||
if (conclusion === 'tg') return 'success' as const
|
||
if (conclusion === 'btg') return 'danger' as const
|
||
return 'info' as const
|
||
}
|
||
|
||
function taskStatusTagType(status?: string) {
|
||
if (status === 'ywc') return 'success' as const
|
||
if (status === 'jxz') return 'warning' as const
|
||
return 'info' as const
|
||
}
|
||
|
||
function formatConfiguredLabItems(d: QcWorkOrderDetail) {
|
||
return d.labItems?.map((li) => `${li.itemName || ''}(${li.itemCode || ''})`).join('、') || '无'
|
||
}
|
||
|
||
function formatConfiguredThirdItems(d: QcWorkOrderDetail) {
|
||
return (
|
||
d.thirdPartyItems
|
||
?.map(
|
||
(li) =>
|
||
`${li.companyName || ''} — ${li.itemName || ''}(${li.feeAmount != null ? '¥' + formatMoneyDisplay(li.feeAmount) : '—'})`
|
||
)
|
||
.join('、') || '无'
|
||
)
|
||
}
|
||
|
||
function buildConfiguredOnlySection(
|
||
title: string,
|
||
configuredLabel: string,
|
||
configuredValue: string,
|
||
attachmentLabel: string
|
||
): ViewSection {
|
||
return {
|
||
title,
|
||
fields: [
|
||
{ label: configuredLabel, value: configuredValue, span: true },
|
||
{ label: '检测结论', value: '—' },
|
||
{ label: '检测参数', value: '—' },
|
||
{ label: '备注', value: '—' },
|
||
{ label: attachmentLabel, value: '—' }
|
||
]
|
||
}
|
||
}
|
||
|
||
function buildTaskTypeSections(
|
||
tasks: QcWorkOrderTaskView[],
|
||
taskType: 'sysz' | 'dsfjc',
|
||
sectionTitle: string
|
||
): ViewSection[] {
|
||
const filtered = tasks.filter((t) => t.taskType === taskType)
|
||
if (!filtered.length) return []
|
||
|
||
return filtered.flatMap((task, index) => {
|
||
const title = filtered.length > 1 ? `${sectionTitle}(${index + 1})` : sectionTitle
|
||
const taskInfoFields: ViewField[] = [
|
||
{ label: '检测项目', value: task.itemName || '—' },
|
||
{ label: '项目编码', value: task.itemCode || '—' },
|
||
{
|
||
label: '任务状态',
|
||
value: taskStatusLabel(task.taskStatus || ''),
|
||
tagType: taskStatusTagType(task.taskStatus)
|
||
},
|
||
{
|
||
label: '检测结论',
|
||
value: task.conclusionLabel || qcTaskConclusionLabel(task.conclusion || '') || '—',
|
||
tagType: conclusionTagType(task.conclusion)
|
||
},
|
||
{ label: '任务号', value: task.taskNo || '—' },
|
||
{ label: '完成时间', value: task.completedTime || '—' }
|
||
]
|
||
if (taskType === 'dsfjc') {
|
||
taskInfoFields.splice(2, 0, { label: '第三方公司', value: task.companyName || '—' })
|
||
if (task.feeAmount != null) {
|
||
taskInfoFields.push({ label: '费用(元)', value: String(task.feeAmount) })
|
||
}
|
||
}
|
||
const paramFields: ViewField[] = (task.results || []).map((item) => ({
|
||
label: item.paramName + (item.paramUnit ? `(${item.paramUnit})` : ''),
|
||
value: item.paramValue || '—'
|
||
}))
|
||
const remarkField: ViewField = { label: '备注', value: task.remark || '—', span: true }
|
||
|
||
const section: ViewSection =
|
||
taskType === 'sysz'
|
||
? {
|
||
title,
|
||
fields: [],
|
||
fieldGroups: [
|
||
{ fields: taskInfoFields },
|
||
{
|
||
title: '检测参数',
|
||
titleStyle: 'field',
|
||
fields: paramFields,
|
||
emptyHint: '—'
|
||
},
|
||
{ fields: [remarkField] }
|
||
]
|
||
}
|
||
: {
|
||
title,
|
||
fields: [
|
||
...taskInfoFields,
|
||
...(paramFields.length ? paramFields : [{ label: '检测参数', value: '—' as const }]),
|
||
remarkField
|
||
]
|
||
}
|
||
if (task.attachmentFiles?.length) {
|
||
section.slot = taskAttachmentSlotName(task.id)
|
||
section.slotTitle = taskType === 'dsfjc' ? '检测材料' : '检测附件'
|
||
section.slotTitleStyle = 'field'
|
||
} else if (taskType === 'dsfjc') {
|
||
section.fields!.push({ label: '检测材料', value: '—' })
|
||
} else if (taskType === 'sysz') {
|
||
section.fieldGroups?.push({ fields: [{ label: '检测附件', value: '—' }] })
|
||
} else {
|
||
section.fields!.push({ label: '检测附件', value: '—' })
|
||
}
|
||
const sections: ViewSection[] = [section]
|
||
if (taskType === 'sysz') {
|
||
sections.push({
|
||
title: '',
|
||
fields: [],
|
||
slotTitle: '物料使用',
|
||
slotTitleStyle: 'field',
|
||
slot: taskMaterialSlotName(task.id)
|
||
})
|
||
}
|
||
return sections
|
||
})
|
||
}
|
||
|
||
function formatMetric(v: unknown) {
|
||
if (v === null || v === undefined || v === '') return '—'
|
||
return String(v)
|
||
}
|
||
|
||
function formatCheckResults(items?: Array<{ itemName?: string; checked?: boolean }>) {
|
||
if (!items?.length) return '—'
|
||
return items.map((x) => `${x.itemName || ''}${x.checked ? '(✓)' : '(×)'}`).join('、')
|
||
}
|
||
|
||
const viewSections = computed<ViewSection[]>(() => {
|
||
if (!detail.value) return []
|
||
const d = detail.value
|
||
const isTemp = d.workOrderType === 'temporary'
|
||
const r = d.reception || {}
|
||
const transportFields: ViewSection['fields'] = [
|
||
{ label: '运输方式', value: transportTypeLabel(String(r.transportType || '')) }
|
||
]
|
||
if (r.transportType === 'kdys') {
|
||
transportFields.push(
|
||
{ label: '快递公司', value: r.expressCompanyName || '—' },
|
||
{ label: '快递单号', value: r.waybillNo || '—' }
|
||
)
|
||
} else if (r.transportType === 'ryxd') {
|
||
transportFields.push(
|
||
{ label: '携带人', value: r.carrierName || '—' },
|
||
{ label: '携带人电话', value: r.carrierPhone || '—' }
|
||
)
|
||
}
|
||
if (r.etaTime) {
|
||
transportFields.push({ label: '预计到达', value: r.etaTime })
|
||
}
|
||
const transportSection =
|
||
transportFields.length > 1 || r.transportType
|
||
? [{ title: '运输信息', fields: transportFields }]
|
||
: []
|
||
const paymentSection =
|
||
!isTemp && (d.receptionSource || 'cell_storage') === 'cell_storage'
|
||
? [{ title: '协议缴费信息', fields: [], slot: 'protocol-payment-details' }]
|
||
: []
|
||
|
||
return [
|
||
{
|
||
title: '基本信息',
|
||
fields: [
|
||
{ label: '工单号', value: d.workOrderNo },
|
||
{
|
||
label: '工单类型',
|
||
value: isTemp ? '临时' : '入库',
|
||
tagType: isTemp ? 'warning' : 'success'
|
||
},
|
||
...(isTemp
|
||
? [
|
||
{
|
||
label: '业务类型',
|
||
value: d.qcBizTypeLabel || qcBizTypeLabel('temporary'),
|
||
tagType: 'warning' as const
|
||
},
|
||
{ label: '产品编码', value: d.cellProductCode || '—' },
|
||
{ label: '产品名称', value: d.cellProductName || '—' },
|
||
{ label: '产品类型', value: d.cellProductTypeName || '—' },
|
||
{ label: '代次', value: d.cellProductPassage || '—' },
|
||
{ label: '规格', value: d.cellProductSpecName || '—' }
|
||
]
|
||
: []),
|
||
...(isTemp
|
||
? []
|
||
: [
|
||
{
|
||
label: '协议状态',
|
||
value: protocolStatusLabelBySource(
|
||
String(d.protocolStatus || ''),
|
||
d.receptionSource || 'cell_storage'
|
||
),
|
||
tagType: protocolStatusTagTypeBySource(
|
||
String(d.protocolStatus || ''),
|
||
d.receptionSource || 'cell_storage'
|
||
)
|
||
},
|
||
{ label: '样本编码', value: d.sampleCode },
|
||
{
|
||
label: '来源类型',
|
||
value: d.receptionSourceLabel || receptionSourceLabel(d.receptionSource || ''),
|
||
tagType:
|
||
d.receptionSource === 'research_donation'
|
||
? ('warning' as const)
|
||
: ('success' as const)
|
||
},
|
||
{ label: '样本类型', value: r.sampleTypeName || d.sampleTypeName || '—' }
|
||
]),
|
||
{
|
||
label: '状态',
|
||
value: workOrderStatusLabel(d.workOrderStatus),
|
||
tagType:
|
||
d.workOrderStatus === 'ywc'
|
||
? 'success'
|
||
: d.workOrderStatus === 'ypz'
|
||
? 'warning'
|
||
: 'info'
|
||
},
|
||
{ label: '备注', value: d.remark || '—', span: true },
|
||
{ label: '创建时间', value: d.createTime || '—' }
|
||
]
|
||
},
|
||
...(isTemp ? [] : transportSection),
|
||
...(isTemp ? [] : paymentSection),
|
||
...(isTemp
|
||
? []
|
||
: [
|
||
{
|
||
title: '样本接收',
|
||
slot: 'reception-extra',
|
||
fields: [
|
||
{ label: '接收时间', value: r.receptionTime || d.receptionTime || '—' },
|
||
{ label: '接收经手人', value: r.handlerName || d.handlerName || '—' },
|
||
...(String(r.acceptConclusion || '') === 'js'
|
||
? [
|
||
{
|
||
label: '实验室入库编码',
|
||
value: r.labStorageCode || d.labStorageCode || '—'
|
||
}
|
||
]
|
||
: []),
|
||
{
|
||
label: '接收温度(℃)',
|
||
value: r.receiveTemperature ? `${r.receiveTemperature}℃` : '—'
|
||
},
|
||
{ label: '体积(ml)', value: formatMetric(r.volumeMl) },
|
||
{ label: '净重(g)', value: formatMetric(r.netWeightG) },
|
||
{
|
||
label: '结论',
|
||
value:
|
||
r.acceptConclusionLabel ||
|
||
acceptConclusionLabel(String(r.acceptConclusion || '')),
|
||
tagType:
|
||
r.acceptConclusion === 'js'
|
||
? 'success'
|
||
: r.acceptConclusion === 'jj'
|
||
? 'danger'
|
||
: 'info'
|
||
},
|
||
{ label: '检查项', value: formatCheckResults(r.checkResults), span: true },
|
||
{ label: '备注', value: r.remark || '—', span: true }
|
||
]
|
||
}
|
||
]),
|
||
...(viewTasks.value.some((t) => t.taskType === 'sysz')
|
||
? buildTaskTypeSections(viewTasks.value, 'sysz', '实验室项目')
|
||
: d.hasLab
|
||
? [
|
||
buildConfiguredOnlySection(
|
||
'实验室项目',
|
||
'已配置项目',
|
||
formatConfiguredLabItems(d),
|
||
'检测附件'
|
||
)
|
||
]
|
||
: []),
|
||
...(viewTasks.value.some((t) => t.taskType === 'dsfjc')
|
||
? buildTaskTypeSections(viewTasks.value, 'dsfjc', '第三方项目')
|
||
: d.hasThirdParty
|
||
? [
|
||
buildConfiguredOnlySection(
|
||
'第三方项目',
|
||
'已配置项目',
|
||
formatConfiguredThirdItems(d),
|
||
'检测材料'
|
||
)
|
||
]
|
||
: [])
|
||
]
|
||
})
|
||
|
||
async function openInboundOrTempView(row: QcWorkOrder) {
|
||
viewVisible.value = true
|
||
viewLoading.value = true
|
||
viewTasks.value = []
|
||
try {
|
||
const d = await fetchQcWorkOrderDetail(row.id)
|
||
detail.value = d
|
||
const rawTasks = (d.tasks || []) as QcWorkOrderTaskView[]
|
||
viewTasks.value = await Promise.all(
|
||
rawTasks.map(async (task) => {
|
||
const base: QcWorkOrderTaskView = { ...task, results: [], materialUsages: [] }
|
||
if (task.taskStatus !== 'ywc') return base
|
||
try {
|
||
base.results = (await fetchQcTaskResults(task.id)) || []
|
||
} catch {
|
||
base.results = []
|
||
}
|
||
if (task.taskType === 'sysz') {
|
||
try {
|
||
base.materialUsages = materialRowsFromApi(await fetchQcTaskMaterialUsages(task.id))
|
||
} catch {
|
||
base.materialUsages = []
|
||
}
|
||
}
|
||
return base
|
||
})
|
||
)
|
||
} finally {
|
||
viewLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function openView(row: QcWorkOrder) {
|
||
const woType = row.workOrderType || row.qcBizType || 'inbound'
|
||
if (woType === 'outbound') {
|
||
outboundViewId.value = row.id
|
||
outboundViewVisible.value = true
|
||
return
|
||
}
|
||
await openInboundOrTempView(row)
|
||
}
|
||
|
||
return {
|
||
viewVisible,
|
||
viewLoading,
|
||
detail,
|
||
viewSections,
|
||
viewTasksWithAttachments,
|
||
viewLabTasks,
|
||
viewReceptionAttachmentFiles,
|
||
outboundViewVisible,
|
||
outboundViewId,
|
||
taskAttachmentSlotName,
|
||
taskMaterialSlotName,
|
||
handleReceptionAttachmentPreview,
|
||
handleReceptionAttachmentDownload,
|
||
handleTaskAttachmentPreview,
|
||
handleTaskAttachmentDownload,
|
||
openView
|
||
}
|
||
}
|