提交
This commit is contained in:
1 parent
1ca2668112
commit
5b3edb1700
278 files changed
+53761
-36931
No files matched your search
@@ -1,5 +1,23 @@
|
||||
import { fetchCellProductSpecList } from '@/api/admin'
|
||||
|
||||
/** 从产出/细胞产品行解析规格展示文案(兼容 specName / specText 等字段) */
|
||||
export function resolveCellProductSpecDisplay(row: Record<string, any> | null | undefined): string {
|
||||
if (!row || typeof row !== 'object') return ''
|
||||
const raw =
|
||||
row.specName ?? row.specText ?? row.spec_name ?? row.spec_text ?? row.cellProductSpecName
|
||||
return raw == null ? '' : String(raw).trim()
|
||||
}
|
||||
|
||||
/** 规范化细胞产品行:规格统一写入 specName / specText(文本,非主数据 ID) */
|
||||
export function normalizeCellProductRow<T extends Record<string, any>>(row: T): T {
|
||||
const specText = resolveCellProductSpecDisplay(row)
|
||||
return {
|
||||
...row,
|
||||
specName: specText,
|
||||
specText
|
||||
}
|
||||
}
|
||||
|
||||
/** 按规格名称精确匹配主数据,返回 specId(可选关联) */
|
||||
export async function resolveCellProductSpecIdByName(
|
||||
specName: string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** 细胞产品类型是否需要代次(仅 MSC) */
|
||||
/** 细胞产品类型是否需要代次(仅 MSC;NK、存储等类型不展示代次) */
|
||||
export function cellProductTypeNeedsPassage(
|
||||
productTypeName?: string | null,
|
||||
productTypeCode?: string | null
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/** 预计细胞数量:界面与库统一单位为「千万」 */
|
||||
export const EXPECTED_CELL_COUNT_LABEL = '预计细胞数量(千万)'
|
||||
|
||||
export function formatExpectedCellCountDisplay(value: number | null | undefined): string {
|
||||
if (value == null || Number.isNaN(Number(value))) return '—'
|
||||
return String(value)
|
||||
}
|
||||
@@ -12,7 +12,9 @@ import { useMenuStore } from '@/store/modules/menu'
|
||||
import type { AppRouteRecord } from '@/types/router'
|
||||
import { PRODUCTION_WORK_ORDER_VISIBLE_TABS } from '@/utils/production-work-order-tabs'
|
||||
|
||||
const VISIBLE_TAB_KEYS = PRODUCTION_WORK_ORDER_VISIBLE_TABS.map((tab) => tab.key)
|
||||
const VISIBLE_TAB_KEYS = PRODUCTION_WORK_ORDER_VISIBLE_TABS
|
||||
.filter((tab) => tab.key !== 'all')
|
||||
.map((tab) => tab.key)
|
||||
|
||||
/** 解析菜单项对应的生产角标 menuCode(兼容后端菜单未带 menuCode) */
|
||||
export function resolveProductionMenuBadgeCode(item: AppRouteRecord): string | undefined {
|
||||
|
||||
@@ -6,19 +6,26 @@ import {
|
||||
resolveTabKeyFromWorkOrder,
|
||||
tabUsesInfusionTimeFilter,
|
||||
getProductionWorkOrderTab,
|
||||
toWorkOrderListQuery
|
||||
toWorkOrderListQuery,
|
||||
isAllProductionWorkOrderTab
|
||||
} from '@/utils/production-work-order-tabs'
|
||||
import {
|
||||
toAllTabListQuery,
|
||||
workOrderMatchesCategory
|
||||
} from '@/utils/production-work-order-category'
|
||||
import { matchesWorkOrderStatusFilter } from '@/utils/status-tag-registry'
|
||||
|
||||
export type ProductionTaskMineFilters = {
|
||||
keyword?: string
|
||||
taskStatus?: string
|
||||
productionCategory?: string
|
||||
tabKey: ProductionWorkOrderTabKey
|
||||
}
|
||||
|
||||
export type MineWorkOrderSearchFilters = {
|
||||
keyword?: string
|
||||
workOrderStatus?: string | string[]
|
||||
productionCategory?: string
|
||||
createTimeStart?: string
|
||||
createTimeEnd?: string
|
||||
infusionTimeStart?: string
|
||||
@@ -143,7 +150,9 @@ async function fetchMineTasksByScope(
|
||||
filters: ProductionTaskMineFilters
|
||||
): Promise<ProductionTaskRow[]> {
|
||||
const tab = getProductionWorkOrderTab(filters.tabKey)!
|
||||
const listQuery = toWorkOrderListQuery(tab)
|
||||
const listQuery = isAllProductionWorkOrderTab(filters.tabKey)
|
||||
? toAllTabListQuery(filters.productionCategory || '')
|
||||
: toWorkOrderListQuery(tab)
|
||||
const pageSize = MINE_TASK_FETCH_PAGE_SIZE
|
||||
let page = 1
|
||||
const all: ProductionTaskRow[] = []
|
||||
@@ -223,15 +232,26 @@ export function filterMineEntriesByWorkOrderSearch(
|
||||
const tab = getProductionWorkOrderTab(tabKey)!
|
||||
return entries.filter((entry) => {
|
||||
const row = entry.workOrder
|
||||
if (resolveTabKeyFromWorkOrder(row) !== tabKey) return false
|
||||
if (entry.tabKey !== tabKey) return false
|
||||
if (!isAllProductionWorkOrderTab(tabKey) && resolveTabKeyFromWorkOrder(row) !== tabKey) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
isAllProductionWorkOrderTab(tabKey) &&
|
||||
!workOrderMatchesCategory(row, filters.productionCategory || '')
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (!isAllProductionWorkOrderTab(tabKey) && entry.tabKey !== tabKey) 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)) {
|
||||
const keywordTabKey = isAllProductionWorkOrderTab(tabKey)
|
||||
? resolveTabKeyFromWorkOrder(row)
|
||||
: tabKey
|
||||
if (!keywordMatchesWorkOrder(row, filters.keyword || '', keywordTabKey)) return false
|
||||
if (isAllProductionWorkOrderTab(tabKey) || !tabUsesInfusionTimeFilter(tab)) {
|
||||
if (!dateInRange(row.createTime, filters.createTimeStart, filters.createTimeEnd)) {
|
||||
return false
|
||||
}
|
||||
} else if (!dateInRange(row.createTime, filters.createTimeStart, filters.createTimeEnd)) {
|
||||
} else if (!dateInRange(row.infusionTime, filters.infusionTimeStart, filters.infusionTimeEnd)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@/utils/status-tag-registry'
|
||||
import type { TagType } from '@/utils/status-tag-registry'
|
||||
import type { ProductionWorkOrderTabKey } from '@/utils/production-work-order-tabs'
|
||||
import { resolveTabKeyFromWorkOrder } from '@/utils/production-work-order-tabs'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
|
||||
export type CardMetaFieldKind = 'text' | 'tag'
|
||||
@@ -23,6 +24,7 @@ export type CardMetaField = {
|
||||
|
||||
export type ProductionWorkOrderCardMetaConfig = {
|
||||
tabKey: ProductionWorkOrderTabKey
|
||||
showCategoryTag: boolean
|
||||
showProtocolFeeSummary: boolean
|
||||
showOutboundFeeSummary: boolean
|
||||
/** 卡片展示关联申请单/输注计划的细胞产品明细 */
|
||||
@@ -181,8 +183,22 @@ const temporaryFields: CardMetaField[] = [
|
||||
]
|
||||
|
||||
const CARD_META_BY_TAB: Record<ProductionWorkOrderTabKey, ProductionWorkOrderCardMetaConfig> = {
|
||||
all: {
|
||||
tabKey: 'all',
|
||||
showCategoryTag: true,
|
||||
showProtocolFeeSummary: false,
|
||||
showOutboundFeeSummary: false,
|
||||
showCellProducts: false,
|
||||
metaFields: [],
|
||||
allowDispatchQc: false,
|
||||
allowSampleReception: false,
|
||||
allowDispatchComplete: false,
|
||||
allowEditRemark: false,
|
||||
allowDelete: false
|
||||
},
|
||||
inbound_cell_storage: {
|
||||
tabKey: 'inbound_cell_storage',
|
||||
showCategoryTag: false,
|
||||
showProtocolFeeSummary: true,
|
||||
showOutboundFeeSummary: false,
|
||||
showCellProducts: false,
|
||||
@@ -195,6 +211,7 @@ const CARD_META_BY_TAB: Record<ProductionWorkOrderTabKey, ProductionWorkOrderCar
|
||||
},
|
||||
inbound_research_donation: {
|
||||
tabKey: 'inbound_research_donation',
|
||||
showCategoryTag: false,
|
||||
showProtocolFeeSummary: false,
|
||||
showOutboundFeeSummary: false,
|
||||
showCellProducts: false,
|
||||
@@ -207,6 +224,7 @@ const CARD_META_BY_TAB: Record<ProductionWorkOrderTabKey, ProductionWorkOrderCar
|
||||
},
|
||||
outbound_pilot_service: {
|
||||
tabKey: 'outbound_pilot_service',
|
||||
showCategoryTag: false,
|
||||
showProtocolFeeSummary: false,
|
||||
showOutboundFeeSummary: false,
|
||||
showCellProducts: true,
|
||||
@@ -219,6 +237,7 @@ const CARD_META_BY_TAB: Record<ProductionWorkOrderTabKey, ProductionWorkOrderCar
|
||||
},
|
||||
outbound_basic_experiment: {
|
||||
tabKey: 'outbound_basic_experiment',
|
||||
showCategoryTag: false,
|
||||
showProtocolFeeSummary: false,
|
||||
showOutboundFeeSummary: true,
|
||||
showCellProducts: true,
|
||||
@@ -231,6 +250,7 @@ const CARD_META_BY_TAB: Record<ProductionWorkOrderTabKey, ProductionWorkOrderCar
|
||||
},
|
||||
outbound_clinical_application: {
|
||||
tabKey: 'outbound_clinical_application',
|
||||
showCategoryTag: false,
|
||||
showProtocolFeeSummary: false,
|
||||
showOutboundFeeSummary: true,
|
||||
showCellProducts: false,
|
||||
@@ -243,6 +263,7 @@ const CARD_META_BY_TAB: Record<ProductionWorkOrderTabKey, ProductionWorkOrderCar
|
||||
},
|
||||
temporary: {
|
||||
tabKey: 'temporary',
|
||||
showCategoryTag: false,
|
||||
showProtocolFeeSummary: false,
|
||||
showOutboundFeeSummary: false,
|
||||
showCellProducts: false,
|
||||
@@ -266,6 +287,20 @@ export function getCardMetaConfig(tabKey: ProductionWorkOrderTabKey): Production
|
||||
return CARD_META_BY_TAB[tabKey]
|
||||
}
|
||||
|
||||
export function resolveCardMetaForRow(
|
||||
row: ProductionWorkOrderItem,
|
||||
tabKey: ProductionWorkOrderTabKey
|
||||
): ProductionWorkOrderCardMetaConfig {
|
||||
if (tabKey !== 'all') return getCardMetaConfig(tabKey)
|
||||
const rowTabKey = resolveTabKeyFromWorkOrder(row)
|
||||
const rowMeta = getCardMetaConfig(rowTabKey)
|
||||
return {
|
||||
...rowMeta,
|
||||
tabKey: 'all',
|
||||
showCategoryTag: true
|
||||
}
|
||||
}
|
||||
|
||||
export function getCardMetaConfigForLegacyTab(
|
||||
activeTypeTab: 'inbound' | 'outbound' | 'temporary'
|
||||
): ProductionWorkOrderCardMetaConfig {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { ProductionWorkOrderItem } from '@/api/production-work-order'
|
||||
import type { TagType } from '@/utils/status-tag-registry'
|
||||
import {
|
||||
resolveTabKeyFromWorkOrder,
|
||||
type ProductionWorkOrderTabKey,
|
||||
getProductionWorkOrderTab,
|
||||
PRODUCTION_WORK_ORDER_VISIBLE_TABS
|
||||
} from '@/utils/production-work-order-tabs'
|
||||
|
||||
/** 「所有」Tab 内生产类型子筛选(不含 all / hidden clinical) */
|
||||
export type ProductionWorkOrderCategoryKey = Exclude<
|
||||
ProductionWorkOrderTabKey,
|
||||
'all' | 'outbound_clinical_application'
|
||||
>
|
||||
|
||||
export const PRODUCTION_WORK_ORDER_CATEGORY_OPTIONS: {
|
||||
key: ProductionWorkOrderCategoryKey
|
||||
label: string
|
||||
}[] = PRODUCTION_WORK_ORDER_VISIBLE_TABS.filter((t) => t.key !== 'all').map((t) => ({
|
||||
key: t.key as ProductionWorkOrderCategoryKey,
|
||||
label: t.label.replace(/生产$/, '')
|
||||
}))
|
||||
|
||||
const CATEGORY_LABELS: Record<ProductionWorkOrderCategoryKey, string> = {
|
||||
inbound_cell_storage: '细胞存储',
|
||||
inbound_research_donation: '科研捐赠',
|
||||
outbound_pilot_service: '中试服务',
|
||||
outbound_basic_experiment: '基础实验',
|
||||
temporary: '临时'
|
||||
}
|
||||
|
||||
const CATEGORY_TAG_TYPES: Record<ProductionWorkOrderCategoryKey, TagType> = {
|
||||
inbound_cell_storage: 'primary',
|
||||
inbound_research_donation: 'primary',
|
||||
outbound_pilot_service: 'info',
|
||||
outbound_basic_experiment: 'info',
|
||||
temporary: 'warning'
|
||||
}
|
||||
|
||||
export function resolveWorkOrderCategoryKey(row: {
|
||||
workOrderType?: string
|
||||
receptionSource?: string
|
||||
bizType?: string
|
||||
}): ProductionWorkOrderCategoryKey {
|
||||
const tabKey = resolveTabKeyFromWorkOrder(row)
|
||||
if (tabKey === 'outbound_clinical_application') return 'outbound_pilot_service'
|
||||
return tabKey as ProductionWorkOrderCategoryKey
|
||||
}
|
||||
|
||||
export function productionWorkOrderCategoryLabel(row: ProductionWorkOrderItem): string {
|
||||
return CATEGORY_LABELS[resolveWorkOrderCategoryKey(row)] || '—'
|
||||
}
|
||||
|
||||
export function productionWorkOrderCategoryTagType(row: ProductionWorkOrderItem): TagType {
|
||||
return CATEGORY_TAG_TYPES[resolveWorkOrderCategoryKey(row)] || 'info'
|
||||
}
|
||||
|
||||
export function toAllTabListQuery(categoryKey: string) {
|
||||
const key = (categoryKey || '').trim()
|
||||
if (!key) {
|
||||
return {
|
||||
workOrderType: undefined as string | undefined,
|
||||
receptionSource: undefined as string | undefined,
|
||||
bizType: undefined as string | undefined
|
||||
}
|
||||
}
|
||||
const tab = getProductionWorkOrderTab(key)
|
||||
if (!tab || tab.key === 'all') {
|
||||
return {
|
||||
workOrderType: undefined,
|
||||
receptionSource: undefined,
|
||||
bizType: undefined
|
||||
}
|
||||
}
|
||||
return {
|
||||
workOrderType: tab.workOrderType,
|
||||
receptionSource: tab.receptionSource,
|
||||
bizType: tab.bizType
|
||||
}
|
||||
}
|
||||
|
||||
export function workOrderMatchesCategory(
|
||||
row: ProductionWorkOrderItem,
|
||||
categoryKey: string
|
||||
): boolean {
|
||||
const key = (categoryKey || '').trim()
|
||||
if (!key) return true
|
||||
return resolveWorkOrderCategoryKey(row) === key
|
||||
}
|
||||
|
||||
const INBOUND_LAB_STORAGE_HEADER_CATEGORIES = new Set<ProductionWorkOrderCategoryKey>([
|
||||
'inbound_cell_storage',
|
||||
'inbound_research_donation'
|
||||
])
|
||||
|
||||
/** 任务执行/详情弹窗顶部:细胞存储、科研捐赠入库单展示实验室入库编码 */
|
||||
export function shouldShowTaskHeaderLabStorageCode(row: {
|
||||
workOrderType?: string
|
||||
receptionSource?: string
|
||||
bizType?: string
|
||||
}): boolean {
|
||||
return INBOUND_LAB_STORAGE_HEADER_CATEGORIES.has(resolveWorkOrderCategoryKey(row))
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ProductionWorkOrderItem } from '@/api/production-work-order'
|
||||
|
||||
export type ProductionWorkOrderTabKey =
|
||||
| 'all'
|
||||
| 'inbound_cell_storage'
|
||||
| 'inbound_research_donation'
|
||||
| 'outbound_pilot_service'
|
||||
@@ -8,7 +9,7 @@ export type ProductionWorkOrderTabKey =
|
||||
| 'outbound_clinical_application'
|
||||
| 'temporary'
|
||||
|
||||
export type ProductionWorkOrderTabGroup = 'inbound' | 'outbound' | 'temporary'
|
||||
export type ProductionWorkOrderTabGroup = 'all' | 'inbound' | 'outbound' | 'temporary'
|
||||
|
||||
export type ProductionWorkOrderTabConfig = {
|
||||
key: ProductionWorkOrderTabKey
|
||||
@@ -23,6 +24,13 @@ export type ProductionWorkOrderTabConfig = {
|
||||
}
|
||||
|
||||
export const PRODUCTION_WORK_ORDER_TABS: ProductionWorkOrderTabConfig[] = [
|
||||
{
|
||||
key: 'all',
|
||||
label: '所有',
|
||||
group: 'all',
|
||||
workOrderType: 'inbound',
|
||||
listLayout: 'inbound-grouped'
|
||||
},
|
||||
{
|
||||
key: 'inbound_cell_storage',
|
||||
label: '细胞存储生产',
|
||||
@@ -76,7 +84,7 @@ export const PRODUCTION_WORK_ORDER_TABS: ProductionWorkOrderTabConfig[] = [
|
||||
/** 生产工单 / 我的生产任务 Tab 栏展示项(不含 hidden) */
|
||||
export const PRODUCTION_WORK_ORDER_VISIBLE_TABS = PRODUCTION_WORK_ORDER_TABS.filter((t) => !t.hidden)
|
||||
|
||||
export const DEFAULT_PRODUCTION_WORK_ORDER_TAB: ProductionWorkOrderTabKey = 'inbound_cell_storage'
|
||||
export const DEFAULT_PRODUCTION_WORK_ORDER_TAB: ProductionWorkOrderTabKey = 'all'
|
||||
|
||||
export function getProductionWorkOrderTab(key: string): ProductionWorkOrderTabConfig | undefined {
|
||||
return PRODUCTION_WORK_ORDER_TABS.find((t) => t.key === key)
|
||||
@@ -93,6 +101,13 @@ export function parseProductionWorkOrderTabKey(raw?: string | null): ProductionW
|
||||
}
|
||||
|
||||
export function toWorkOrderListQuery(tab: ProductionWorkOrderTabConfig) {
|
||||
if (tab.key === 'all') {
|
||||
return {
|
||||
workOrderType: undefined as string | undefined,
|
||||
receptionSource: undefined as string | undefined,
|
||||
bizType: undefined as string | undefined
|
||||
}
|
||||
}
|
||||
return {
|
||||
workOrderType: tab.workOrderType,
|
||||
receptionSource: tab.receptionSource,
|
||||
@@ -123,6 +138,7 @@ export function tabUsesInfusionTimeFilter(tab: ProductionWorkOrderTabConfig): bo
|
||||
}
|
||||
|
||||
export function keywordPlaceholderForTab(tab: ProductionWorkOrderTabConfig): string {
|
||||
if (tab.key === 'all') return '工单号 / 样本编码 / 申请单号 / 产品名称等'
|
||||
if (tab.key === 'temporary') return '工单号'
|
||||
if (tab.key === 'outbound_basic_experiment') {
|
||||
return '工单号/申请单号/课题名称/客户'
|
||||
@@ -132,12 +148,16 @@ export function keywordPlaceholderForTab(tab: ProductionWorkOrderTabConfig): str
|
||||
}
|
||||
|
||||
export function filterTimeLabelForTab(tab: ProductionWorkOrderTabConfig): string {
|
||||
if (tab.key === 'all') return '创建时间'
|
||||
if (tab.key === 'outbound_basic_experiment') return '交付时间'
|
||||
if (tab.group === 'outbound') return '输注时间'
|
||||
return '创建时间'
|
||||
}
|
||||
|
||||
export function filterTimeHintForTab(tab: ProductionWorkOrderTabConfig): string {
|
||||
if (tab.key === 'all') {
|
||||
return '出库工单按输注/交付时间筛选请进入对应分类 Tab。'
|
||||
}
|
||||
if (tab.group === 'outbound') {
|
||||
return tab.key === 'outbound_basic_experiment'
|
||||
? '支持按交付时间筛选。'
|
||||
@@ -150,5 +170,18 @@ export function workOrderMatchesTab(
|
||||
row: ProductionWorkOrderItem,
|
||||
tabKey: ProductionWorkOrderTabKey
|
||||
): boolean {
|
||||
if (tabKey === 'all') return true
|
||||
return resolveTabKeyFromWorkOrder(row) === tabKey
|
||||
}
|
||||
|
||||
export function isAllProductionWorkOrderTab(tabKey: ProductionWorkOrderTabKey): boolean {
|
||||
return tabKey === 'all'
|
||||
}
|
||||
|
||||
/** 列表 Tab 为「所有」时,按工单解析其分类 Tab Key */
|
||||
export function resolveEffectiveRowTabKey(
|
||||
listTabKey: ProductionWorkOrderTabKey,
|
||||
row: ProductionWorkOrderItem
|
||||
): ProductionWorkOrderTabKey {
|
||||
return isAllProductionWorkOrderTab(listTabKey) ? resolveTabKeyFromWorkOrder(row) : listTabKey
|
||||
}
|
||||
@@ -4,10 +4,15 @@ export {
|
||||
payStatusLabel,
|
||||
payStatusTagType,
|
||||
payModeLabel,
|
||||
payModeTagType,
|
||||
transferTypeLabel,
|
||||
transferTypeTagType
|
||||
payModeTagType
|
||||
} from './status-tag-registry'
|
||||
export {
|
||||
loadTransferTypeDict,
|
||||
getTransferTypeOptions,
|
||||
TRANSFER_TYPE_DICT,
|
||||
resolveTransferTypeLabel as transferTypeLabel,
|
||||
resolveTransferTypeTagType as transferTypeTagType
|
||||
} from './transfer-type-dict'
|
||||
|
||||
export function formatPaymentDueDate(d: unknown) {
|
||||
const s = String(d || '')
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
export type SampleReceptionSourceType = 'cell_storage' | 'research_donation'
|
||||
|
||||
export const SAMPLE_TYPE_PERINATAL_HSC = '围产期造血干细胞'
|
||||
export const SAMPLE_TYPE_UC_MSC = '脐带间充质干细胞'
|
||||
|
||||
export type ProductionReceptionFieldKey =
|
||||
| 'originalBloodWeightG'
|
||||
| 'hesVolumeMl'
|
||||
| 'bloodType'
|
||||
| 'infectiousFourTestResult'
|
||||
| 'enrichedBloodWeightG'
|
||||
| 'cryopreservationVolumeMl'
|
||||
| 'inoculationAmount'
|
||||
| 'cryopreservationTime'
|
||||
| 'storageTotalTubes'
|
||||
| 'availableCellCount1e7'
|
||||
| 'originalBloodVolumeMl'
|
||||
| 'nucleatedCellCount1e8'
|
||||
|
||||
export type ProductionReceptionFieldDef = {
|
||||
key: ProductionReceptionFieldKey
|
||||
label: string
|
||||
kind: 'number' | 'text' | 'datetime' | 'select' | 'integer'
|
||||
selectOptions?: Array<{ label: string; value: string }>
|
||||
}
|
||||
|
||||
const INFECTIOUS_OPTIONS = [
|
||||
{ label: '正常', value: 'zc' },
|
||||
{ label: '不正常', value: 'bzc' }
|
||||
]
|
||||
|
||||
const FIELD_DEFS: Record<ProductionReceptionFieldKey, ProductionReceptionFieldDef> = {
|
||||
originalBloodWeightG: { key: 'originalBloodWeightG', label: '原血重量/g', kind: 'number' },
|
||||
hesVolumeMl: { key: 'hesVolumeMl', label: 'HES用量/ml', kind: 'number' },
|
||||
bloodType: { key: 'bloodType', label: '血型', kind: 'text' },
|
||||
infectiousFourTestResult: {
|
||||
key: 'infectiousFourTestResult',
|
||||
label: '传染四项检测(检测卡)',
|
||||
kind: 'select',
|
||||
selectOptions: INFECTIOUS_OPTIONS
|
||||
},
|
||||
enrichedBloodWeightG: { key: 'enrichedBloodWeightG', label: '富集后血量/g', kind: 'number' },
|
||||
cryopreservationVolumeMl: {
|
||||
key: 'cryopreservationVolumeMl',
|
||||
label: '冻存液用量/ml',
|
||||
kind: 'number'
|
||||
},
|
||||
inoculationAmount: { key: 'inoculationAmount', label: '接种量', kind: 'number' },
|
||||
cryopreservationTime: { key: 'cryopreservationTime', label: '冻存时间', kind: 'datetime' },
|
||||
storageTotalTubes: { key: 'storageTotalTubes', label: '储存总量(管)', kind: 'integer' },
|
||||
availableCellCount1e7: {
|
||||
key: 'availableCellCount1e7',
|
||||
label: '可提供细胞数量/*10E7',
|
||||
kind: 'number'
|
||||
},
|
||||
originalBloodVolumeMl: { key: 'originalBloodVolumeMl', label: '原血体积/ml', kind: 'number' },
|
||||
nucleatedCellCount1e8: {
|
||||
key: 'nucleatedCellCount1e8',
|
||||
label: '有核细胞数/10E8',
|
||||
kind: 'number'
|
||||
}
|
||||
}
|
||||
|
||||
const PROFILE_KEYS: Record<string, ProductionReceptionFieldKey[]> = {
|
||||
'cell_storage|围产期造血干细胞': [
|
||||
'originalBloodWeightG',
|
||||
'hesVolumeMl',
|
||||
'bloodType',
|
||||
'infectiousFourTestResult',
|
||||
'enrichedBloodWeightG',
|
||||
'cryopreservationVolumeMl'
|
||||
],
|
||||
'cell_storage|脐带间充质干细胞': [
|
||||
'inoculationAmount',
|
||||
'cryopreservationTime',
|
||||
'storageTotalTubes',
|
||||
'availableCellCount1e7'
|
||||
],
|
||||
'research_donation|脐带间充质干细胞': ['inoculationAmount', 'cryopreservationTime'],
|
||||
'research_donation|围产期造血干细胞': ['originalBloodVolumeMl', 'nucleatedCellCount1e8']
|
||||
}
|
||||
|
||||
export function resolveProductionReceptionFields(
|
||||
sourceType: string,
|
||||
sampleTypeName: string
|
||||
): ProductionReceptionFieldDef[] {
|
||||
const key = `${sourceType === 'research_donation' ? 'research_donation' : 'cell_storage'}|${String(sampleTypeName || '').trim()}`
|
||||
const keys = PROFILE_KEYS[key] || []
|
||||
return keys.map((k) => FIELD_DEFS[k])
|
||||
}
|
||||
|
||||
export function infectiousFourTestLabel(value: string) {
|
||||
return INFECTIOUS_OPTIONS.find((o) => o.value === value)?.label || value || '—'
|
||||
}
|
||||
|
||||
export type ProductionReceptionRecord = {
|
||||
id?: string
|
||||
receptionId?: string
|
||||
handlerUserId?: string
|
||||
handlerName?: string
|
||||
processTime?: string
|
||||
originalBloodWeightG?: number
|
||||
hesVolumeMl?: number
|
||||
bloodType?: string
|
||||
infectiousFourTestResult?: string
|
||||
infectiousFourTestResultLabel?: string
|
||||
enrichedBloodWeightG?: number
|
||||
cryopreservationVolumeMl?: number
|
||||
inoculationAmount?: number
|
||||
cryopreservationTime?: string
|
||||
storageTotalTubes?: number
|
||||
availableCellCount1e7?: number
|
||||
originalBloodVolumeMl?: number
|
||||
nucleatedCellCount1e8?: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
export type QcReceptionRecord = {
|
||||
receptionTime?: string
|
||||
handlerName?: string
|
||||
receiveTemperature?: string
|
||||
acceptConclusion?: string
|
||||
acceptConclusionLabel?: string
|
||||
labStorageCode?: string
|
||||
checkResults?: Array<{ itemName?: string; checked?: boolean }>
|
||||
remark?: string
|
||||
receiveAttachmentFiles?: Array<Record<string, unknown>>
|
||||
}
|
||||
|
||||
export function formatProductionFieldValue(
|
||||
def: ProductionReceptionFieldDef,
|
||||
record: ProductionReceptionRecord | null | undefined
|
||||
) {
|
||||
if (!record) return '—'
|
||||
const raw = record[def.key as keyof ProductionReceptionRecord]
|
||||
if (raw === null || raw === undefined || raw === '') return '—'
|
||||
if (def.key === 'infectiousFourTestResult') {
|
||||
return infectiousFourTestLabel(String(raw))
|
||||
}
|
||||
return String(raw)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import { acceptConclusionLabel } from '@/utils/qc-labels'
|
||||
import {
|
||||
formatProductionFieldValue,
|
||||
resolveProductionReceptionFields,
|
||||
type ProductionReceptionRecord
|
||||
} from '@/utils/sample-production-reception-fields'
|
||||
|
||||
export function buildQcReceptionViewFields(
|
||||
r: Record<string, unknown>,
|
||||
d?: Record<string, unknown>
|
||||
): ViewSection['fields'] {
|
||||
const accept = String(r.acceptConclusion || d?.acceptConclusion || '')
|
||||
return [
|
||||
{ label: '接收时间', value: String(r.receptionTime || d?.receptionTime || '—') },
|
||||
{ label: '接收经手人', value: String(r.handlerName || d?.handlerName || '—') },
|
||||
...(accept === 'js'
|
||||
? [
|
||||
{
|
||||
label: '实验室入库编码',
|
||||
value: String(r.labStorageCode || d?.labStorageCode || '—')
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: '接收温度(℃)',
|
||||
value: r.receiveTemperature ? `${r.receiveTemperature}℃` : '—'
|
||||
},
|
||||
{
|
||||
label: '结论',
|
||||
value:
|
||||
String(r.acceptConclusionLabel || '') ||
|
||||
acceptConclusionLabel(accept) ||
|
||||
'—',
|
||||
tagType: accept === 'js' ? 'success' : accept === 'bj' ? 'danger' : 'info'
|
||||
},
|
||||
{
|
||||
label: '检查项',
|
||||
checkItems: (r.checkResults as ViewSection['fields'][0]['checkItems']) ?? [],
|
||||
span: true
|
||||
},
|
||||
{ label: '备注', value: String(r.remark || '—'), span: true }
|
||||
]
|
||||
}
|
||||
|
||||
export function buildProductionReceptionViewSection(
|
||||
productionReception: ProductionReceptionRecord | null | undefined,
|
||||
sourceType: string,
|
||||
sampleTypeName: string
|
||||
): ViewSection {
|
||||
if (!productionReception) {
|
||||
return {
|
||||
title: '生产接收',
|
||||
fields: [{ label: '状态', value: '生产尚未登记', span: true }]
|
||||
}
|
||||
}
|
||||
const fields: ViewSection['fields'] = [
|
||||
{ label: '处理时间', value: productionReception.processTime || '—' },
|
||||
{ label: '处理人', value: productionReception.handlerName || '—' }
|
||||
]
|
||||
for (const def of resolveProductionReceptionFields(sourceType, sampleTypeName)) {
|
||||
fields.push({
|
||||
label: def.label,
|
||||
value: formatProductionFieldValue(def, productionReception)
|
||||
})
|
||||
}
|
||||
if (productionReception.remark) {
|
||||
fields.push({ label: '备注', value: productionReception.remark, span: true })
|
||||
}
|
||||
return { title: '生产接收', fields }
|
||||
}
|
||||
@@ -95,7 +95,8 @@ export const transferType: StatusRegistry = {
|
||||
wxzz: { label: '微信转账', tagType: 'success' },
|
||||
zzbzz: { label: '支付宝转账', tagType: 'primary' },
|
||||
yhkzz: { label: '银行卡转账', tagType: 'info' },
|
||||
xj: { label: '现金', tagType: 'warning' }
|
||||
xj: { label: '现金', tagType: 'warning' },
|
||||
lszz: { label: '乐刷', tagType: 'primary' }
|
||||
}
|
||||
export const transferTypeLabel = createLabelGetter(transferType)
|
||||
export const transferTypeTagType = createTagTypeGetter(transferType)
|
||||
|
||||
@@ -13,6 +13,50 @@ export type LoadStorageSlotSelectOptions = {
|
||||
includeSlotIds?: string[]
|
||||
}
|
||||
|
||||
/** 远程检索防抖(毫秒):用户停顿后再请求 */
|
||||
export const STORAGE_SLOT_REMOTE_SEARCH_DEBOUNCE_MS = 1000
|
||||
|
||||
/** 储物格下拉每次返回条数 */
|
||||
export const STORAGE_SLOT_SELECT_PAGE_SIZE = 20
|
||||
|
||||
/** 远程检索序号:丢弃过期的空关键词/旧输入响应,避免覆盖最新检索结果 */
|
||||
let storageSlotRemoteSearchSeq = 0
|
||||
let lastStorageSlotSearchQuery = ''
|
||||
|
||||
export function getLastStorageSlotSearchQuery(): string {
|
||||
return lastStorageSlotSearchQuery
|
||||
}
|
||||
|
||||
export async function remoteSearchStorageSlotSelectOptions(
|
||||
query: string,
|
||||
opts?: LoadStorageSlotSelectOptions
|
||||
): Promise<StorageSlotSelectOption[] | null> {
|
||||
const trimmed = (query ?? '').trim()
|
||||
lastStorageSlotSearchQuery = trimmed
|
||||
const seq = ++storageSlotRemoteSearchSeq
|
||||
const includeSlotIds = (opts?.includeSlotIds || [])
|
||||
.map((id) => String(id).trim())
|
||||
.filter(Boolean)
|
||||
.join(',')
|
||||
try {
|
||||
const res = await fetchStorageSlotSelectOptions({
|
||||
keyword: trimmed || undefined,
|
||||
availableOnly: opts?.availableOnly ? true : undefined,
|
||||
includeSlotIds: includeSlotIds || undefined,
|
||||
page: 1,
|
||||
pageSize: STORAGE_SLOT_SELECT_PAGE_SIZE
|
||||
})
|
||||
if (seq !== storageSlotRemoteSearchSeq) return null
|
||||
return (res?.list || []).map((item) => ({
|
||||
id: String(item.id || ''),
|
||||
name: String(item.name || item.fullSlotCode || item.slotCode || item.id || '')
|
||||
}))
|
||||
} catch {
|
||||
if (seq !== storageSlotRemoteSearchSeq) return null
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export function getStorageFacilityOrderNum(item: Record<string, unknown>): number {
|
||||
const raw = item?.orderNum ?? item?.order_num ?? item?.sortOrder ?? item?.sort_order ?? 0
|
||||
const parsed = Number(raw)
|
||||
@@ -246,31 +290,6 @@ export async function loadStorageSlotSelectOptions(
|
||||
return options
|
||||
}
|
||||
|
||||
export async function remoteSearchStorageSlotSelectOptions(
|
||||
query: string,
|
||||
opts?: LoadStorageSlotSelectOptions
|
||||
): Promise<StorageSlotSelectOption[]> {
|
||||
const includeSlotIds = (opts?.includeSlotIds || [])
|
||||
.map((id) => String(id).trim())
|
||||
.filter(Boolean)
|
||||
.join(',')
|
||||
try {
|
||||
const res = await fetchStorageSlotSelectOptions({
|
||||
keyword: query?.trim() || undefined,
|
||||
availableOnly: opts?.availableOnly ? true : undefined,
|
||||
includeSlotIds: includeSlotIds || undefined,
|
||||
page: 1,
|
||||
pageSize: 100
|
||||
})
|
||||
return (res?.list || []).map((item) => ({
|
||||
id: String(item.id || ''),
|
||||
name: String(item.name || item.fullSlotCode || item.slotCode || item.id || '')
|
||||
}))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureStorageSlotsInOptions(
|
||||
options: StorageSlotSelectOption[],
|
||||
stubs: Array<{ id?: string; name?: string }>
|
||||
@@ -317,3 +336,25 @@ export function validateUniqueStorageSlotIds(
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export type OutputStorageSlotRow = {
|
||||
storageSlotId?: string | null
|
||||
storageSlotName?: string | null
|
||||
productStatus?: string | null
|
||||
}
|
||||
|
||||
/** 产出是否尚未分配储物格(已作废产出不计入) */
|
||||
export function isOutputMissingStorageSlot(row: OutputStorageSlotRow): boolean {
|
||||
if (String(row.productStatus ?? '').trim() === 'voided') return false
|
||||
const slotId = String(row.storageSlotId ?? '').trim()
|
||||
const slotName = String(row.storageSlotName ?? '').trim()
|
||||
return !slotId && !slotName
|
||||
}
|
||||
|
||||
/** 统计未分配储物格的产出份数 */
|
||||
export function countOutputsMissingStorageSlot(
|
||||
items: OutputStorageSlotRow[] | null | undefined
|
||||
): number {
|
||||
if (!items?.length) return 0
|
||||
return items.filter(isOutputMissingStorageSlot).length
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { fetchDictDataOptions } from '@/api/admin'
|
||||
import {
|
||||
transferTypeLabel as transferTypeFallbackLabel,
|
||||
transferTypeTagType as transferTypeTagTypeFallback
|
||||
} from './status-tag-registry'
|
||||
|
||||
/** 转账方式字典类型编码(与 sys_dict_type.dict_type 一致) */
|
||||
export const TRANSFER_TYPE_DICT = 'zzfs'
|
||||
|
||||
let labelMap: Record<string, string> = {}
|
||||
let optionsCache: Api.Dict.DictDataOption[] = []
|
||||
let loadPromise: Promise<void> | null = null
|
||||
|
||||
export function getTransferTypeOptions(): Api.Dict.DictDataOption[] {
|
||||
return optionsCache
|
||||
}
|
||||
|
||||
export async function loadTransferTypeDict(force = false): Promise<void> {
|
||||
if (!force && optionsCache.length) return
|
||||
if (!force && loadPromise) return loadPromise
|
||||
loadPromise = (async () => {
|
||||
try {
|
||||
const res = await fetchDictDataOptions(TRANSFER_TYPE_DICT)
|
||||
optionsCache = res || []
|
||||
labelMap = Object.fromEntries(optionsCache.map((o) => [o.value, o.label]))
|
||||
} catch (e) {
|
||||
console.error('加载转账方式字典失败:', e)
|
||||
if (!optionsCache.length) {
|
||||
optionsCache = []
|
||||
labelMap = {}
|
||||
}
|
||||
} finally {
|
||||
loadPromise = null
|
||||
}
|
||||
})()
|
||||
return loadPromise
|
||||
}
|
||||
|
||||
export function resolveTransferTypeLabel(value: string): string {
|
||||
const code = String(value || '').trim()
|
||||
if (!code) return '—'
|
||||
return labelMap[code] || transferTypeFallbackLabel(code) || code
|
||||
}
|
||||
|
||||
export function resolveTransferTypeTagType(value: string): string {
|
||||
return transferTypeTagTypeFallback(String(value || ''))
|
||||
}
|
||||
|
||||
/** @deprecated 请使用 resolveTransferTypeLabel;保留别名便于渐进迁移 */
|
||||
export const transferTypeLabel = resolveTransferTypeLabel
|
||||
|
||||
/** @deprecated 请使用 resolveTransferTypeTagType */
|
||||
export const transferTypeTagType = resolveTransferTypeTagType
|
||||
Reference in new issue
Block a user