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

+18 -2
View File
@@ -1,9 +1,19 @@
/**
* 将用户粘贴的中文日期时间(如「2026年6月22日12时18分」)解析为系统标准格式。
*/
export type ChineseDatetimeFormat = 'date' | 'datetime' | 'datetime-minute'
/** 将日期时间规范为 `YYYY-MM-DD HH:mm`(去掉秒,兼容历史带秒数据) */
export function formatDatetimeToMinute(val: string): string {
const s = String(val || '').trim()
if (!s) return ''
const parsed = parseChineseDatetimeText(s, 'datetime-minute')
return parsed || s
}
export function parseChineseDatetimeText(
text: string,
format: 'date' | 'datetime'
format: ChineseDatetimeFormat
): string | null {
const raw = text.trim()
if (!raw) return null
@@ -20,6 +30,9 @@ export function parseChineseDatetimeText(
const timeParts = time.split(':')
const hh = timeParts[0]?.padStart(2, '0') ?? '00'
const mm = timeParts[1]?.padStart(2, '0') ?? '00'
if (format === 'datetime-minute') {
return `${parts[0]} ${hh}:${mm}`
}
const ss = timeParts[2]?.padStart(2, '0') ?? '00'
return `${parts[0]} ${hh}:${mm}:${ss}`
}
@@ -38,13 +51,16 @@ export function parseChineseDatetimeText(
const h = String(Number(cnMatch[4] || 0)).padStart(2, '0')
const mi = String(Number(cnMatch[5] || 0)).padStart(2, '0')
if (format === 'datetime-minute') {
return `${dateStr} ${h}:${mi}`
}
const s = String(Number(cnMatch[6] || 0)).padStart(2, '0')
return `${dateStr} ${h}:${mi}:${s}`
}
export function handleChineseDatetimePaste(
e: ClipboardEvent,
format: 'date' | 'datetime',
format: ChineseDatetimeFormat,
setter: (val: string) => void
) {
const text = e.clipboardData?.getData('text')?.trim()
@@ -90,13 +90,15 @@ function parseScore(value: string): number | null | undefined {
return Number.isFinite(n) ? n : undefined
}
function parseDatetime(value: string): string | undefined {
function parseDatetime(value: string, minutePrecision = false): string | undefined {
const v = value.trim()
if (!v) return undefined
if (MATERNAL_BLOOD_EMPTY_MARKERS.some((m) => v === m || v.includes(m))) {
return undefined
}
return parseChineseDatetimeText(v, 'datetime') || undefined
return (
parseChineseDatetimeText(v, minutePrecision ? 'datetime-minute' : 'datetime') || undefined
)
}
function isMaternalBloodEmpty(value: string): boolean {
@@ -153,7 +155,7 @@ export function parseDonationStandardImportText(text: string): DonationStandardI
break
}
case 'sampleCollectTime': {
const dt = parseDatetime(kv.value)
const dt = parseDatetime(kv.value, true)
if (dt) result.sampleCollectTime = dt
break
}
@@ -161,7 +163,7 @@ export function parseDonationStandardImportText(text: string): DonationStandardI
if (isMaternalBloodEmpty(kv.value)) {
result.maternalBloodExplicitEmpty = true
} else {
const dt = parseDatetime(kv.value)
const dt = parseDatetime(kv.value, true)
if (dt) result.maternalBloodCollectTime = dt
}
break
+66
View File
@@ -80,3 +80,69 @@ export function normalizePlannedStartDate(raw?: string | null): string | null {
const formatted = formatPlannedStartDate(raw)
return formatted || null
}
export function productionStepAssigneeDisplayName(raw: string): string {
const s = String(raw || '').trim()
if (!s) return ''
const matched = s.match(/^(.+?)[((][^))]*[))]$/)
if (matched) return matched[1].trim()
return s
}
export function productionStepAssigneeLabels(row: Record<string, any>): string[] {
const namesStr = String(row.assigneeUserNames || '').trim()
if (namesStr) {
return namesStr
.split(/[,,、]/)
.map((n) => productionStepAssigneeDisplayName(n))
.filter(Boolean)
}
const namesArr: string[] = Array.isArray(row.assigneeNames) ? row.assigneeNames : []
const fromNamesArr = namesArr
.map((n) => productionStepAssigneeDisplayName(String(n || '')))
.filter(Boolean)
if (fromNamesArr.length) return fromNamesArr
const assignees: Array<{ userId?: string; nickName?: string; userNo?: string }> = Array.isArray(
row.assignees
)
? row.assignees
: []
const assigneeIds: string[] = Array.isArray(row.assigneeUserIds)
? row.assigneeUserIds.map((id) => String(id || '').trim()).filter(Boolean)
: []
if (assigneeIds.length) {
const byId = new Map(
assignees
.map((a) => [String(a.userId || '').trim(), a] as const)
.filter(([id]) => Boolean(id))
)
const fromIds = assigneeIds
.map((id) => {
const matched = byId.get(id)
const name = String(matched?.nickName || matched?.userNo || '').trim()
return name ? productionStepAssigneeDisplayName(name) : ''
})
.filter(Boolean)
if (fromIds.length) return fromIds
}
if (assignees.length) {
return assignees
.map((a) => String(a.nickName || a.userNo || '').trim())
.map((n) => productionStepAssigneeDisplayName(n))
.filter(Boolean)
}
return []
}
export function productionStepAssigneeText(row: Record<string, any>): string {
return productionStepAssigneeLabels(row).join('、')
}
export function productionStepPlannedStartText(row: Record<string, any>): string {
return formatPlannedStartDate(row.plannedStartTime)
}
+4 -4
View File
@@ -1,6 +1,6 @@
import {
fetchMineProductionWorkOrderTabStepCounts,
fetchProductionWorkOrderTabStepCounts,
fetchMineProductionWorkOrderTabStepCountsDeduped,
fetchProductionWorkOrderTabStepCountsDeduped,
sumProductionWorkOrderTabStepCounts
} from '@/api/production-work-order'
import {
@@ -57,7 +57,7 @@ export async function refreshProductionMenuBadges() {
if (visibleCodes.includes(PRODUCTION_MENU_CODE_WORK_ORDER)) {
tasks.push(
fetchProductionWorkOrderTabStepCounts()
fetchProductionWorkOrderTabStepCountsDeduped()
.then((data) =>
badgeStore.setBadge(
PRODUCTION_MENU_CODE_WORK_ORDER,
@@ -75,7 +75,7 @@ export async function refreshProductionMenuBadges() {
if (visibleCodes.includes(PRODUCTION_MENU_CODE_MY_TASK)) {
tasks.push(
fetchMineProductionWorkOrderTabStepCounts()
fetchMineProductionWorkOrderTabStepCountsDeduped()
.then((data) =>
badgeStore.setBadge(
PRODUCTION_MENU_CODE_MY_TASK,
@@ -8,6 +8,7 @@ import {
getProductionWorkOrderTab,
toWorkOrderListQuery
} from '@/utils/production-work-order-tabs'
import { matchesWorkOrderStatusFilter } from '@/utils/status-tag-registry'
export type ProductionTaskMineFilters = {
keyword?: string
@@ -17,7 +18,7 @@ export type ProductionTaskMineFilters = {
export type MineWorkOrderSearchFilters = {
keyword?: string
workOrderStatus?: string
workOrderStatus?: string | string[]
createTimeStart?: string
createTimeEnd?: string
infusionTimeStart?: string
@@ -59,7 +60,7 @@ function keywordMatchesWorkOrder(
? [row.workOrderNo, row.applicationNo, row.projectTitle, row.customerName, row.applicationName]
: tab.group === 'outbound'
? [row.workOrderNo, row.applicationNo, row.applicationName, row.patientName]
: [row.workOrderNo, row.sampleCode, row.labStorageCode]
: [row.workOrderNo, row.sampleCode, row.labStorageCode, row.motherName, row.newbornName]
return fields.some((f) => (f || '').toLowerCase().includes(kw))
}
@@ -100,6 +101,8 @@ export function buildWorkOrderFromTasks(tasks: ProductionTaskRow[]): ProductionW
handlerName: t.handlerName,
receptionTime: t.receptionTime,
labStorageCode: t.labStorageCode,
motherName: t.motherName,
newbornName: t.newbornName,
remark: t.workOrderRemark || t.remark,
createTime: t.workOrderCreateTime || t.createTime || ''
}
@@ -222,7 +225,7 @@ export function filterMineEntriesByWorkOrderSearch(
const row = entry.workOrder
if (resolveTabKeyFromWorkOrder(row) !== tabKey) return false
if (entry.tabKey !== tabKey) return false
if (filters.workOrderStatus && row.workOrderStatus !== filters.workOrderStatus) return false
if (!matchesWorkOrderStatusFilter(row.workOrderStatus, filters.workOrderStatus)) return false
if (!keywordMatchesWorkOrder(row, filters.keyword || '', tabKey)) return false
if (tabUsesInfusionTimeFilter(tab)) {
if (!dateInRange(row.infusionTime, filters.infusionTimeStart, filters.infusionTimeEnd)) {
@@ -0,0 +1,34 @@
/** 生产任务编辑弹窗 · 工艺库与步骤配置参考数据(会话级缓存,减少重复 editor-init 请求) */
export type ProductionTaskEditorReferenceOptions = {
processCategories: Record<string, any>[]
sops: Record<string, any>[]
outputTypes: Record<string, any>[]
equipments: Record<string, any>[]
reagents: Record<string, any>[]
consumables: Record<string, any>[]
itemUnits: Record<string, any>[]
}
export type ProductionTaskEditorReferenceCache = {
processLibrary: Record<string, any>[]
options: ProductionTaskEditorReferenceOptions
}
let cachedReference: ProductionTaskEditorReferenceCache | null = null
export function getCachedProductionTaskEditorReference(): ProductionTaskEditorReferenceCache | null {
return cachedReference
}
export function setCachedProductionTaskEditorReference(data: ProductionTaskEditorReferenceCache) {
cachedReference = data
}
export function hasCachedProductionTaskEditorReference(): boolean {
return cachedReference !== null
}
export function clearProductionTaskEditorReferenceCache() {
cachedReference = null
}
@@ -99,6 +99,8 @@ function buildInboundMetaFields(tabKey: ProductionWorkOrderTabKey): CardMetaFiel
})
}
fields.push(
{ label: '母亲姓名', getValue: (row) => row.motherName || '—' },
{ label: '新生儿姓名', getValue: (row) => row.newbornName || '—' },
{
label: tabKey === 'inbound_research_donation' ? '捐赠状态' : '协议状态',
kind: 'tag',
+46 -12
View File
@@ -34,6 +34,13 @@ export function fetchQcInspectionBatches(workOrderId: string) {
})
}
export function fetchQcInspectionBatchesBatch(workOrderIds: string[]) {
return request.post<Record<string, QcInspectionBatch[]>>({
url: '/web/admin/qc-work-orders/inspection-batches/batch',
data: { workOrderIds: workOrderIds.filter(Boolean) }
})
}
/** 优先使用详情内嵌批次,否则独立 GET */
export async function loadQcInspectionBatches(
workOrderId: string,
@@ -70,21 +77,45 @@ export function batchTasksPanelSlot(batchKey: string) {
return `batch-tasks-${batchKey}`
}
function resolveBatchSectionTasks<T extends QcBatchTaskLike>(
batch: QcInspectionBatch,
fallbackTasks?: T[]
): T[] {
const enrichedById = new Map<string, T>()
if (fallbackTasks?.length) {
for (const t of fallbackTasks) {
if (t.id) enrichedById.set(t.id, t)
}
}
const rawFromBatch = (batch.tasks || []) as T[]
if (enrichedById.size && rawFromBatch.length) {
return rawFromBatch.map((t) => (t.id && enrichedById.get(t.id)) || t)
}
if (enrichedById.size && batch.id) {
return fallbackTasks!.filter((t) => (t.batchId || '').trim() === batch.id)
}
return rawFromBatch
}
export function buildBatchSectionsFromBatches<T extends QcBatchTaskLike>(
batches: QcInspectionBatch[],
fallbackTasks?: T[]
): QcBatchTaskSection<T>[] {
if (batches.length) {
return batches.map((b) => ({
key: b.id || b.batchNo,
batchId: b.id,
batchNo: b.batchNo,
batchName: b.batchName || b.batchNo || '检测批次',
assigneeUserNames: b.assigneeUserNames || '',
taskCount: b.taskCount ?? (b.tasks?.length || 0),
completedTaskCount: b.completedTaskCount ?? 0,
tasks: (b.tasks || []) as T[]
}))
return batches.map((b) => {
const tasks = resolveBatchSectionTasks(b, fallbackTasks)
const completed = tasks.filter((t) => t.taskStatus === 'ywc').length
return {
key: b.id || b.batchNo,
batchId: b.id,
batchNo: b.batchNo,
batchName: b.batchName || b.batchNo || '检测批次',
assigneeUserNames: b.assigneeUserNames || '',
taskCount: b.taskCount ?? tasks.length,
completedTaskCount: b.completedTaskCount ?? completed,
tasks
}
})
}
if (fallbackTasks?.length) {
return buildBatchSectionsFromTasks(fallbackTasks)
@@ -131,14 +162,17 @@ export function resolveBatchSections<T extends QcBatchTaskLike>(
return []
}
export function createEmptyInspectionBatch(): QcInspectionBatchConfigureItem {
export function createEmptyInspectionBatch(defaultAssigneeUserIds?: string[]): QcInspectionBatchConfigureItem {
const today = new Date()
const y = today.getFullYear()
const m = String(today.getMonth() + 1).padStart(2, '0')
const d = String(today.getDate()).padStart(2, '0')
const assigneeUserIds = (defaultAssigneeUserIds || [])
.map((id) => String(id || '').trim())
.filter(Boolean)
return {
batchName: `${y}${m}${d}检测`,
assigneeUserIds: [],
assigneeUserIds,
hasLab: false,
hasThirdParty: false,
labItemIds: [],
+44 -37
View File
@@ -1,5 +1,4 @@
import type { QcWorkOrder } from '@/api/qc-work-order'
import { fetchQcWorkOrderDetail } from '@/api/qc-work-order'
import { fetchMyQcTaskList, type QcTask } from '@/api/qc-task'
import {
groupInboundQcWorkOrders,
@@ -18,6 +17,7 @@ import {
toAllTabListQuery,
workOrderMatchesCategory
} from '@/utils/qc-work-order-category'
import { matchesWorkOrderStatusFilter } from '@/utils/status-tag-registry'
export type QcTaskMineFilters = {
keyword?: string
@@ -28,7 +28,7 @@ export type QcTaskMineFilters = {
export type MineWorkOrderSearchFilters = {
keyword?: string
workOrderStatus?: string
workOrderStatus?: string | string[]
triggerSource?: string
qcCategory?: string
createTimeStart?: string
@@ -76,7 +76,14 @@ function keywordMatchesWorkOrder(
]
: tab.group === 'outbound'
? [row.workOrderNo, row.applicationNo, row.applicationName, row.patientName]
: [row.workOrderNo, row.protocolNo, row.sampleCode, row.labStorageCode]
: [
row.workOrderNo,
row.protocolNo,
row.sampleCode,
row.labStorageCode,
row.motherName,
row.newbornName
]
return fields.some((f) => (f || '').toLowerCase().includes(kw))
}
@@ -94,7 +101,7 @@ export function filterMineEntriesByWorkOrderSearch(
if (isAllQcWorkOrderTab(tabKey) && !workOrderMatchesCategory(row, filters.qcCategory || '')) {
return false
}
if (filters.workOrderStatus && row.workOrderStatus !== filters.workOrderStatus) return false
if (!matchesWorkOrderStatusFilter(row.workOrderStatus, filters.workOrderStatus)) return false
if (filters.triggerSource) {
if (isAllQcWorkOrderTab(tabKey)) {
if (!matchesAllTabTriggerSource(row, filters.triggerSource)) return false
@@ -149,30 +156,8 @@ export async function fetchAllMineTasks(filters: QcTaskMineFilters): Promise<QcT
export async function enrichMineEntriesWithWorkOrders(
entries: MineWorkOrderEntry[]
): Promise<MineWorkOrderEntry[]> {
const cache = new Map<string, QcWorkOrder>()
const uniqueIds = [...new Set(entries.map((entry) => entry.workOrderId))]
await Promise.all(
uniqueIds.map(async (id) => {
try {
const detail = await fetchQcWorkOrderDetail(id)
cache.set(id, detail)
} catch {
// 保留任务聚合的工单摘要
}
})
)
return entries.map((entry) => {
const detail = cache.get(entry.workOrderId)
if (!detail) return entry
return {
...entry,
workOrder: {
...detail,
hasLab: detail.hasLab ?? entry.workOrder.hasLab,
hasThirdParty: detail.hasThirdParty ?? entry.workOrder.hasThirdParty
}
}
})
// 工单摘要已由 /qc-tasks/mine 接口批量 enrich,无需再逐条请求详情
return entries
}
export function paginateMineEntries<T>(items: T[], page: number, pageSize: number) {
@@ -221,11 +206,12 @@ export function buildWorkOrderFromTasks(tasks: QcTask[]): QcWorkOrder {
workOrderNo: t.workOrderNo || '—',
workOrderType: woType,
workOrderStatus: t.workOrderStatus || '',
receptionId: '',
protocolSampleId: '',
collectionProtocolId: '',
receptionId: t.receptionId || '',
protocolSampleId: t.protocolSampleId || '',
collectionProtocolId: t.collectionProtocolId || '',
protocolNo: t.protocolNo || '',
sampleCode: t.sampleCode || '',
labStorageCode: t.labStorageCode,
receptionSource: t.receptionSource,
receptionSourceLabel: t.receptionSourceLabel,
qcBizType: t.qcBizType || woType,
@@ -239,13 +225,34 @@ export function buildWorkOrderFromTasks(tasks: QcTask[]): QcWorkOrder {
sampleTypeName: t.sampleTypeName,
receptionTime: t.receptionTime,
handlerName: t.handlerName,
hasLab: tasks.some((x) => x.taskType === 'sysz'),
hasThirdParty: tasks.some((x) => x.taskType === 'dsfjc'),
createTime: t.createTime || '',
bizType: woType === 'outbound' ? t.receptionSource : undefined,
bizTypeLabel: woType === 'outbound' ? t.receptionSourceLabel : undefined,
hasLab: t.hasLab ?? tasks.some((x) => x.taskType === 'sysz'),
hasThirdParty: t.hasThirdParty ?? tasks.some((x) => x.taskType === 'dsfjc'),
createTime: t.workOrderCreateTime || t.createTime || '',
bizType: woType === 'outbound' ? t.bizType || t.receptionSource : undefined,
bizTypeLabel: woType === 'outbound' ? t.bizTypeLabel || t.receptionSourceLabel : undefined,
applicationNo: t.applicationNo,
applicationName: t.applicationName,
projectTitle: t.projectTitle,
customerName: t.customerName,
channelName: t.channelName,
patientName: t.patientName,
motherName: t.motherName,
newbornName: t.newbornName,
sortNo: t.sortNo,
totalCourseCount: t.totalCourseCount,
infusionTime: t.infusionTime,
products: t.products,
cellType: t.cellType,
specText: t.specText,
triggerSource: t.triggerSource,
triggerSourceLabel: t.triggerSourceLabel,
productionWorkOrderId: t.productionWorkOrderId,
productionWorkOrderNo: t.productionWorkOrderNo,
protocolStatus: t.protocolStatus,
protocolStatusLabel: t.protocolStatusLabel
protocolStatusLabel: t.protocolStatusLabel,
paymentSummary: t.paymentSummary,
feeSummary: t.feeSummary,
remark: t.workOrderRemark || t.remark
}
}
@@ -77,6 +77,11 @@ function formatTempCellProductField(row: QcWorkOrder, value?: string | null) {
return String(value || '').trim() || '—'
}
function isInboundUserInfoRow(row: QcWorkOrder) {
const source = (row.receptionSource || '').trim()
return source === 'cell_storage' || source === 'research_donation'
}
function protocolStatusLabelBySource(status: string, source: string): string {
return source === 'research_donation' ? donationStatusLabel(status) : protocolStatusLabel(status)
}
@@ -99,6 +104,8 @@ const commonTailFields: CardMetaField[] = [
]
const inboundGroupedFields: CardMetaField[] = [
{ label: '母亲姓名', getValue: (row) => row.motherName || '—', visible: isInboundUserInfoRow },
{ label: '新生儿姓名', getValue: (row) => row.newbornName || '—', visible: isInboundUserInfoRow },
{ label: '质检对象', getValue: formatQcTarget, colSpan: 2 },
{
label: '关联生产工单',
@@ -159,6 +166,8 @@ const temporaryFields: CardMetaField[] = [
function buildCellStorageGroupContext(row: QcWorkOrder): string[] {
const parts: string[] = []
if (row.motherName) parts.push(`母亲 ${row.motherName}`)
if (row.newbornName) parts.push(`新生儿 ${row.newbornName}`)
if (row.sampleTypeName) parts.push(row.sampleTypeName)
if (row.protocolNo) parts.push(`协议 ${row.protocolNo}`)
if (row.protocolStatus) {
@@ -171,6 +180,8 @@ function buildCellStorageGroupContext(row: QcWorkOrder): string[] {
function buildDonationGroupContext(row: QcWorkOrder): string[] {
const parts: string[] = []
if (row.motherName) parts.push(`母亲 ${row.motherName}`)
if (row.newbornName) parts.push(`新生儿 ${row.newbornName}`)
if (row.sampleTypeName) parts.push(row.sampleTypeName)
if (row.protocolNo) parts.push(`捐赠 ${row.protocolNo}`)
if (row.protocolStatus) {
@@ -160,6 +160,49 @@ export const workOrderStatus: StatusRegistry = {
export const workOrderStatusLabel = createLabelGetter(workOrderStatus)
export const workOrderStatusTagType = createTagTypeGetter(workOrderStatus)
export const QC_WORK_ORDER_STATUS_FILTER_OPTIONS = Object.entries(workOrderStatus).map(
([value, meta]) => ({
value,
label: meta.label
})
)
export function joinWorkOrderStatusFilter(statuses: string[]): string | undefined {
const list = statuses.map((s) => s.trim()).filter(Boolean)
return list.length ? list.join(',') : undefined
}
export function matchesWorkOrderStatusFilter(
status: string,
filter: string | string[] | undefined
): boolean {
if (!filter) return true
const list = Array.isArray(filter)
? filter
: filter
.split(',')
.map((s) => s.trim())
.filter(Boolean)
if (!list.length) return true
return list.includes(status)
}
export const productionWorkOrderStatus: StatusRegistry = {
dpz: { label: '待配置', tagType: 'info' },
jxz: { label: '进行中', tagType: 'warning' },
ywc: { label: '已完成', tagType: 'success' },
yzf: { label: '已作废', tagType: 'danger' }
}
export const productionWorkOrderStatusLabel = createLabelGetter(productionWorkOrderStatus)
export const productionWorkOrderStatusTagType = createTagTypeGetter(productionWorkOrderStatus)
export const PRODUCTION_WORK_ORDER_STATUS_FILTER_OPTIONS = Object.entries(
productionWorkOrderStatus
).map(([value, meta]) => ({
value,
label: meta.label
}))
export const taskType: StatusRegistry = {
sysz: { label: '实验室自检', tagType: 'success' },
dsfjc: { label: '第三方检测', tagType: 'primary' }