`feat(mes-qc): 迁移 antd 来料检验及检测结果、缺陷记录组件`

pull/350/head
YunaiV 2026-05-29 15:54:58 +08:00
parent d3233c4bb4
commit abc8789fe3
10 changed files with 1826 additions and 0 deletions

View File

@ -0,0 +1,165 @@
<script lang="ts" setup>
import type { MesQcDefectRecordApi } from '#/api/mes/qc/defectrecord';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createDefectRecord,
getDefectRecord,
updateDefectRecord,
} from '#/api/mes/qc/defectrecord';
import { $t } from '#/locales';
interface OpenData {
formType: 'create' | 'update';
id?: number;
lineId: number;
qcId: number;
qcType: number;
}
defineOptions({ name: 'DefectRecordInlineForm' });
const emit = defineEmits(['success']);
// TODO @AI createupdate
const formType = ref<'create' | 'update'>('create');
const formData = ref<MesQcDefectRecordApi.DefectRecord>();
const getTitle = computed(() =>
formType.value === 'update'
? $t('ui.actionTitle.edit', ['缺陷记录'])
: $t('ui.actionTitle.create', ['缺陷记录']),
);
// TODO @AI data.ts
// TODO @AI
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 90,
},
layout: 'horizontal',
schema: [
{
fieldName: 'name',
label: '缺陷描述',
component: 'Textarea',
formItemClass: 'col-span-2',
componentProps: {
placeholder: '请输入缺陷描述',
rows: 2,
},
rules: 'required',
},
{
fieldName: 'level',
label: '缺陷等级',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.MES_DEFECT_LEVEL, 'number'),
placeholder: '请选择缺陷等级',
},
rules: 'selectRequired',
},
{
fieldName: 'quantity',
label: '缺陷数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 1,
placeholder: '请输入缺陷数量',
},
rules: 'required',
},
{
fieldName: 'remark',
label: '备注',
component: 'Input',
formItemClass: 'col-span-2',
componentProps: {
placeholder: '请输入备注',
},
},
],
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!formData.value) {
return;
}
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
try {
const values =
(await formApi.getValues()) as MesQcDefectRecordApi.DefectRecord;
const payload: MesQcDefectRecordApi.DefectRecord = {
...values,
lineId: formData.value.lineId,
qcId: formData.value.qcId,
qcType: formData.value.qcType,
};
if (formType.value === 'update') {
await updateDefectRecord({ ...payload, id: formData.value.id });
message.success($t('common.updateSuccess'));
} else {
await createDefectRecord(payload);
message.success($t('common.createSuccess'));
}
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
const data = modalApi.getData<OpenData>();
formType.value = data.formType;
if (data.id) {
// id
modalApi.lock();
try {
formData.value = await getDefectRecord(data.id);
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
} else {
// 1
formData.value = {
lineId: data.lineId,
qcId: data.qcId,
qcType: data.qcType,
};
await formApi.setValues({ quantity: 1 });
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-2/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@ -0,0 +1,215 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesQcDefectRecordApi } from '#/api/mes/qc/defectrecord';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteDefectRecord,
getDefectRecordPage,
} from '#/api/mes/qc/defectrecord';
import { $t } from '#/locales';
import DefectRecordInlineForm from './defect-record-inline-form.vue';
defineOptions({ name: 'DefectRecordInlineList' });
const emit = defineEmits(['success']);
// TODO @AI QcData
interface OpenData {
formType?: string;
lineId: number;
qcId: number;
qcType: number;
}
const qcData = ref<OpenData>();
const isReadonly = computed(() => qcData.value?.formType === 'detail');
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
emit('success');
}
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: DefectRecordInlineForm,
destroyOnClose: true,
});
/** 新增缺陷 */
function handleCreate() {
if (!qcData.value) {
return;
}
formModalApi
.setData({
formType: 'create',
lineId: qcData.value.lineId,
qcId: qcData.value.qcId,
qcType: qcData.value.qcType,
})
.open();
}
/** 编辑缺陷 */
function handleEdit(row: MesQcDefectRecordApi.DefectRecord) {
if (!qcData.value) {
return;
}
formModalApi
.setData({
formType: 'update',
id: row.id,
lineId: qcData.value.lineId,
qcId: qcData.value.qcId,
qcType: qcData.value.qcType,
})
.open();
}
/** 删除缺陷 */
async function handleDelete(row: MesQcDefectRecordApi.DefectRecord) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
});
try {
await deleteDefectRecord(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh();
} finally {
hideLoading();
}
}
// TODO @AI data.ts
// TODO @AI
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: [
{
field: 'name',
title: '缺陷描述',
minWidth: 200,
},
{
field: 'level',
title: '缺陷等级',
width: 120,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_DEFECT_LEVEL },
},
},
{
field: 'quantity',
title: '缺陷数量',
width: 100,
},
{
field: 'remark',
title: '备注',
minWidth: 150,
},
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
],
height: 320,
proxyConfig: {
ajax: {
query: async ({ page }) => {
if (!qcData.value) {
return { list: [], total: 0 };
}
return await getDefectRecordPage({
lineId: qcData.value.lineId,
pageNo: page.currentPage,
pageSize: page.pageSize,
qcId: qcData.value.qcId,
qcType: qcData.value.qcType,
});
},
},
},
rowConfig: {
isHover: true,
keyField: 'id',
},
toolbarConfig: {
refresh: true,
},
} as VxeTableGridOptions<MesQcDefectRecordApi.DefectRecord>,
});
const [Modal, modalApi] = useVbenModal({
showCancelButton: false,
showConfirmButton: false,
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
qcData.value = undefined;
return;
}
qcData.value = modalApi.getData<OpenData>();
await gridApi.query();
},
});
</script>
<template>
<Modal title="缺陷记录" class="w-3/5">
<FormModal @success="handleRefresh" />
<Grid class="mx-4">
<template v-if="!isReadonly" #toolbar-tools>
<TableAction
:actions="[
{
auth: ['mes:qc-defect:create'],
icon: ACTION_ICON.ADD,
label: '新增缺陷',
onClick: handleCreate,
type: 'primary',
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
auth: ['mes:qc-defect:update'],
icon: ACTION_ICON.EDIT,
ifShow: !isReadonly,
label: $t('common.edit'),
onClick: handleEdit.bind(null, row),
type: 'link',
},
{
auth: ['mes:qc-defect:delete'],
danger: true,
icon: ACTION_ICON.DELETE,
ifShow: !isReadonly,
label: $t('common.delete'),
popConfirm: {
confirm: handleDelete.bind(null, row),
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
},
type: 'link',
},
]"
/>
</template>
</Grid>
</Modal>
</template>

