Files
zkxw/web-admin/src/utils/qc-work-order-attachment-export.ts
T
2026-07-02 12:25:00 +08:00

242 lines
7.2 KiB
TypeScript

import JSZip from 'jszip'
import FileSaver from 'file-saver'
import type { QcWorkOrder } from '@/api/qc-work-order'
import type { QcInspectionBatch } from '@/utils/qc-inspection-batch'
import {
ensureUniqueZipEntryName,
fetchAttachmentBlob,
resolveAttachmentFileName,
sanitizeZipPathSegment
} from '@/utils/attachment-zip'
export type AttachmentZipExportPhase =
| 'preparing'
| 'downloading'
| 'zipping'
| 'done'
| 'cancelled'
export type AttachmentZipExportProgress = {
phase: AttachmentZipExportPhase
percent: number
currentFile?: string
currentIndex?: number
totalFiles?: number
contextHint?: string
}
export type AttachmentZipExportEntry = {
zipPath: string
fileUrl: string
fileName: string
contextHint?: string
}
export type AttachmentZipExportResult = {
added: number
failed: number
cancelled: boolean
}
const PREPARING_MAX = 5
const DOWNLOADING_MAX = 85
function isInboundQcWorkOrder(row: QcWorkOrder) {
return (row.workOrderType || 'inbound') === 'inbound'
}
function workOrderFolderName(row: QcWorkOrder, used: Set<string>) {
if (isInboundQcWorkOrder(row)) {
const labCode = sanitizeZipPathSegment(row.labStorageCode || '', '未命名')
const workOrderNo = sanitizeZipPathSegment(row.workOrderNo || row.id, '工单')
const base = `入库${labCode}_${workOrderNo}`
return ensureUniqueZipEntryName(base, used)
}
const parts = [sanitizeZipPathSegment(row.workOrderNo || row.id, '工单')]
if (row.sampleCode) {
parts.push(`样本${sanitizeZipPathSegment(row.sampleCode)}`)
}
return ensureUniqueZipEntryName(parts.join('_'), used)
}
function applyInboundLabStorageCodePrefix(code: string, workOrder: QcWorkOrder) {
if (!isInboundQcWorkOrder(workOrder)) return code
const raw = String(code || '').trim()
const workOrderNo = String(workOrder.workOrderNo || '').trim()
if (!raw || !workOrderNo || !raw.startsWith(workOrderNo)) return raw
const labCode = sanitizeZipPathSegment(workOrder.labStorageCode || '', '未命名')
return sanitizeZipPathSegment(`${labCode}${raw.slice(workOrderNo.length)}`, '')
}
function batchFolderName(batch: QcInspectionBatch, workOrder: QcWorkOrder, used: Set<string>) {
const no = sanitizeZipPathSegment(
applyInboundLabStorageCodePrefix(batch.batchNo || '', workOrder),
''
)
const name = sanitizeZipPathSegment(batch.batchName || '未命名批次')
const base = no ? `${no}_${name}` : name || `批次${batch.orderNum ?? 1}`
return ensureUniqueZipEntryName(base, used)
}
function taskFolderName(
task: { taskNo?: string; itemName?: string },
workOrder: QcWorkOrder,
used: Set<string>
) {
const no = sanitizeZipPathSegment(
applyInboundLabStorageCodePrefix(task.taskNo || '', workOrder),
''
)
const name = sanitizeZipPathSegment(task.itemName || '未命名任务')
const base = no ? `${no}_${name}` : name
return ensureUniqueZipEntryName(base, used)
}
export function buildQcWorkOrderZipEntries(
workOrders: QcWorkOrder[],
batchMap: Record<string, QcInspectionBatch[]>
): AttachmentZipExportEntry[] {
const entries: AttachmentZipExportEntry[] = []
const usedWorkOrderFolders = new Set<string>()
for (const wo of workOrders) {
const woFolder = workOrderFolderName(wo, usedWorkOrderFolders)
const batches = batchMap[wo.id] || []
const usedBatchFolders = new Set<string>()
for (const batch of batches) {
const batchFolder = batchFolderName(batch, wo, usedBatchFolders)
const tasks = batch.tasks || []
const usedTaskFolders = new Set<string>()
for (const task of tasks) {
const files = task.attachmentFiles?.filter((f) => String(f.fileUrl || '').trim()) || []
if (!files.length) continue
const taskFolder = taskFolderName(task, wo, usedTaskFolders)
const usedFileNames = new Set<string>()
const contextHint = [wo.workOrderNo, batch.batchName, task.itemName]
.filter(Boolean)
.join(' · ')
files.forEach((file, fileIndex) => {
const fileName = resolveAttachmentFileName(file, fileIndex)
const uniqueName = ensureUniqueZipEntryName(fileName, usedFileNames)
entries.push({
zipPath: `${woFolder}/${batchFolder}/${taskFolder}/${uniqueName}`,
fileUrl: file.fileUrl,
fileName,
contextHint
})
})
}
}
}
return entries
}
function calcDownloadingPercent(currentIndex: number, totalFiles: number) {
if (totalFiles <= 0) return PREPARING_MAX
const ratio = Math.min(1, (currentIndex + 1) / totalFiles)
return PREPARING_MAX + ratio * (DOWNLOADING_MAX - PREPARING_MAX)
}
function calcZippingPercent(zipPercent: number) {
return DOWNLOADING_MAX + (zipPercent / 100) * (100 - DOWNLOADING_MAX)
}
export async function runAttachmentZipExport(options: {
entries: AttachmentZipExportEntry[]
zipFileName: string
signal?: AbortSignal
onProgress?: (progress: AttachmentZipExportProgress) => void
}): Promise<AttachmentZipExportResult> {
const { entries, zipFileName, signal, onProgress } = options
const zip = new JSZip()
let added = 0
let failed = 0
const report = (partial: AttachmentZipExportProgress) => {
onProgress?.(partial)
}
const totalFiles = entries.length
for (let i = 0; i < entries.length; i += 1) {
if (signal?.aborted) {
report({ phase: 'cancelled', percent: calcDownloadingPercent(i, totalFiles) })
return { added, failed, cancelled: true }
}
const entry = entries[i]
report({
phase: 'downloading',
percent: Math.round(calcDownloadingPercent(i, totalFiles)),
currentFile: entry.fileName,
currentIndex: i + 1,
totalFiles,
contextHint: entry.contextHint
})
try {
const blob = await fetchAttachmentBlob(entry.fileUrl, entry.fileName, { signal })
if (signal?.aborted) {
report({ phase: 'cancelled', percent: calcDownloadingPercent(i, totalFiles) })
return { added, failed, cancelled: true }
}
if (blob) {
zip.file(entry.zipPath, blob)
added += 1
} else {
failed += 1
}
} catch {
if (signal?.aborted) {
report({ phase: 'cancelled', percent: calcDownloadingPercent(i, totalFiles) })
return { added, failed, cancelled: true }
}
failed += 1
}
}
if (added === 0) {
report({ phase: 'done', percent: 100 })
return { added, failed, cancelled: false }
}
if (signal?.aborted) {
return { added, failed, cancelled: true }
}
report({
phase: 'zipping',
percent: DOWNLOADING_MAX,
totalFiles,
currentIndex: totalFiles
})
const content = await zip.generateAsync({ type: 'blob' }, (metadata) => {
if (signal?.aborted) return
report({
phase: 'zipping',
percent: Math.round(calcZippingPercent(metadata.percent)),
totalFiles,
currentIndex: totalFiles
})
})
if (signal?.aborted) {
return { added, failed, cancelled: true }
}
FileSaver.saveAs(content, zipFileName)
report({ phase: 'done', percent: 100, totalFiles, currentIndex: totalFiles })
return { added, failed, cancelled: false }
}
export function reportPreparingProgress(
onProgress?: (progress: AttachmentZipExportProgress) => void
) {
onProgress?.({ phase: 'preparing', percent: 2 })
}