<script lang="ts" setup>
|
import type { FormType } from './data';
|
|
import type { MesQcIndicatorResultApi } from '#/api/mes/qc/indicatorresult';
|
|
import { computed, ref } from 'vue';
|
|
import { useVbenModal } from '@vben/common-ui';
|
import { MesQcIndicatorItemType, MesQcResultValueType } from '@vben/constants';
|
import { getDictOptions } from '@vben/hooks';
|
|
import {
|
Button,
|
Form as AForm,
|
Divider,
|
Input,
|
InputNumber,
|
message,
|
Select,
|
Textarea,
|
} from 'ant-design-vue';
|
|
import { useVbenForm } from '#/adapter/form';
|
import {
|
createIndicatorResult,
|
getIndicatorResultDetail,
|
updateIndicatorResult,
|
} from '#/api/mes/qc/indicatorresult';
|
import { $t } from '#/locales';
|
|
import { useQcIndicatorResultFormSchema } from './data';
|
|
interface CtxData {
|
formType: FormType;
|
id?: number;
|
qcId: number;
|
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 roots = ref<EditItem[]>([]);
|
const ctxData = ref<CtxData>();
|
|
const getTitle = computed(() =>
|
formType.value === 'update'
|
? $t('ui.actionTitle.edit', ['检验结果'])
|
: $t('ui.actionTitle.create', ['检验结果']),
|
);
|
|
const [Form, formApi] = useVbenForm({
|
commonConfig: {
|
componentProps: {
|
class: 'w-full',
|
},
|
formItemClass: 'col-span-1',
|
labelWidth: 100,
|
},
|
layout: 'horizontal',
|
schema: [],
|
showDefaultActions: false,
|
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
|
? getDictOptions(dictType, 'string').map(({ label, value }) => ({
|
label,
|
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({
|
async onConfirm() {
|
if (!ctxData.value) {
|
return;
|
}
|
const { valid } = await formApi.validate();
|
if (!valid) {
|
return;
|
}
|
modalApi.lock();
|
const head =
|
(await formApi.getValues()) as MesQcIndicatorResultApi.IndicatorResult;
|
// 展开:每个录入项的每个测量批次 → 一条明细
|
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,
|
items: submitItems,
|
qcId: ctxData.value.qcId,
|
qcType: ctxData.value.qcType,
|
};
|
try {
|
if (formType.value === 'update') {
|
await updateIndicatorResult(payload);
|
message.success($t('common.updateSuccess'));
|
} else {
|
await createIndicatorResult(payload);
|
message.success($t('common.createSuccess'));
|
}
|
await modalApi.close();
|
emit('success');
|
} finally {
|
modalApi.unlock();
|
}
|
},
|
async onOpenChange(isOpen: boolean) {
|
if (!isOpen) {
|
ctxData.value = undefined;
|
roots.value = [];
|
return;
|
}
|
formApi.setState({ schema: useQcIndicatorResultFormSchema(formApi) });
|
const data = modalApi.getData<CtxData>();
|
ctxData.value = data;
|
formType.value = data.formType;
|
modalApi.lock();
|
try {
|
const detail = await getIndicatorResultDetail(
|
data.qcId,
|
data.qcType,
|
data.id,
|
);
|
roots.value = toEditItems(detail.items);
|
await formApi.setValues({
|
id: detail.id,
|
code: detail.code,
|
remark: detail.remark,
|
sn: detail.sn,
|
});
|
} finally {
|
modalApi.unlock();
|
}
|
},
|
});
|
|
/** 单个测量值输入控件绑定(数值型走 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>
|
<Modal :title="getTitle" class="w-3/5">
|
<div class="px-4">
|
<Form />
|
<Divider>检测值</Divider>
|
<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>
|