View File

@ -0,0 +1,2 @@
export { default as DefectRecordInlineForm } from './defect-record-inline-form.vue';
export { default as DefectRecordInlineList } from './defect-record-inline-list.vue';

View File

@ -0,0 +1,2 @@
export { default as QcIndicatorResultForm } from './qc-indicator-result-form.vue';
export { default as QcIndicatorResultList } from './qc-indicator-result-list.vue';

View File

@ -0,0 +1,270 @@
<script lang="ts" setup>
import type { MesQcIndicatorResultApi } from '#/api/mes/qc/indicatorresult';
import { computed, h, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import {
Form as AForm,
Button,
Divider,
Input,
InputNumber,
message,
Select,
Textarea,
} from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import {
createIndicatorResult,
getIndicatorResultDetail,
updateIndicatorResult,
} from '#/api/mes/qc/indicatorresult';
import { $t } from '#/locales';
import {
MesAutoCodeRuleCode,
MesQcResultValueType,
} from '#/views/mes/utils/constants';
interface OpenData {
formType: 'create' | 'update';
id?: number;
qcId: number;
qcType: number;
}
const emit = defineEmits(['success']);
const formType = ref<'create' | 'update'>('create');
const items = ref<MesQcIndicatorResultApi.IndicatorResultDetail[]>([]);
const ctxData = ref<OpenData>();
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,
},
wrapperClass: 'grid-cols-2',
layout: 'horizontal',
// TODO @AI data.ts
// TODO @AI
schema: [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'code',
label: '样品编号',
component: 'Input',
componentProps: {
placeholder: '请输入样品编号',
},
rules: 'required',
suffix: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
try {
const code = await generateAutoCode(
MesAutoCodeRuleCode.QC_INDICATOR_RESULT_CODE,
);
await formApi.setFieldValue('code', code);
} catch (error) {
console.error(error);
}
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'sn',
label: '物资 SN',
component: 'Input',
componentProps: {
placeholder: '请输入物资 SN',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-2',
componentProps: {
placeholder: '请输入备注',
rows: 2,
},
},
],
showDefaultActions: false,
});
/** 解析字典选项字符串value=label;value=label */
function parseValueOptions(spec?: string) {
if (!spec) {
return [];
}
return spec
.split(';')
.map((part) => part.trim())
.filter(Boolean)
.map((part) => {
const [value, label] = part.split('=');
return { value: value ?? '', label: label ?? value ?? '' };
});
}
// TODO @AI
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!ctxData.value) {
return;
}
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
try {
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 payload: MesQcIndicatorResultApi.IndicatorResult = {
...head,
qcId: ctxData.value.qcId,
qcType: ctxData.value.qcType,
items: submitItems,
};
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;
items.value = [];
return;
}
const data = modalApi.getData<OpenData>();
ctxData.value = data;
formType.value = data.formType;
modalApi.lock();
try {
const detail = await getIndicatorResultDetail(
data.qcId,
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,
}));
await formApi.setValues({
id: detail.id,
code: detail.code,
sn: detail.sn,
remark: detail.remark,
});
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<div class="px-4">
<Form />
<Divider>检测值</Divider>
<!-- TODO @AI这里可以改成更大化使用 schema 方式么 -->
<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="parseValueOptions(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>
</Modal>
</template>

