| | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { MesQcResultValueType } from '@vben/constants'; |
| | | import { MesQcIndicatorItemType, MesQcResultValueType } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { |
| | | Button, |
| | | Form as AForm, |
| | | Divider, |
| | | Input, |
| | |
| | | qcType: number; |
| | | } |
| | | |
| | | /** 可编辑测量批次(在接口结构上增加数值绑定字段) */ |
| | | interface EditMeasure { |
| | | detailId?: number; |
| | | resultId?: number; |
| | | measureNo: number; |
| | | value?: string; |
| | | valueNumber?: number; |
| | | remark?: string; |
| | | } |
| | | |
| | | /** 可编辑检验项(树形节点) */ |
| | | interface EditItem { |
| | | indicatorId?: number; |
| | | indicatorName?: string; |
| | | valueType?: number; |
| | | valueSpecification?: string; |
| | | parentId?: number; |
| | | itemType?: number; |
| | | formulaText?: string; |
| | | unitText?: string; |
| | | sortOrder?: number; |
| | | measures: EditMeasure[]; |
| | | children?: EditItem[]; |
| | | } |
| | | |
| | | defineOptions({ name: 'QcIndicatorResultForm' }); |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<FormType>('create'); |
| | | const items = ref<MesQcIndicatorResultApi.IndicatorResultDetail[]>([]); |
| | | const roots = ref<EditItem[]>([]); |
| | | const ctxData = ref<CtxData>(); |
| | | |
| | | const getTitle = computed(() => |
| | |
| | | wrapperClass: 'grid-cols-2', |
| | | }); |
| | | |
| | | /** 是否分组项(分组项不录入值,仅承载层级与公式文本) */ |
| | | function isGroup(item: EditItem) { |
| | | return item.itemType === MesQcIndicatorItemType.GROUP; |
| | | } |
| | | |
| | | /** 是否数值型录入(决定 InputNumber 与文本控件) */ |
| | | function isNumeric(item: EditItem) { |
| | | return ( |
| | | item.valueType === MesQcResultValueType.FLOAT || |
| | | item.valueType === MesQcResultValueType.INTEGER |
| | | ); |
| | | } |
| | | |
| | | /** 获取字典选项(valueSpecification 为系统字典类型名) */ |
| | | function getValueOptions(dictType?: string) { |
| | | return dictType |
| | |
| | | value: String(value), |
| | | })) |
| | | : []; |
| | | } |
| | | |
| | | /** 将接口返回的明细树转换为可编辑结构:分组递归、录入项保证至少一个测量批次 */ |
| | | function toEditItems( |
| | | items: MesQcIndicatorResultApi.IndicatorResultDetail[] = [], |
| | | ): EditItem[] { |
| | | return items.map((item) => { |
| | | const node: EditItem = { |
| | | indicatorId: item.indicatorId, |
| | | indicatorName: item.indicatorName, |
| | | valueType: item.valueType, |
| | | valueSpecification: item.valueSpecification, |
| | | parentId: item.parentId, |
| | | itemType: item.itemType, |
| | | formulaText: item.formulaText, |
| | | unitText: item.unitText, |
| | | sortOrder: item.sortOrder, |
| | | measures: [], |
| | | children: item.children ? toEditItems(item.children) : undefined, |
| | | }; |
| | | if (isGroup(node)) { |
| | | return node; |
| | | } |
| | | const measures = (item.measures ?? []).map((m) => ({ |
| | | detailId: m.detailId, |
| | | resultId: m.resultId, |
| | | measureNo: m.measureNo ?? 1, |
| | | value: m.value, |
| | | valueNumber: |
| | | isNumeric(node) && m.value !== null && m.value !== undefined |
| | | ? Number(m.value) |
| | | : undefined, |
| | | remark: m.remark, |
| | | })); |
| | | node.measures = measures.length > 0 ? measures : [{ measureNo: 1 }]; |
| | | return node; |
| | | }); |
| | | } |
| | | |
| | | /** 收集所有录入项(叶子)用于提交与展示统计 */ |
| | | function collectLeaves(nodes: EditItem[]): EditItem[] { |
| | | const leaves: EditItem[] = []; |
| | | const walk = (list: EditItem[]) => { |
| | | list.forEach((n) => { |
| | | if (isGroup(n)) { |
| | | walk(n.children ?? []); |
| | | } else { |
| | | leaves.push(n); |
| | | } |
| | | }); |
| | | }; |
| | | walk(nodes); |
| | | return leaves; |
| | | } |
| | | |
| | | /** 新增一个测量批次 */ |
| | | function addMeasure(item: EditItem) { |
| | | const nextNo = |
| | | item.measures.reduce((max, m) => Math.max(max, m.measureNo ?? 0), 0) + 1; |
| | | item.measures.push({ measureNo: nextNo }); |
| | | } |
| | | |
| | | /** 删除指定测量批次(至少保留一个) */ |
| | | function removeMeasure(item: EditItem, index: number) { |
| | | if (item.measures.length <= 1) { |
| | | message.warning('至少保留一个测量批次'); |
| | | return; |
| | | } |
| | | item.measures.splice(index, 1); |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | // 提交表单 |
| | | const head = |
| | | (await formApi.getValues()) as MesQcIndicatorResultApi.IndicatorResult; |
| | | const submitItems = items.value.map((item) => { |
| | | const submit: MesQcIndicatorResultApi.IndicatorResultDetail = { |
| | | id: item.id, |
| | | indicatorId: item.indicatorId, |
| | | remark: item.remark, |
| | | }; |
| | | if ( |
| | | item.valueType === MesQcResultValueType.FLOAT || |
| | | item.valueType === MesQcResultValueType.INTEGER |
| | | ) { |
| | | submit.value = |
| | | item.valueNumber === null ? undefined : String(item.valueNumber); |
| | | } else { |
| | | submit.value = item.value; |
| | | } |
| | | return submit; |
| | | // 展开:每个录入项的每个测量批次 → 一条明细 |
| | | const submitItems: MesQcIndicatorResultApi.IndicatorResultDetail[] = []; |
| | | collectLeaves(roots.value).forEach((item) => { |
| | | item.measures.forEach((measure) => { |
| | | const value = isNumeric(item) |
| | | ? measure.valueNumber === null || measure.valueNumber === undefined |
| | | ? undefined |
| | | : String(measure.valueNumber) |
| | | : measure.value; |
| | | submitItems.push({ |
| | | id: measure.detailId, |
| | | resultId: measure.resultId, |
| | | indicatorId: item.indicatorId, |
| | | measureNo: measure.measureNo, |
| | | value, |
| | | remark: measure.remark, |
| | | }); |
| | | }); |
| | | }); |
| | | const payload: MesQcIndicatorResultApi.IndicatorResult = { |
| | | ...head, |
| | |
| | | await createIndicatorResult(payload); |
| | | message.success($t('common.createSuccess')); |
| | | } |
| | | // 关闭并提示 |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | } finally { |
| | |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | ctxData.value = undefined; |
| | | items.value = []; |
| | | roots.value = []; |
| | | return; |
| | | } |
| | | formApi.setState({ schema: useQcIndicatorResultFormSchema(formApi) }); |
| | | // 加载数据 |
| | | const data = modalApi.getData<CtxData>(); |
| | | ctxData.value = data; |
| | | formType.value = data.formType; |
| | |
| | | data.qcType, |
| | | data.id, |
| | | ); |
| | | // 回填数值字段(用于 InputNumber 绑定) |
| | | items.value = (detail.items ?? []).map((item) => ({ |
| | | ...item, |
| | | valueNumber: |
| | | (item.valueType === MesQcResultValueType.FLOAT || |
| | | item.valueType === MesQcResultValueType.INTEGER) && |
| | | item.value !== null |
| | | ? Number(item.value) |
| | | : undefined, |
| | | })); |
| | | // 设置到 values |
| | | roots.value = toEditItems(detail.items); |
| | | await formApi.setValues({ |
| | | id: detail.id, |
| | | code: detail.code, |
| | |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | /** 单个测量值输入控件绑定(数值型走 valueNumber,其余走 value) */ |
| | | function bindMeasure(item: EditItem, measure: EditMeasure) { |
| | | return isNumeric(item) ? measure.valueNumber : measure.value; |
| | | } |
| | | function onMeasureInput(item: EditItem, measure: EditMeasure, v: any) { |
| | | if (isNumeric(item)) { |
| | | measure.valueNumber = v === null ? undefined : Number(v); |
| | | } else { |
| | | measure.value = v; |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | <div class="px-4"> |
| | | <Form /> |
| | | <Divider>检测值</Divider> |
| | | <!-- 检测值控件由每行 valueType 驱动,逐行渲染:FLOAT/INTEGER 走 InputNumber,DICT 走 Select 并按 valueSpecification 解析选项 --> |
| | | <AForm :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }"> |
| | | <div |
| | | v-for="(item, index) in items" |
| | | :key="item.indicatorId ?? index" |
| | | class="mb-2" |
| | | > |
| | | <AForm.Item |
| | | :label="`检测项${index + 1}:${item.indicatorName ?? ''}`" |
| | | > |
| | | <InputNumber |
| | | v-if=" |
| | | item.valueType === MesQcResultValueType.FLOAT || |
| | | item.valueType === MesQcResultValueType.INTEGER |
| | | " |
| | | v-model:value="item.valueNumber" |
| | | :precision="item.valueType === MesQcResultValueType.FLOAT ? 4 : 0" |
| | | class="!w-full" |
| | | placeholder="请输入" |
| | | /> |
| | | <Textarea |
| | | v-else-if="item.valueType === MesQcResultValueType.TEXT" |
| | | v-model:value="item.value" |
| | | :rows="2" |
| | | placeholder="请输入检测值" |
| | | /> |
| | | <Select |
| | | v-else-if="item.valueType === MesQcResultValueType.DICT" |
| | | v-model:value="item.value" |
| | | allow-clear |
| | | :options="getValueOptions(item.valueSpecification)" |
| | | placeholder="请选择" |
| | | /> |
| | | <Input |
| | | v-else-if="item.valueType === MesQcResultValueType.FILE" |
| | | v-model:value="item.value" |
| | | placeholder="请输入文件地址" |
| | | /> |
| | | <Input v-else v-model:value="item.value" placeholder="请输入" /> |
| | | </AForm.Item> |
| | | </div> |
| | | </AForm> |
| | | <div v-if="roots.length === 0" class="py-4 text-center text-gray-400"> |
| | | 该质检单未配置检测指标 |
| | | </div> |
| | | <div v-else class="qc-result-tree"> |
| | | <template v-for="(node, ni) in roots" :key="node.indicatorId ?? ni"> |
| | | <!-- 分组项:标题行 + 公式文本,下挂子录入项 --> |
| | | <div v-if="isGroup(node)"> |
| | | <div |
| | | class="mb-2 flex items-center justify-between rounded bg-gray-100 px-3 py-1.5 font-medium" |
| | | > |
| | | <span>{{ node.indicatorName }}</span> |
| | | <span v-if="node.formulaText" class="text-xs text-gray-500"> |
| | | {{ node.formulaText }} |
| | | </span> |
| | | </div> |
| | | <div |
| | | v-for="(child, ci) in node.children ?? []" |
| | | :key="child.indicatorId ?? ci" |
| | | class="mb-3 ml-4" |
| | | > |
| | | <div class="mb-1 flex items-center justify-between"> |
| | | <span class="font-medium"> |
| | | {{ child.indicatorName }} |
| | | <span v-if="child.unitText" class="text-xs text-gray-500"> |
| | | ({{ child.unitText }}) |
| | | </span> |
| | | </span> |
| | | <Button |
| | | size="small" |
| | | type="link" |
| | | @click="addMeasure(child)" |
| | | > |
| | | + 测量批次 |
| | | </Button> |
| | | </div> |
| | | <div |
| | | v-for="(measure, mi) in child.measures" |
| | | :key="mi" |
| | | class="mb-1 flex items-center gap-2" |
| | | > |
| | | <span class="w-16 shrink-0 text-right text-xs text-gray-500"> |
| | | 第 {{ measure.measureNo }} 次 |
| | | </span> |
| | | <InputNumber |
| | | v-if="child.valueType === MesQcResultValueType.FLOAT || child.valueType === MesQcResultValueType.INTEGER" |
| | | :value="bindMeasure(child, measure)" |
| | | :precision="child.valueType === MesQcResultValueType.FLOAT ? 4 : 0" |
| | | class="!w-48" |
| | | placeholder="请输入" |
| | | @update:value="(v) => onMeasureInput(child, measure, v)" |
| | | /> |
| | | <Textarea |
| | | v-else-if="child.valueType === MesQcResultValueType.TEXT" |
| | | v-model:value="measure.value" |
| | | :rows="1" |
| | | class="!w-48" |
| | | placeholder="请输入检测值" |
| | | /> |
| | | <Select |
| | | v-else-if="child.valueType === MesQcResultValueType.DICT" |
| | | v-model:value="measure.value" |
| | | allow-clear |
| | | class="!w-48" |
| | | :options="getValueOptions(child.valueSpecification)" |
| | | placeholder="请选择" |
| | | /> |
| | | <Input |
| | | v-else-if="child.valueType === MesQcResultValueType.FILE" |
| | | v-model:value="measure.value" |
| | | class="!w-48" |
| | | placeholder="请输入文件地址" |
| | | /> |
| | | <Input |
| | | v-else |
| | | v-model:value="measure.value" |
| | | class="!w-48" |
| | | placeholder="请输入" |
| | | /> |
| | | <Input |
| | | v-model:value="measure.remark" |
| | | class="flex-1" |
| | | placeholder="备注" |
| | | /> |
| | | <Button |
| | | size="small" |
| | | type="text" |
| | | danger |
| | | :disabled="child.measures.length <= 1" |
| | | @click="removeMeasure(child, mi)" |
| | | > |
| | | 删除 |
| | | </Button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- 顶级录入项 --> |
| | | <div v-else class="mb-3"> |
| | | <div class="mb-1 flex items-center justify-between"> |
| | | <span class="font-medium"> |
| | | {{ node.indicatorName }} |
| | | <span v-if="node.unitText" class="text-xs text-gray-500"> |
| | | ({{ node.unitText }}) |
| | | </span> |
| | | </span> |
| | | <Button size="small" type="link" @click="addMeasure(node)"> |
| | | + 测量批次 |
| | | </Button> |
| | | </div> |
| | | <div |
| | | v-for="(measure, mi) in node.measures" |
| | | :key="mi" |
| | | class="mb-1 flex items-center gap-2" |
| | | > |
| | | <span class="w-16 shrink-0 text-right text-xs text-gray-500"> |
| | | 第 {{ measure.measureNo }} 次 |
| | | </span> |
| | | <InputNumber |
| | | v-if="node.valueType === MesQcResultValueType.FLOAT || node.valueType === MesQcResultValueType.INTEGER" |
| | | :value="bindMeasure(node, measure)" |
| | | :precision="node.valueType === MesQcResultValueType.FLOAT ? 4 : 0" |
| | | class="!w-48" |
| | | placeholder="请输入" |
| | | @update:value="(v) => onMeasureInput(node, measure, v)" |
| | | /> |
| | | <Textarea |
| | | v-else-if="node.valueType === MesQcResultValueType.TEXT" |
| | | v-model:value="measure.value" |
| | | :rows="1" |
| | | class="!w-48" |
| | | placeholder="请输入检测值" |
| | | /> |
| | | <Select |
| | | v-else-if="node.valueType === MesQcResultValueType.DICT" |
| | | v-model:value="measure.value" |
| | | allow-clear |
| | | class="!w-48" |
| | | :options="getValueOptions(node.valueSpecification)" |
| | | placeholder="请选择" |
| | | /> |
| | | <Input |
| | | v-else-if="node.valueType === MesQcResultValueType.FILE" |
| | | v-model:value="measure.value" |
| | | class="!w-48" |
| | | placeholder="请输入文件地址" |
| | | /> |
| | | <Input |
| | | v-else |
| | | v-model:value="measure.value" |
| | | class="!w-48" |
| | | placeholder="请输入" |
| | | /> |
| | | <Input |
| | | v-model:value="measure.remark" |
| | | class="flex-1" |
| | | placeholder="备注" |
| | | /> |
| | | <Button |
| | | size="small" |
| | | type="text" |
| | | danger |
| | | :disabled="node.measures.length <= 1" |
| | | @click="removeMeasure(node, mi)" |
| | | > |
| | | 删除 |
| | | </Button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </div> |
| | | </div> |
| | | </Modal> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .qc-result-tree { |
| | | max-height: 60vh; |
| | | overflow-y: auto; |
| | | } |
| | | </style> |