This commit is contained in:
zlwu committed 2026-06-29 09:21:09 +08:00
1 parent 4daf990fab
commit 1ca2668112
219 files changed
+29817 -20945

No files matched your search

@@ -77,7 +77,7 @@
>
<template #default="{ row }">{{ row.expectedCellCount ?? '—' }}</template>
</el-table-column>
<el-table-column label="活性" min-width="80" align="center" header-align="center">
<el-table-column label="活率(%)" min-width="80" align="center" header-align="center">
<template #default="{ row }">{{
row.viability != null ? row.viability : '—'
}}</template>
@@ -76,10 +76,15 @@
</template>
<template #task-attachments>
<ProtocolAttachmentReadonly
:files="viewDetail?.attachmentFiles || []"
v-if="taskAttachmentFiles.length"
:files="taskAttachmentFiles"
@preview="handleAttachmentPreview"
@download="handleAttachmentDownload"
/>
<p v-else class="view-empty-hint">—</p>
</template>
<template #params-empty>
<p class="view-empty-hint">—</p>
</template>
</SystemViewDialog>
</template>
@@ -90,7 +95,7 @@
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
import { fetchQcTaskDetail, type QcTaskDetail } from '@/api/qc-task'
import { fetchQcTaskDetail, type QcTask, type QcTaskDetail } from '@/api/qc-task'
import {
fetchQcTaskResults,
fetchQcTaskMaterialUsages,
@@ -109,12 +114,21 @@
workOrderStatusLabel,
qcTaskConclusionLabel
} from '@/utils/qc-labels'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import { fetchQcInspectionBatches, loadQcInspectionBatches } from '@/utils/qc-inspection-batch'
import { formatMoneyDisplay } from '@/utils/money-format'
import { loadQcInspectionBatches, type QcInspectionBatch } from '@/utils/qc-inspection-batch'
import {
qcAttachmentFilesFromCommaUrls,
qcPreviewAttachment,
qcDownloadAttachment
} from '@/views/quality-inspection/qc-work-order/composables/useQcViewAttachmentHelpers'
const props = defineProps<{
visible: boolean
taskId?: string
/** 协议/捐赠等嵌入场景:工单摘要内任务行,详情接口失败时作回退 */
initialTask?: Partial<QcTask> | null
/** 嵌入场景批次列表,用于展示检测批次而无需额外权限 */
inspectionBatches?: QcInspectionBatch[] | null
/** 不展示样本信息、运输信息、样本接收(出库放行审核等) */
hideSampleContext?: boolean
}>()
@@ -143,7 +157,6 @@
const viewMaterialUsages = ref<ViewMaterialUsageRow[]>([])
const viewBatchName = ref('')
const viewBatchNo = ref('')
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
function materialRowsFromApi(items: QcTaskMaterialUsageItem[]): ViewMaterialUsageRow[] {
return (items || [])
@@ -159,9 +172,50 @@
}))
}
function mergeTaskDetail(base: Partial<QcTask>, fetched?: QcTaskDetail | null): QcTaskDetail {
const merged = { ...(base as QcTaskDetail), ...(fetched || {}) }
if (!merged.id && base.id) merged.id = base.id
return merged
}
function resolveBatchMeta(
detail: QcTaskDetail,
batches?: QcInspectionBatch[] | null
) {
const batchId = (detail.batchId || '').trim()
if (!batchId) {
viewBatchName.value = ''
viewBatchNo.value = ''
return
}
const embedded = batches || props.inspectionBatches
if (embedded?.length) {
const batch = embedded.find((b) => b.id === batchId)
viewBatchName.value = batch?.batchName || ''
viewBatchNo.value = batch?.batchNo || ''
return
}
const workOrderId = (detail.workOrderId || '').trim()
if (!workOrderId) {
viewBatchName.value = ''
viewBatchNo.value = ''
return
}
loadQcInspectionBatches(workOrderId, embedded)
.then((list) => {
const batch = list.find((b) => b.id === batchId)
viewBatchName.value = batch?.batchName || ''
viewBatchNo.value = batch?.batchNo || ''
})
.catch(() => {
viewBatchName.value = ''
viewBatchNo.value = ''
})
}
watch(
() => [props.visible, props.taskId] as const,
async ([vis, id]) => {
() => [props.visible, props.taskId, props.initialTask] as const,
async ([vis, id, preset]) => {
if (!vis || !id) {
viewDetail.value = null
viewMaterialUsages.value = []
@@ -170,28 +224,35 @@
return
}
loading.value = true
viewBatchName.value = ''
viewBatchNo.value = ''
try {
const detail = await fetchQcTaskDetail(id)
viewBatchName.value = ''
viewBatchNo.value = ''
const batchId = (detail.batchId || '').trim()
const workOrderId = (detail.workOrderId || '').trim()
if (batchId && workOrderId) {
try {
const batches = await loadQcInspectionBatches(workOrderId)
const batch = batches.find((b) => b.id === batchId)
viewBatchName.value = batch?.batchName || ''
viewBatchNo.value = batch?.batchNo || ''
} catch {
viewBatchName.value = ''
viewBatchNo.value = ''
}
}
let detail: QcTaskDetail | null = null
try {
const results = await fetchQcTaskResults(id)
;(detail as QcTaskDetail).results = results || []
detail = await fetchQcTaskDetail(id)
} catch {
;(detail as QcTaskDetail).results = []
detail = null
}
if (!detail && preset) {
detail = mergeTaskDetail(preset)
} else if (detail && preset) {
detail = mergeTaskDetail(preset, detail)
}
if (!detail) {
viewDetail.value = null
viewMaterialUsages.value = []
return
}
resolveBatchMeta(detail, props.inspectionBatches)
if ((detail.taskStatus || '').trim() !== 'dzx') {
try {
const results = await fetchQcTaskResults(id)
detail.results = results || []
} catch {
detail.results = []
}
} else {
detail.results = []
}
viewMaterialUsages.value = []
if (SHOW_QC_TASK_MATERIAL_USAGE && detail.taskType !== 'dsfjc') {
@@ -206,7 +267,7 @@
}
viewDetail.value = detail
} catch {
viewDetail.value = null
viewDetail.value = preset ? mergeTaskDetail(preset) : null
viewMaterialUsages.value = []
viewBatchName.value = ''
viewBatchNo.value = ''
@@ -217,16 +278,20 @@
{ immediate: true }
)
const taskAttachmentFiles = computed(() => {
const d = viewDetail.value
if (!d) return [] as Array<Record<string, unknown>>
if (Array.isArray(d.attachmentFiles) && d.attachmentFiles.length) {
return d.attachmentFiles as Array<Record<string, unknown>>
}
return qcAttachmentFilesFromCommaUrls(String(d.thirdPartyAttachments || ''))
})
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 viewReceptionAttachmentFiles = computed(() => {
const r = viewDetail.value?.reception
if (!r) return []
@@ -325,7 +390,7 @@
value:
r.acceptConclusionLabel || acceptConclusionLabel(String(r.acceptConclusion || ''))
},
{ label: '检查项', value: formatCheckResults(r.checkResults), span: true },
{ label: '检查项', checkItems: r.checkResults ?? [], span: true },
{ label: '备注', value: r.remark || '—', span: true }
]
}
@@ -343,6 +408,27 @@
},
{ label: '备注', value: d.remark || '—' }
]
const itemSection: ViewSection = {
title: '检测项目',
fields: [
{
label: '检测类型',
value: taskTypeLabel(d.taskType),
tagType: taskTypeTagType(d.taskType)
},
{ label: '项目名称', value: d.itemName || '—' },
{ label: '项目编码', value: d.itemCode || '—' },
...(d.taskType === 'dsfjc'
? [
{ label: '第三方公司', value: d.companyName || '—' },
{
label: '检测费用(元)',
value: d.feeAmount != null ? `¥${formatMoneyDisplay(d.feeAmount)}` : '—'
}
]
: [])
]
}
const resultSection: ViewSection[] = [
{
title: '质检结论',
@@ -358,13 +444,13 @@
}))
}
]
: [{ title: '检测参数', fields: [] }])
: [{ title: '检测参数', fields: [], slot: 'params-empty' }])
]
const tailSections: ViewSection[] = []
if (SHOW_QC_TASK_MATERIAL_USAGE && d.taskType !== 'dsfjc') {
tailSections.push({ title: '物料使用', fields: [], slot: 'material-usages' })
}
if (d.attachmentFiles?.length) {
if (d.taskType === 'dsfjc' || taskAttachmentFiles.value.length) {
tailSections.push({
title: d.taskType === 'dsfjc' ? '检测材料' : '检测附件',
fields: [],
@@ -401,17 +487,16 @@
: 'info'
},
{
label: '检测类型',
value: taskTypeLabel(d.taskType),
tagType: taskTypeTagType(d.taskType)
label: '任务状态',
value: taskStatusLabel(d.taskStatus),
tagType: d.taskStatus === 'ywc' ? 'success' : d.taskStatus === 'jxz' ? 'warning' : 'info'
},
{ label: '检测项目', value: d.itemName },
{ label: '项目编码', value: d.itemCode || '—' },
{ label: '第三方公司', value: d.companyName || '—' },
{ label: '负责人', value: d.assigneeUserNames || '—' },
{ label: '完成时间', value: d.completedTime || '—' },
{ label: '创建时间', value: d.createTime || '—' }
]
},
itemSection,
...cellProductSection,
...sampleSections,
...resultSection,
@@ -419,21 +504,8 @@
]
})
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 handleAttachmentPreview(file: Record<string, unknown>) {
const url = attachmentFullUrl(String(file.fileUrl || ''))
if (url) window.open(url, '_blank')
}
function handleAttachmentDownload(file: Record<string, unknown>) {
downloadAttachmentFile(file, { fullFileUrl: attachmentFullUrl })
}
const handleAttachmentPreview = qcPreviewAttachment
const handleAttachmentDownload = qcDownloadAttachment
</script>
<style lang="scss">
@@ -54,6 +54,8 @@
<QcTaskViewDialog
v-model:visible="taskViewVisible"
:task-id="taskViewId"
:initial-task="taskViewPreset"
:inspection-batches="qcInfo?.inspectionBatches"
:hide-sample-context="hideSampleContext"
/>
</Teleport>
@@ -132,6 +134,7 @@
const taskViewVisible = ref(false)
const taskViewId = ref('')
const taskViewPreset = ref<SampleQcTask | null>(null)
const hasTaskContent = computed(
() =>
@@ -156,6 +159,7 @@
function openTaskView(task: SampleQcTask) {
if (!task.id) return
taskViewId.value = task.id
taskViewPreset.value = task
taskViewVisible.value = true
}
</script>
@@ -38,12 +38,6 @@
<el-table-column label="样本编码" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ row.sampleCode || '—' }}</template>
</el-table-column>
<el-table-column label="采集量" min-width="88" align="center" header-align="center">
<template #default="{ row }">{{ row.collectAmount ?? '—' }}</template>
</el-table-column>
<el-table-column label="单位" min-width="72" align="center" header-align="center">
<template #default="{ row }">{{ displayUnitName(row) }}</template>
</el-table-column>
<el-table-column
v-if="showPrice"
label="原价格(元)"
@@ -193,38 +187,6 @@
/>
</el-select>
</el-form-item>
<el-form-item
v-if="!hideCollectDetailFields && !hideDialogCollectAmountUnit"
label="采集量"
>
<el-input
v-model="sampleDraft.collectAmount"
type="number"
step="any"
:min="0"
style="width: 100%"
/>
</el-form-item>
<el-form-item
v-if="!hideCollectDetailFields && !hideDialogCollectAmountUnit"
label="单位"
>
<el-select
v-model="sampleDraft.unitId"
placeholder="输入单位检索"
filterable
remote
reserve-keyword
clearable
:loading="unitOptionsLoading"
:remote-method="handleRemoteSearchUnit"
style="width: 100%"
@visible-change="onUnitSelectVisible"
@change="onSampleUnitChange"
>
<el-option v-for="u in unitOptions" :key="u.id" :label="u.unitName" :value="u.id" />
</el-select>
</el-form-item>
<el-form-item v-if="showPrice" label="原价格(元)">
<el-input
:model-value="fmtMoney(sampleDraft.originalPrice)"
@@ -71,6 +71,22 @@
<ul v-else-if="field.items?.length" class="view-plain-list">
<li v-for="(item, idx) in field.items" :key="idx">{{ item }}</li>
</ul>
<div
v-else-if="field.checkItems !== undefined"
class="check-list check-list-view"
>
<template v-if="field.checkItems.length">
<el-checkbox
v-for="item in field.checkItems"
:key="item.itemCode + String(item.checkItemId ?? item.itemName)"
:model-value="item.checked"
disabled
>
{{ item.itemName }}
</el-checkbox>
</template>
<template v-else>—</template>
</div>
<template v-else>{{ formatViewValue(field.value) }}</template>
</dd>
</div>
@@ -97,6 +113,22 @@
<ul v-else-if="field.items?.length" class="view-plain-list">
<li v-for="(item, idx) in field.items" :key="idx">{{ item }}</li>
</ul>
<div
v-else-if="field.checkItems !== undefined"
class="check-list check-list-view"
>
<template v-if="field.checkItems.length">
<el-checkbox
v-for="item in field.checkItems"
:key="item.itemCode + String(item.checkItemId ?? item.itemName)"
:model-value="item.checked"
disabled
>
{{ item.itemName }}
</el-checkbox>
</template>
<template v-else>—</template>
</div>
<template v-else>{{ formatViewValue(field.value) }}</template>
</dd>
</div>
@@ -145,11 +177,20 @@
export type TagType = 'success' | 'warning' | 'danger' | 'info' | '' | 'primary'
export interface ViewCheckItem {
itemCode?: string
itemName?: string
checked?: boolean
checkItemId?: string | number
}
export interface ViewField {
label: string
value?: string | number | boolean | null | undefined
/** 无编号列表(ul),与 value / tagType 二选一 */
items?: string[]
/** 样本接收检查项(只读多选框) */
checkItems?: ViewCheckItem[]
span?: boolean
/** 传入后以 el-tag 渲染,值为 el-tag 的 type 属性 */
tagType?: TagType
@@ -581,6 +622,42 @@
color: var(--mint-text-muted, #334155);
line-height: 1.5;
}
.check-list-view {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
pointer-events: none;
.el-checkbox.is-disabled {
opacity: 1;
cursor: default;
.el-checkbox__label {
color: var(--el-text-color-regular);
}
.el-checkbox__input.is-disabled {
opacity: 1;
cursor: default;
.el-checkbox__inner {
background-color: var(--el-checkbox-bg-color);
border-color: var(--el-checkbox-input-border-color);
cursor: default;
}
&.is-checked .el-checkbox__inner {
background-color: var(--el-checkbox-checked-bg-color);
border-color: var(--el-checkbox-checked-input-border-color);
&::after {
border-color: var(--el-checkbox-checked-icon-color);
}
}
}
}
}
}
.system-view-dialog--compact {