View File

@ -0,0 +1,156 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesQcIndicatorResultApi } from '#/api/mes/qc/indicatorresult';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteIndicatorResult,
getIndicatorResultPage,
} from '#/api/mes/qc/indicatorresult';
import { $t } from '#/locales';
import QcIndicatorResultForm from './qc-indicator-result-form.vue';
const props = defineProps<{
qcId: number;
qcType: number;
readonly?: boolean;
}>();
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: QcIndicatorResultForm,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 新增检测结果 */
function handleCreate() {
formModalApi
.setData({
formType: 'create',
qcId: props.qcId,
qcType: props.qcType,
})
.open();
}
/** 编辑检测结果 */
function handleEdit(row: MesQcIndicatorResultApi.IndicatorResult) {
formModalApi
.setData({
formType: 'update',
id: row.id,
qcId: props.qcId,
qcType: props.qcType,
})
.open();
}
/** 删除检测结果 */
async function handleDelete(row: MesQcIndicatorResultApi.IndicatorResult) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.code]),
duration: 0,
});
try {
await deleteIndicatorResult(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.code]));
handleRefresh();
} finally {
hideLoading();
}
}
// TODO @AI data.ts
// TODO @AI
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: [
{ field: 'code', title: '样品编号', width: 200 },
{ field: 'sn', title: '物资SN', minWidth: 200 },
{ field: 'remark', title: '备注', minWidth: 200 },
{
title: '操作',
width: 150,
fixed: 'right',
slots: { default: 'actions' },
},
],
height: 360,
proxyConfig: {
ajax: {
query: async ({ page }) => {
if (!props.qcId) {
return { list: [], total: 0 };
}
return await getIndicatorResultPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
qcId: props.qcId,
qcType: props.qcType,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
} as VxeTableGridOptions<MesQcIndicatorResultApi.IndicatorResult>,
});
</script>
<template>
<div>
<FormModal @success="handleRefresh" />
<Grid table-title="">
<template v-if="!readonly" #toolbar-tools>
<TableAction
:actions="[
{
label: '新增',
type: 'primary',
icon: ACTION_ICON.ADD,
onClick: handleCreate,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
ifShow: !readonly,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
ifShow: !readonly,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.code]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</div>
</template>

View File

