Files
zkxw/web-admin/src/views/quality-inspection/qc-work-order/useQcWorkOrderViewDialog.ts
T
2026-06-15 11:43:16 +08:00

499 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}