@ -0,0 +1,508 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesQcIqcApi } from '#/api/mes/qc/iqc';
import type { MesQcIqcLineApi } from '#/api/mes/qc/iqc/line';
import { h, markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Button } from 'ant-design-vue';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
import MdItemSelect from '#/views/mes/md/item/components/md-item-select.vue';
import MdVendorSelect from '#/views/mes/md/vendor/components/md-vendor-select.vue';
import { MesAutoCodeRuleCode } from '#/views/mes/utils/constants';
/** 表单类型 */
export type FormType = 'create' | 'detail' | 'update';
/** 新增/修改的表单 */
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'sourceDocId',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'sourceLineId',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'status',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'code',
label: '检验单编号',
component: 'Input',
componentProps: {
placeholder: '请输入检验单编号',
},
rules: 'required',
suffix: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
try {
const code = await generateAutoCode(
MesAutoCodeRuleCode.QC_IQC_CODE,
);
await formApi?.setFieldValue('code', code);
} catch (error) {
console.error(error);
}
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',
label: '检验单名称',
component: 'Input',
componentProps: {
placeholder: '请输入检验单名称',
},
rules: 'required',
},
{
fieldName: 'sourceDocType',
label: '来源单据类型',
component: 'Select',
componentProps: {
disabled: true,
options: getDictOptions(DICT_TYPE.MES_QC_SOURCE_DOC_TYPE, 'number'),
placeholder: '来源单据类型',
},
dependencies: {
triggerFields: ['sourceDocType'],
show: (values) => !!values.sourceDocType,
},
},
{
fieldName: 'sourceDocCode',
label: '来源单据编号',
component: 'Input',
componentProps: {
disabled: true,
placeholder: '来源单据编号',
},
dependencies: {
triggerFields: ['sourceDocType', 'sourceDocId'],
show: (values) => !!values.sourceDocType && !!values.sourceDocId,
},
},
{
fieldName: 'itemId',
label: '产品物料',
component: markRaw(MdItemSelect),
componentProps: {
placeholder: '请选择产品物料',
},
rules: 'selectRequired',
dependencies: {
triggerFields: ['id', 'sourceDocId'],
componentProps: (values) => ({
disabled: !!values.id || !!values.sourceDocId,
placeholder: '请选择产品物料',
}),
},
},
{
fieldName: 'vendorId',
label: '供应商',
component: markRaw(MdVendorSelect),
componentProps: {
placeholder: '请选择供应商',
},
rules: 'selectRequired',
dependencies: {
triggerFields: ['sourceDocId'],
componentProps: (values) => ({
disabled: !!values.sourceDocId,
placeholder: '请选择供应商',
}),
},
},
{
fieldName: 'vendorBatch',
label: '供应商批次号',
component: 'Input',
componentProps: {
placeholder: '请输入供应商批次号',
},
},
{
fieldName: 'receivedQuantity',
label: '本次接收数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
placeholder: '请输入本次接收数量',
},
rules: 'required',
dependencies: {
triggerFields: ['sourceDocId'],
componentProps: (values) => ({
class: '!w-full',
disabled: !!values.sourceDocId,
min: 0,
precision: 2,
placeholder: '请输入本次接收数量',
}),
},
},
{
fieldName: 'qualifiedQuantity',
label: '合格品数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入合格品数量',
},
rules: 'required',
},
{
fieldName: 'unqualifiedQuantity',
label: '不合格品数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入不合格品数量',
},
rules: 'required',
},
{
fieldName: 'receiveDate',
label: '来料日期',
component: 'DatePicker',
componentProps: {
format: 'YYYY-MM-DD',
placeholder: '请选择来料日期',
valueFormat: 'x',
},
rules: 'required',
},
{
fieldName: 'inspectorUserId',
label: '检测人员',
component: 'ApiSelect',
componentProps: {
allowClear: true,
api: getSimpleUserList,
labelField: 'nickname',
placeholder: '请选择检测人员',
valueField: 'id',
},
rules: 'selectRequired',
},
{
fieldName: 'inspectDate',
label: '检测日期',
component: 'DatePicker',
componentProps: {
format: 'YYYY-MM-DD',
placeholder: '请选择检测日期',
valueFormat: 'x',
},
rules: 'required',
},
{
fieldName: 'checkResult',
label: '检测结果',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.MES_QC_CHECK_RESULT, 'number'),
placeholder: '请选择检测结果',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '检验单编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入检验单编号',
},
},
{
fieldName: 'vendorId',
label: '供应商',
component: markRaw(MdVendorSelect),
componentProps: {
placeholder: '请选择供应商',
},
},
{
fieldName: 'vendorBatch',
label: '供应商批次',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入供应商批次号',
},
},
{
fieldName: 'itemId',
label: '产品物料',
component: markRaw(MdItemSelect),
componentProps: {
placeholder: '请选择产品物料',
},
},
{
fieldName: 'checkResult',
label: '检测结果',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.MES_QC_CHECK_RESULT, 'number'),
placeholder: '请选择检测结果',
},
},
{
fieldName: 'receiveDate',
label: '来料日期',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
},
},
{
fieldName: 'inspectDate',
label: '检测日期',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
},
},
{
fieldName: 'inspectorUserId',
label: '检测人员',
component: 'ApiSelect',
componentProps: {
allowClear: true,
api: getSimpleUserList,
labelField: 'nickname',
placeholder: '请选择检测人员',
valueField: 'id',
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesQcIqcApi.Iqc>['columns'] {
return [
{
field: 'code',
title: '来料检验单编号',
width: 160,
slots: { default: 'code' },
},
{
field: 'name',
title: '来料检验单名称',
minWidth: 180,
},
{
field: 'vendorNickname',
title: '供应商简称',
width: 120,
},
{
field: 'vendorBatch',
title: '供应商批次号',
width: 130,
},
{
field: 'itemCode',
title: '产品物料编码',
width: 130,
},
{
field: 'itemName',
title: '产品物料名称',
minWidth: 150,
},
{
field: 'receivedQuantity',
title: '接收数量',
width: 100,
},
{
field: 'checkQuantity',
title: '检测数量',
width: 100,
},
{
field: 'unqualifiedQuantity',
title: '不合格数',
width: 100,
},
{
field: 'checkResult',
title: '检测结果',
width: 110,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_QC_CHECK_RESULT },
},
},
{
field: 'receiveDate',
title: '来料日期',
width: 120,
formatter: 'formatDate',
},
{
field: 'inspectDate',
title: '检测日期',
width: 120,
formatter: 'formatDate',
},
{
field: 'inspectorNickname',
title: '检测人员',
width: 100,
},
{
field: 'status',
title: '单据状态',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_ORDER_STATUS },
},
},
{
title: '操作',
width: 180,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 来料检验行子表的字段 */
export function useLineGridColumns(): VxeTableGridOptions<MesQcIqcLineApi.IqcLine>['columns'] {
return [
{
field: 'indicatorName',
title: '检测项名称',
width: 150,
},
{
field: 'indicatorType',
title: '检测项类型',
width: 120,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_INDICATOR_TYPE },
},
},
{
field: 'tool',
title: '检测工具',
width: 120,
},
{
field: 'checkMethod',
title: '检测要求',
minWidth: 180,
},
{
field: 'standardValue',
title: '标准值',
width: 100,
},
{
field: 'unitMeasureName',
title: '单位',
width: 80,
},
{
field: 'maxThreshold',
title: '误差上限',
width: 100,
},
{
field: 'minThreshold',
title: '误差下限',
width: 100,
},
{
field: 'criticalQuantity',
title: '致命缺陷数',
width: 110,
},
{
field: 'majorQuantity',
title: '严重缺陷数',
width: 110,
},
{
field: 'minorQuantity',
title: '轻微缺陷数',
width: 110,
},
{
field: 'remark',
title: '备注',
minWidth: 150,
},
{
title: '操作',
width: 110,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@ -0,0 +1,167 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesQcIqcApi } from '#/api/mes/qc/iqc';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteIqc, exportIqc, getIqcPage } from '#/api/mes/qc/iqc';
import { $t } from '#/locales';
import { MesQcStatusEnum } from '#/views/mes/utils/constants';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 创建来料检验单 */
function handleCreate() {
formModalApi.setData({ formType: 'create' }).open();
}
/** 查看来料检验单 */
function handleDetail(row: MesQcIqcApi.Iqc) {
formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
/** 编辑来料检验单 */
function handleEdit(row: MesQcIqcApi.Iqc) {
formModalApi.setData({ formType: 'update', id: row.id }).open();
}
/** 删除来料检验单 */
async function handleDelete(row: MesQcIqcApi.Iqc) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.code]),
duration: 0,
});
try {
await deleteIqc(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.code]));
handleRefresh();
} finally {
hideLoading();
}
}
/** 导出表格 */
async function handleExport() {
const data = await exportIqc(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '来料检验单.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getIqcPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesQcIqcApi.Iqc>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【质量】来料检验IQC"
url="https://doc.iocoder.cn/mes/qc/iqc/"
/>
</template>
<FormModal @success="handleRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['来料检验单']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['mes:qc-iqc:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['mes:qc-iqc:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #code="{ row }">
<Button type="link" @click="handleDetail(row)">
{{ row.code }}
</Button>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['mes:qc-iqc:update'],
ifShow: row.status === MesQcStatusEnum.DRAFT,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['mes:qc-iqc:delete'],
ifShow: row.status === MesQcStatusEnum.DRAFT,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.code]),
confirm: handleDelete.bind(null, row),
},
},
{
label: $t('common.detail'),
type: 'link',
icon: ACTION_ICON.VIEW,
auth: ['mes:qc-iqc:query'],
onClick: handleDetail.bind(null, row),
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,244 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { MesQcIqcApi } from '#/api/mes/qc/iqc';
import { computed, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui';
import {
Button,
Descriptions,
message,
Tabs,
} from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createIqc,
finishIqc,
getIqc,
updateIqc,
} from '#/api/mes/qc/iqc';
import { $t } from '#/locales';
import { MesQcStatusEnum, MesQcTypeEnum } from '#/views/mes/utils/constants';
import { QcIndicatorResultList } from '../../indicatorresult/components';
import { useFormSchema } from '../data';
import LineList from './line-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesQcIqcApi.Iqc>();
const subTabsName = ref('line');
const originalSnapshot = ref(''); // finish
const isDetail = computed(() => formType.value === 'detail');
const canFinish = computed(
() =>
formType.value === 'update' &&
formData.value?.status === MesQcStatusEnum.DRAFT,
);
const getTitle = computed(() => {
if (formType.value === 'detail') {
return $t('ui.actionTitle.view', ['来料检验单']);
}
return 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: 110,
},
wrapperClass: 'grid-cols-3',
layout: 'horizontal',
schema: [],
showDefaultActions: false,
});
// TODO @AI handleRefresh
/** 重新加载主表头数据(用于子表变更后刷新缺陷统计) */
async function reloadHead() {
if (!formData.value?.id) {
return;
}
try {
formData.value = await getIqc(formData.value.id);
} catch (error) {
console.error('[IqcForm] reload head failed:', error);
}
}
/** 提交表单 */
async function handleSubmit(): Promise<boolean> {
const { valid } = await formApi.validate();
if (!valid) {
return false;
}
const data = (await formApi.getValues()) as MesQcIqcApi.Iqc;
if (formData.value?.id) {
await updateIqc({ ...data, id: formData.value.id });
formData.value = { ...formData.value, ...data };
} else {
const id = await createIqc(data);
formData.value = {
...data,
id: id as unknown as number,
status: MesQcStatusEnum.DRAFT,
};
await formApi.setFieldValue('id', formData.value.id);
await formApi.setFieldValue('status', formData.value.status);
formType.value = 'update';
}
// id / status finish
originalSnapshot.value = JSON.stringify(await formApi.getValues());
return true;
}
/** 完成检验单:表单有修改时先保存,再调用完成接口 */
async function handleFinish() {
const id = formData.value?.id;
if (!id) {
return;
}
const { valid } = await formApi.validate();
if (!valid) {
return;
}
await confirm({
content: '是否完成来料检验单编制?【完成后将不能更改】',
});
modalApi.lock();
try {
const current = JSON.stringify(await formApi.getValues());
if (current !== originalSnapshot.value) {
const data = (await formApi.getValues()) as MesQcIqcApi.Iqc;
await updateIqc({ ...data, id });
formData.value = { ...formData.value, ...data };
originalSnapshot.value = current;
}
await finishIqc(id);
message.success('完成成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (isDetail.value) {
await modalApi.close();
return;
}
modalApi.lock();
try {
const ok = await handleSubmit();
if (!ok) {
return;
}
message.success($t('ui.actionMessage.operationSuccess'));
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
originalSnapshot.value = '';
return;
}
formApi.setState({ schema: useFormSchema(formApi) });
subTabsName.value = 'line';
//
const data = modalApi.getData<{
formType: FormType;
id?: number;
prefill?: MesQcIqcApi.Iqc;
}>();
formType.value = data.formType;
formApi.setDisabled(formType.value === 'detail');
modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
if (data?.id) {
modalApi.lock();
try {
formData.value = await getIqc(data.id);
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
} else if (data?.prefill) {
//
formData.value = { ...data.prefill };
await formApi.setValues(data.prefill);
}
// finish
originalSnapshot.value = JSON.stringify(await formApi.getValues());
},
});
</script>
<template>
<Modal :title="getTitle" class="w-4/5">
<Form class="mx-4" />
<!-- 缺陷统计只读 -->
<div v-if="formData?.id" class="mx-4 mt-4">
<Descriptions title="缺陷情况" :column="3" bordered size="small">
<Descriptions.Item label="致命缺陷数">
{{ formData.criticalQuantity ?? 0 }}
</Descriptions.Item>
<Descriptions.Item label="严重缺陷数">
{{ formData.majorQuantity ?? 0 }}
</Descriptions.Item>
<Descriptions.Item label="轻微缺陷数">
{{ formData.minorQuantity ?? 0 }}
</Descriptions.Item>
<Descriptions.Item label="致命缺陷率">
{{ formData.criticalRate ?? 0 }}%
</Descriptions.Item>
<Descriptions.Item label="严重缺陷率">
{{ formData.majorRate ?? 0 }}%
</Descriptions.Item>
<Descriptions.Item label="轻微缺陷率">
{{ formData.minorRate ?? 0 }}%
</Descriptions.Item>
</Descriptions>
</div>
<Tabs
v-if="formData?.id"
v-model:active-key="subTabsName"
class="mx-4 mt-4"
>
<Tabs.TabPane key="line" tab="检验项">
<LineList
:form-type="formType"
:iqc-id="formData.id"
@refresh="reloadHead"
/>
</Tabs.TabPane>
<Tabs.TabPane key="result" tab="检测结果">
<QcIndicatorResultList
:qc-id="formData.id"
:qc-type="MesQcTypeEnum.IQC"
:readonly="isDetail"
/>
</Tabs.TabPane>
</Tabs>
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">
<Button v-if="canFinish" type="primary" @click="handleFinish">
完成
</Button>
</div>
</template>
</Modal>
</template>

View File

@ -0,0 +1,97 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesQcIqcLineApi } from '#/api/mes/qc/iqc/line';
import { useVbenModal } from '@vben/common-ui';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getIqcLinePage } from '#/api/mes/qc/iqc/line';
import { MesQcTypeEnum } from '#/views/mes/utils/constants';
import { DefectRecordInlineList } from '../../defectrecord/components';
import { useLineGridColumns } from '../data';
const props = defineProps<{
formType?: string;
iqcId: number;
}>();
const emit = defineEmits<{ refresh: [] }>();
const [DefectModal, defectModalApi] = useVbenModal({
connectedComponent: DefectRecordInlineList,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 缺陷记录变更后,刷新本表格并通知父组件刷新主表统计 */
function handleDefectChanged() {
handleRefresh();
emit('refresh');
}
/** 打开缺陷记录弹窗 */
function handleOpenDefect(row: MesQcIqcLineApi.IqcLine) {
defectModalApi
.setData({
formType: props.formType,
lineId: row.id,
qcId: props.iqcId,
qcType: MesQcTypeEnum.IQC,
})
.open();
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useLineGridColumns(),
height: 360,
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }) => {
if (!props.iqcId) {
return { list: [], total: 0 };
}
return await getIqcLinePage({
iqcId: props.iqcId,
pageNo: page.currentPage,
pageSize: page.pageSize,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
} as VxeTableGridOptions<MesQcIqcLineApi.IqcLine>,
});
</script>
<template>
<div>
<DefectModal @success="handleDefectChanged" />
<Grid table-title="">
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '缺陷列表',
type: 'link',
icon: ACTION_ICON.VIEW,
onClick: handleOpenDefect.bind(null, row),
},
]"
/>
</template>
</Grid>
</div>
</template>