优化混炼示方,新增种类配置

This commit is contained in:
geht
2026-05-25 19:44:14 +08:00
parent c85657d199
commit dc3f305303
34 changed files with 3892 additions and 104 deletions

View File

@@ -13,7 +13,8 @@ import { loadTreeData } from '/@/views/system/category/category.api';
import {
MATERIAL_RAW_AUX_CODE,
materialRawAuxCategoryId,
isMaterialRawAuxSubCategory,
ensureMaterialCategoryContext,
isMaterialMinorCategory,
toIsRubberFlag,
fromIsRubberFlag,
} from '/@/views/system/category/category.constants';
@@ -48,7 +49,7 @@ const schemas: FormSchema[] = [
defaultValue: false,
renderComponentContent: '胶料',
colProps: { span: 24 },
ifShow: ({ values }) => isMaterialRawAuxSubCategory(values.pid),
ifShow: ({ values }) => isMaterialMinorCategory(values.pid),
},
];
@@ -95,7 +96,7 @@ async function ensureMaterialRawAuxCategoryId() {
function normalizeSubmitValues(values: Recordable) {
const payload = { ...values };
payload.isRubber = isMaterialRawAuxSubCategory(payload.pid) ? toIsRubberFlag(payload.isRubber) : '0';
payload.isRubber = isMaterialMinorCategory(payload.pid) ? toIsRubberFlag(payload.isRubber) : '0';
return payload;
}
@@ -104,6 +105,7 @@ const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data
setModalProps({ confirmLoading: false });
isUpdate.value = !!data?.isUpdate;
await ensureMaterialRawAuxCategoryId();
await ensureMaterialCategoryContext();
const tree = await buildPidTree();
if (!tree.length) {
createMessage.warning('未加载到物料分类树,请确认分类字典根编码 XSLMES_MATERIAL 已存在');

View File

@@ -1,16 +1,78 @@
import { ref } from 'vue';
import { defHttp } from '/@/utils/http/axios';
import { getChildListBatch, loadTreeData } from './category.api';
/** MES 物料分类 - 原辅材料编码 */
/** MES 物料分类编码 */
export const MATERIAL_ROOT_CODE = 'XSLMES_MATERIAL';
/** MES 物料分类 - 原辅材料编码(兼容旧逻辑) */
export const MATERIAL_RAW_AUX_CODE = 'XSLMES_MATERIAL_RAW_AUX';
/** 原辅材料分类节点 ID运行时加载 */
/** 原辅材料分类节点 ID运行时加载,兼容旧逻辑 */
export const materialRawAuxCategoryId = ref('');
/** 是否为原辅材料的直接子类 */
/** MES 物料分类根节点 ID运行时加载 */
export const materialRootCategoryId = ref('');
/** MES 物料大类节点 ID 集合(运行时加载) */
export const materialMajorCategoryIds = ref<Set<string>>(new Set());
let materialCategoryContextLoading: Promise<void> | null = null;
/** 加载 MES 物料分类上下文(根节点 + 大类 ID */
export async function ensureMaterialCategoryContext(force = false) {
if (!force && materialRootCategoryId.value && materialMajorCategoryIds.value.size > 0) {
return;
}
if (materialCategoryContextLoading) {
await materialCategoryContextLoading;
return;
}
materialCategoryContextLoading = (async () => {
const rootRes = await defHttp.get(
{ url: '/sys/category/loadOne', params: { field: 'code', val: MATERIAL_ROOT_CODE } },
{ isTransformResponse: false },
);
if (rootRes?.success && rootRes?.result?.id) {
materialRootCategoryId.value = String(rootRes.result.id);
}
const auxRes = await defHttp.get(
{ url: '/sys/category/loadOne', params: { field: 'code', val: MATERIAL_RAW_AUX_CODE } },
{ isTransformResponse: false },
);
if (auxRes?.success && auxRes?.result?.id) {
materialRawAuxCategoryId.value = String(auxRes.result.id);
}
const majors = await loadTreeData({ async: false, pcode: MATERIAL_ROOT_CODE });
const majorIds = new Set<string>();
(Array.isArray(majors) ? majors : []).forEach((node) => {
const nodeKey = node?.key ?? node?.value ?? node?.id;
if (nodeKey != null) {
majorIds.add(String(nodeKey));
}
});
materialMajorCategoryIds.value = majorIds;
})();
try {
await materialCategoryContextLoading;
} finally {
materialCategoryContextLoading = null;
}
}
/** 是否为原辅材料的直接子类(兼容旧逻辑) */
export function isMaterialRawAuxSubCategory(pid?: string) {
return !!materialRawAuxCategoryId.value && pid === materialRawAuxCategoryId.value;
}
/** 是否为 MES 物料小类(父节点为物料大类) */
export function isMaterialMinorCategory(pid?: string) {
if (!pid) {
return false;
}
return materialMajorCategoryIds.value.has(String(pid));
}
/** 表单 Checkbox 布尔值 -> 数据库存储值 */
export function toIsRubberFlag(value: unknown) {
return value === true || value === '1' ? '1' : '0';
@@ -20,3 +82,206 @@ export function toIsRubberFlag(value: unknown) {
export function fromIsRubberFlag(value: unknown) {
return value === '1' || value === 1 || value === true;
}
//update-begin---author:cursor ---date:20260525 for【XSLMES-20260525-A50】统一加载MES物料分类树大类+小类)-----------
export interface MesMaterialCategoryMinorItem {
id: string;
name: string;
majorId: string;
majorName: string;
label: string;
}
export interface MesMaterialCategoryMajorItem {
id: string;
name: string;
minors: MesMaterialCategoryMinorItem[];
}
export interface MesMaterialCategoryTreeLoadResult {
majors: MesMaterialCategoryMajorItem[];
minors: MesMaterialCategoryMinorItem[];
treeNodes: Recordable[];
}
function normalizeCategoryNodeKey(node: Recordable): string {
const key = node?.key ?? node?.value ?? node?.id;
return key != null && String(key) !== '' ? String(key) : '';
}
function normalizeCategoryNodeTitle(node: Recordable): string {
return String(node?.title ?? node?.name ?? '');
}
function normalizeTreeSelectNodes(nodes: unknown): Recordable[] {
if (Array.isArray(nodes)) {
return nodes as Recordable[];
}
if (nodes && typeof nodes === 'object') {
const payload = nodes as Recordable;
if (Array.isArray(payload.result)) {
return payload.result;
}
if (Array.isArray(payload.records)) {
return payload.records;
}
}
return [];
}
/** 加载 MES 物料分类树:根下物料大类 + 各物料小类(供密炼物料/混炼示方选料复用) */
export async function loadMesMaterialCategoryTreeData(): Promise<MesMaterialCategoryTreeLoadResult> {
// 优先 loadTreeRoot与密炼物料列表页一致已验证可用
try {
const treeRes = await loadTreeData({ async: false, pcode: MATERIAL_ROOT_CODE });
const treeResult = buildMesMaterialCategoryTreeFromTreeSelect(normalizeTreeSelectNodes(treeRes));
if (treeResult.minors.length) {
if (!materialRootCategoryId.value) {
await ensureMaterialCategoryContext();
}
return treeResult;
}
} catch {
// 继续走 batch 兜底
}
let rootId = materialRootCategoryId.value;
if (!rootId) {
try {
const root = await defHttp.get<Recordable>({
url: '/sys/category/loadOne',
params: { field: 'code', val: MATERIAL_ROOT_CODE },
});
if (root?.id) {
rootId = String(root.id);
materialRootCategoryId.value = rootId;
}
} catch {
rootId = '';
}
}
if (!rootId) {
return { majors: [], minors: [], treeNodes: [] };
}
let majorRecords: Recordable[] = [];
let minorRecords: Recordable[] = [];
try {
const majorBatch = await getChildListBatch({ parentIds: rootId });
if (majorBatch?.success === false) {
return { majors: [], minors: [], treeNodes: [] };
}
majorRecords = (majorBatch?.result?.records || majorBatch?.records || []) as Recordable[];
if (majorRecords.length) {
const majorIds = majorRecords.map((item) => String(item.id)).filter(Boolean).join(',');
if (majorIds) {
const minorBatch = await getChildListBatch({ parentIds: majorIds });
if (minorBatch?.success !== false) {
minorRecords = (minorBatch?.result?.records || minorBatch?.records || []) as Recordable[];
}
}
}
} catch {
return { majors: [], minors: [], treeNodes: [] };
}
const result = buildMesMaterialCategoryTreeFromRecords(majorRecords, minorRecords);
materialMajorCategoryIds.value = new Set(result.majors.map((item) => item.id));
return result;
}
function buildMesMaterialCategoryTreeFromRecords(majorRecords: Recordable[], minorRecords: Recordable[]) {
const majorMap = new Map<string, { id: string; name: string; minors: MesMaterialCategoryMinorItem[] }>();
majorRecords.forEach((record) => {
const majorId = normalizeCategoryNodeKey(record);
if (!majorId) {
return;
}
majorMap.set(majorId, {
id: majorId,
name: normalizeCategoryNodeTitle(record),
minors: [],
});
});
minorRecords.forEach((record) => {
const majorId = record?.pid != null ? String(record.pid) : '';
const minorId = normalizeCategoryNodeKey(record);
const major = majorMap.get(majorId);
if (!major || !minorId) {
return;
}
major.minors.push({
id: minorId,
name: normalizeCategoryNodeTitle(record),
majorId: major.id,
majorName: major.name,
label: major.name && normalizeCategoryNodeTitle(record)
? `${major.name} / ${normalizeCategoryNodeTitle(record)}`
: normalizeCategoryNodeTitle(record) || major.name,
});
});
const majors: MesMaterialCategoryMajorItem[] = [];
const minors: MesMaterialCategoryMinorItem[] = [];
const treeNodes: Recordable[] = [];
majorMap.forEach((major) => {
if (!major.minors.length) {
return;
}
majors.push({ id: major.id, name: major.name, minors: major.minors });
minors.push(...major.minors);
treeNodes.push({
key: major.id,
title: major.name,
children: major.minors.map((minor) => ({ key: minor.id, title: minor.name })),
});
});
return { majors, minors, treeNodes };
}
function buildMesMaterialCategoryTreeFromTreeSelect(majorRaw: Recordable[]) {
const majors: MesMaterialCategoryMajorItem[] = [];
const minors: MesMaterialCategoryMinorItem[] = [];
const treeNodes: Recordable[] = [];
majorRaw.forEach((major) => {
const majorId = normalizeCategoryNodeKey(major);
const majorName = normalizeCategoryNodeTitle(major);
if (!majorId) {
return;
}
const majorMinors: MesMaterialCategoryMinorItem[] = [];
(Array.isArray(major.children) ? major.children : []).forEach((child) => {
const minorId = normalizeCategoryNodeKey(child);
const minorName = normalizeCategoryNodeTitle(child);
if (!minorId) {
return;
}
const item: MesMaterialCategoryMinorItem = {
id: minorId,
name: minorName,
majorId,
majorName,
label: majorName && minorName ? `${majorName} / ${minorName}` : minorName || majorName,
};
majorMinors.push(item);
minors.push(item);
});
if (!majorMinors.length) {
return;
}
majors.push({ id: majorId, name: majorName, minors: majorMinors });
treeNodes.push({
key: majorId,
title: majorName,
children: majorMinors.map((minor) => ({ key: minor.id, title: minor.name })),
});
});
materialMajorCategoryIds.value = new Set(majors.map((item) => item.id));
return { majors, minors, treeNodes };
}
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A50】统一加载MES物料分类树大类+小类)-----------

View File

@@ -1,6 +1,6 @@
import { BasicColumn } from '/@/components/Table';
import { FormSchema } from '/@/components/Table';
import { isMaterialRawAuxSubCategory } from './category.constants';
import { isMaterialMinorCategory } from './category.constants';
export const columns: BasicColumn[] = [
{
@@ -81,6 +81,6 @@ export const formSchema: FormSchema[] = [
defaultValue: false,
renderComponentContent: '胶料',
colProps: { span: 24 },
show: ({ values }) => isMaterialRawAuxSubCategory(values.pid),
show: ({ values }) => isMaterialMinorCategory(values.pid),
},
];

View File

@@ -13,7 +13,8 @@
import {
MATERIAL_RAW_AUX_CODE,
materialRawAuxCategoryId,
isMaterialRawAuxSubCategory,
ensureMaterialCategoryContext,
isMaterialMinorCategory,
toIsRubberFlag,
fromIsRubberFlag,
} from '../category.constants';
@@ -40,7 +41,7 @@
function normalizeSubmitValues(values: Recordable) {
const payload = { ...values };
payload.isRubber = isMaterialRawAuxSubCategory(payload.pid) ? toIsRubberFlag(payload.isRubber) : '0';
payload.isRubber = isMaterialMinorCategory(payload.pid) ? toIsRubberFlag(payload.isRubber) : '0';
return payload;
}
//表单配置
@@ -64,6 +65,7 @@
setModalProps({ confirmLoading: false, minHeight: 80 });
isUpdate.value = !!data?.isUpdate;
await ensureMaterialRawAuxCategoryId();
await ensureMaterialCategoryContext();
// 代码逻辑说明: 分类字典data.record为空报错------------
isSubAdd.value = !data?.isUpdate && data.record && data.record.id;
if (data?.record) {

View File

@@ -23,6 +23,7 @@
columns: [
{ title: '设备名称', dataIndex: 'equipmentName', width: 160 },
{ title: '设备编号', dataIndex: 'equipmentCode', width: 140 },
{ title: '有效体积', dataIndex: 'effectiveVolume', width: 100 },
{ title: '设备类别', dataIndex: 'equipmentCategoryName', width: 120 },
{ title: '设备类型', dataIndex: 'equipmentTypeName', width: 120 },
],

View File

@@ -79,7 +79,7 @@
}
}
if (!row?.id) {
emit('select', { equipmentLedgerId: '', equipmentName: '', equipmentCode: '' });
emit('select', { equipmentLedgerId: '', equipmentName: '', equipmentCode: '', effectiveVolume: '' });
closeModal();
return;
}
@@ -87,6 +87,7 @@
equipmentLedgerId: row.id,
equipmentName: row.equipmentName || '',
equipmentCode: row.equipmentCode || '',
effectiveVolume: row.effectiveVolume || '',
});
closeModal();
}

View File

@@ -47,5 +47,8 @@ export const saveOrUpdate = (params, isUpdate) => defHttp.post({ url: isUpdate ?
//update-begin---author:cursor ---date:20260522 for【XSLMES-20260522-A38】配合示方生成混炼示方-----------
export const buildMixingGeneratePreview = (params) =>
defHttp.get({ url: Api.buildMixingGeneratePreview, params }, { successMessageMode: 'none' });
export const generateMixingSpec = (params) => defHttp.post({ url: Api.generateMixingSpec, params });
//update-begin---author:cursor ---date:20260525 for【XSLMES-20260525-A45】批量生成混炼示方延长超时避免误报失败-----------
export const generateMixingSpec = (params) =>
defHttp.post({ url: Api.generateMixingSpec, params, timeout: 120 * 1000 });
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A45】批量生成混炼示方延长超时避免误报失败-----------
//update-end---author:cursor ---date:20260522 for【XSLMES-20260522-A38】配合示方生成混炼示方-----------

View File

@@ -0,0 +1,36 @@
import { defHttp } from '/@/utils/http/axios';
enum Api {
list = '/xslmes/mesXslMixerMaterialKindCfg/list',
save = '/xslmes/mesXslMixerMaterialKindCfg/add',
edit = '/xslmes/mesXslMixerMaterialKindCfg/edit',
addBatch = '/xslmes/mesXslMixerMaterialKindCfg/addBatch',
expandLines = '/xslmes/mesXslMixerMaterialKindCfg/expandLines',
deleteOne = '/xslmes/mesXslMixerMaterialKindCfg/delete',
deleteBatch = '/xslmes/mesXslMixerMaterialKindCfg/deleteBatch',
importExcel = '/xslmes/mesXslMixerMaterialKindCfg/importExcel',
exportXls = '/xslmes/mesXslMixerMaterialKindCfg/exportXls',
queryById = '/xslmes/mesXslMixerMaterialKindCfg/queryById',
}
export const list = (params) => defHttp.get({ url: Api.list, params });
export const queryById = (params) => defHttp.get({ url: Api.queryById, params });
export const expandLines = (params) => defHttp.get({ url: Api.expandLines, params });
export const addBatch = (params) => defHttp.post({ url: Api.addBatch, params }, { successMessageMode: 'none' });
export const deleteOne = (params, handleSuccess) =>
defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => handleSuccess());
export const batchDelete = (params, handleSuccess) =>
defHttp.delete({ url: Api.deleteBatch, params }, { joinParamsToUrl: true }).then(() => handleSuccess());
export const saveOrUpdate = (params, isUpdate) => {
const url = isUpdate ? Api.edit : Api.save;
return defHttp.post({ url, params }, { successMessageMode: 'none' });
};
export const getExportUrl = Api.exportXls;
export const getImportUrl = Api.importExcel;

View File

@@ -0,0 +1,156 @@
import { BasicColumn, FormSchema } from '/@/components/Table';
import { JVxeColumn, JVxeTypes } from '/@/components/jeecg/JVxeTable/types';
import { list as dictList } from '/@/views/system/dict/dict.api';
import { loadTreeData } from '/@/api/common/api';
export const SOURCE_TYPE_OPTIONS = [
{ label: '数据字典', value: 'dict' },
{ label: '分类字典', value: 'category' },
];
export const sourceTypeTextMap: Record<string, string> = {
dict: '数据字典',
category: '分类字典',
};
export const columns: BasicColumn[] = [
{ title: '种类键值', align: 'center', dataIndex: 'kindKey', width: 120 },
{ title: '种类名称', align: 'center', dataIndex: 'kindName', width: 140 },
{
title: '数据源',
align: 'center',
dataIndex: 'sourceType',
width: 100,
customRender: ({ text }) => sourceTypeTextMap[String(text || '')] || text || '',
},
{ title: '根名称', align: 'center', dataIndex: 'sourceRootName', width: 140 },
{ title: '对应分类', align: 'center', dataIndex: 'categoryRefName', width: 140 },
{ title: '优先级', align: 'center', dataIndex: 'priority', width: 80 },
{ title: '租户ID', align: 'center', dataIndex: 'tenantId', width: 80, defaultHidden: true },
{ title: '创建时间', align: 'center', dataIndex: 'createTime', width: 165 },
];
export const searchFormSchema: FormSchema[] = [
{ label: '种类键值', field: 'kindKey', component: 'Input', colProps: { span: 6 } },
{ label: '种类名称', field: 'kindName', component: 'Input', colProps: { span: 6 } },
{
label: '数据源',
field: 'sourceType',
component: 'Select',
componentProps: { options: SOURCE_TYPE_OPTIONS, allowClear: true },
colProps: { span: 6 },
},
{ label: '根编码', field: 'sourceRootCode', component: 'Input', colProps: { span: 6 } },
];
export const batchFormSchema: FormSchema[] = [
{
label: '数据源',
field: 'sourceType',
component: 'Select',
required: true,
defaultValue: 'category',
componentProps: { options: SOURCE_TYPE_OPTIONS },
},
{
label: '数据字典',
field: 'dictRootCode',
component: 'ApiSelect',
required: true,
ifShow: ({ values }) => values.sourceType === 'dict',
componentProps: {
api: () => dictList({ pageNo: 1, pageSize: 500 }),
resultField: 'records',
labelField: 'dictName',
valueField: 'dictCode',
showSearch: true,
placeholder: '请选择数据字典根',
},
},
{
label: '分类字典',
field: 'categoryRootCode',
component: 'ApiSelect',
required: true,
ifShow: ({ values }) => values.sourceType === 'category',
componentProps: {
api: loadTreeData,
params: { async: false, pcode: '0' },
resultField: '',
labelField: 'title',
valueField: 'code',
showSearch: true,
placeholder: '请选择分类字典根',
},
},
];
export const editFormSchema: FormSchema[] = [
{ label: '', field: 'id', component: 'Input', show: false },
{ label: '', field: 'sourceType', component: 'Input', show: false },
{ label: '', field: 'sourceRootCode', component: 'Input', show: false },
{ label: '', field: 'sourceRootName', component: 'Input', show: false },
{ label: '', field: 'categoryRefId', component: 'Input', show: false },
{ label: '', field: 'categoryRefCode', component: 'Input', show: false },
{
label: '种类键值',
field: 'kindKey',
component: 'Input',
componentProps: { disabled: true },
},
{
label: '种类名称',
field: 'kindName',
component: 'Input',
required: true,
},
{
label: '对应分类',
field: 'categoryRefName',
component: 'Input',
componentProps: { disabled: true },
},
{
label: '优先级',
field: 'priority',
component: 'InputNumber',
required: true,
componentProps: { min: 0, precision: 0, style: { width: '100%' } },
},
{
label: '租户ID',
field: 'tenantId',
component: 'InputNumber',
componentProps: { disabled: true, style: { width: '100%' } },
},
];
export const batchJVxeColumns: JVxeColumn[] = [
{ title: '', key: 'categoryRefId', type: JVxeTypes.hidden },
{ title: '', key: 'categoryRefCode', type: JVxeTypes.hidden },
{ title: '', key: 'sourceType', type: JVxeTypes.hidden },
{ title: '', key: 'sourceRootCode', type: JVxeTypes.hidden },
{ title: '', key: 'sourceRootName', type: JVxeTypes.hidden },
{ title: '', key: 'tenantId', type: JVxeTypes.hidden },
{ title: '种类键值', key: 'kindKey', type: JVxeTypes.normal, width: 200, minWidth: 160, disabled: true },
{ title: '种类名称', key: 'kindName', type: JVxeTypes.input, width: 180, minWidth: 140 },
{ title: '对应分类', key: 'categoryRefName', type: JVxeTypes.normal, width: 180, minWidth: 140, disabled: true },
{
title: '优先级',
key: 'priority',
type: JVxeTypes.inputNumber,
width: 110,
minWidth: 90,
align: 'center',
validateRules: [{ required: true, message: '请输入优先级' }],
},
];
export const superQuerySchema = {
kindKey: { title: '种类键值', order: 0, view: 'text' },
kindName: { title: '种类名称', order: 1, view: 'text' },
sourceType: { title: '数据源', order: 2, view: 'list', enum: SOURCE_TYPE_OPTIONS },
sourceRootCode: { title: '根编码', order: 3, view: 'text' },
categoryRefName: { title: '对应分类', order: 4, view: 'text' },
priority: { title: '优先级', order: 5, view: 'number' },
};

View File

@@ -0,0 +1,162 @@
<template>
<div>
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<template #tableTitle>
<a-button
type="primary"
v-auth="'xslmes:mes_xsl_mixer_material_kind_cfg:add'"
@click="handleBatchAdd"
preIcon="ant-design:plus-outlined"
>
新增
</a-button>
<a-button
type="primary"
v-auth="'xslmes:mes_xsl_mixer_material_kind_cfg:exportXls'"
preIcon="ant-design:export-outlined"
@click="onExportXls"
>
导出
</a-button>
<j-upload-button
type="primary"
v-auth="'xslmes:mes_xsl_mixer_material_kind_cfg:importExcel'"
preIcon="ant-design:import-outlined"
@click="onImportXls"
>
导入
</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined" />
删除
</a-menu-item>
</a-menu>
</template>
<a-button v-auth="'xslmes:mes_xsl_mixer_material_kind_cfg:deleteBatch'">
批量操作
<Icon icon="mdi:chevron-down" />
</a-button>
</a-dropdown>
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
</template>
<template #action="{ record }">
<TableAction
:actions="[
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: 'xslmes:mes_xsl_mixer_material_kind_cfg:edit',
},
]"
:dropDownActions="getDropDownAction(record)"
/>
</template>
</BasicTable>
<MesXslMixerMaterialKindCfgBatchModal @register="registerBatchModal" @success="handleSuccess" />
<MesXslMixerMaterialKindCfgEditModal @register="registerEditModal" @success="handleSuccess" />
</div>
</template>
<script lang="ts" name="xslmes-mesXslMixerMaterialKindCfg" setup>
import { reactive } from 'vue';
import { BasicTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage';
import Icon from '/@/components/Icon';
import MesXslMixerMaterialKindCfgBatchModal from './components/MesXslMixerMaterialKindCfgBatchModal.vue';
import MesXslMixerMaterialKindCfgEditModal from './components/MesXslMixerMaterialKindCfgEditModal.vue';
import { columns, searchFormSchema, superQuerySchema } from './MesXslMixerMaterialKindCfg.data';
import { batchDelete, deleteOne, getExportUrl, getImportUrl, list } from './MesXslMixerMaterialKindCfg.api';
const queryParam = reactive<any>({});
const [registerBatchModal, { openModal: openBatchModal }] = useModal();
const [registerEditModal, { openModal: openEditModal }] = useModal();
const { tableContext, onExportXls, onImportXls } = useListPage({
tableProps: {
title: '密炼物料种类配置',
api: list,
columns,
canResize: true,
formConfig: {
schemas: searchFormSchema,
labelWidth: 100,
autoSubmitOnEnter: true,
showAdvancedButton: true,
},
actionColumn: {
title: '操作',
dataIndex: 'action',
width: 160,
fixed: 'right',
slots: { customRender: 'action' },
},
beforeFetch: (params) => Object.assign(params, queryParam),
},
exportConfig: {
name: '密炼物料种类配置',
url: getExportUrl,
params: queryParam,
},
importConfig: {
url: getImportUrl,
success: handleSuccess,
},
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
const superQueryConfig = reactive(superQuerySchema);
function handleSuperQuery(params) {
Object.keys(params).forEach((k) => {
queryParam[k] = params[k];
});
reload();
}
function handleBatchAdd() {
openBatchModal(true, { showFooter: true });
}
function handleEdit(record: Recordable) {
openEditModal(true, { record, isUpdate: true, showFooter: true });
}
function handleDetail(record: Recordable) {
openEditModal(true, { record, isUpdate: true, showFooter: false });
}
async function handleDelete(record) {
await deleteOne({ id: record.id }, handleSuccess);
}
async function batchHandleDelete() {
await batchDelete({ ids: selectedRowKeys.value.join(',') }, handleSuccess);
}
function handleSuccess() {
selectedRowKeys.value = [];
reload();
}
function getDropDownAction(record) {
return [
{
label: '详情',
onClick: handleDetail.bind(null, record),
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
placement: 'topLeft',
},
auth: 'xslmes:mes_xsl_mixer_material_kind_cfg:delete',
},
];
}
</script>

View File

@@ -0,0 +1,189 @@
<template>
<BasicModal
v-bind="$attrs"
destroyOnClose
title="新增密炼物料种类配置"
:width="'92%'"
:defaultFullscreen="true"
wrapClassName="mes-xsl-mixer-material-kind-batch-modal"
@register="registerModal"
@ok="handleSubmit"
>
<div class="batch-modal-body">
<BasicForm @register="registerForm">
<template #expandAction>
<a-button type="primary" :loading="expanding" @click="handleExpand">带出明细</a-button>
</template>
</BasicForm>
<a-divider orientation="left">种类配置明细</a-divider>
<div class="batch-table-wrap">
<JVxeTable
v-if="tableReady"
ref="lineTableRef"
toolbar
row-number
rowSelection
keep-source
:insert-row="false"
:max-height="tableMaxHeight"
:loading="lineLoading"
:columns="batchJVxeColumns"
:dataSource="lineDataSource"
:toolbar-config="{ btn: ['remove'] }"
:add-btn-cfg="{ enabled: false }"
/>
</div>
</div>
</BasicModal>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import type { JVxeTableInstance } from '/@/components/jeecg/JVxeTable/types';
import { useMessage } from '/@/hooks/web/useMessage';
import { useUserStore } from '/@/store/modules/user';
import { batchFormSchema, batchJVxeColumns } from '../MesXslMixerMaterialKindCfg.data';
import { addBatch, expandLines } from '../MesXslMixerMaterialKindCfg.api';
const emit = defineEmits(['register', 'success']);
const { createMessage } = useMessage();
const userStore = useUserStore();
const tableReady = ref(false);
const lineLoading = ref(false);
const expanding = ref(false);
const lineDataSource = ref<Recordable[]>([]);
const lineTableRef = ref<JVxeTableInstance>();
const tableMaxHeight = ref(560);
function refreshTableHeight() {
// 预留顶部表单、标题与底部按钮区域,表格尽量占满可视区
tableMaxHeight.value = Math.max(420, window.innerHeight - 300);
}
const [registerForm, { resetFields, validate, getFieldsValue }] = useForm({
labelWidth: 110,
schemas: [
...batchFormSchema,
{
label: '',
field: 'expandAction',
component: 'Input',
slot: 'expandAction',
colProps: { span: 24 },
},
],
showActionButtonGroup: false,
baseColProps: { span: 12 },
});
const [registerModal, { setModalProps, closeModal }] = useModalInner(async () => {
tableReady.value = false;
lineDataSource.value = [];
refreshTableHeight();
await resetFields();
setModalProps({ confirmLoading: false, showCancelBtn: true, showOkBtn: true });
tableReady.value = true;
});
onMounted(() => {
refreshTableHeight();
window.addEventListener('resize', refreshTableHeight);
});
onUnmounted(() => {
window.removeEventListener('resize', refreshTableHeight);
});
function resolveTenantId() {
const tenant = userStore.getTenant;
if (tenant == null || tenant === '') {
return undefined;
}
const num = Number(tenant);
return Number.isNaN(num) ? undefined : num;
}
function resolveSourceRootCode(values: Recordable) {
if (values.sourceType === 'dict') {
return values.dictRootCode;
}
return values.categoryRootCode;
}
async function handleExpand() {
try {
const values = await validate();
const sourceRootCode = resolveSourceRootCode(values);
if (!sourceRootCode) {
createMessage.warning(values.sourceType === 'dict' ? '请选择数据字典根' : '请选择分类字典根');
return;
}
expanding.value = true;
const tenantId = resolveTenantId();
const raw = await expandLines({
sourceType: values.sourceType,
sourceRootCode,
tenantId,
});
const rows = Array.isArray(raw) ? raw : (raw as Recordable)?.result ?? [];
if (!rows.length) {
createMessage.warning('未带出任何明细,可能均已配置');
return;
}
const mergedTenant = tenantId ?? rows[0]?.tenantId;
lineDataSource.value = rows.map((row) => ({
...row,
tenantId: mergedTenant,
}));
createMessage.success(`已带出 ${rows.length} 条明细`);
} catch (e: any) {
createMessage.error(e?.message || '带出明细失败');
} finally {
expanding.value = false;
}
}
async function handleSubmit() {
const lineRef = lineTableRef.value as any;
const tableData = (lineRef?.getTableData?.() || lineDataSource.value || []) as Recordable[];
if (!tableData.length) {
createMessage.warning('请先选择根字典/分类并带出明细');
return;
}
const errMap = await lineRef?.validateTable?.();
if (errMap) {
createMessage.warning('请完善明细中的种类名称与优先级');
return;
}
const tenantId = resolveTenantId();
const payload = tableData.map((row) => ({
...row,
tenantId: row.tenantId ?? tenantId,
}));
try {
setModalProps({ confirmLoading: true });
await addBatch(payload);
createMessage.success('新增成功');
closeModal();
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>
<style lang="less" scoped>
.batch-modal-body {
display: flex;
flex-direction: column;
min-height: calc(100vh - 200px);
}
.batch-table-wrap {
flex: 1;
min-height: 420px;
}
</style>

View File

@@ -0,0 +1,52 @@
<template>
<BasicModal v-bind="$attrs" destroyOnClose :title="title" width="640px" @register="registerModal" @ok="handleSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { computed, ref, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { useMessage } from '/@/hooks/web/useMessage';
import { editFormSchema } from '../MesXslMixerMaterialKindCfg.data';
import { saveOrUpdate } from '../MesXslMixerMaterialKindCfg.api';
const emit = defineEmits(['register', 'success']);
const { createMessage } = useMessage();
const isUpdate = ref(true);
const isDetail = ref(false);
const [registerForm, { resetFields, setFieldsValue, validate, setProps }] = useForm({
labelWidth: 110,
schemas: editFormSchema,
showActionButtonGroup: false,
baseColProps: { span: 24 },
});
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
await resetFields();
setModalProps({ confirmLoading: false, showCancelBtn: !!data?.showFooter, showOkBtn: !!data?.showFooter });
isUpdate.value = !!data?.isUpdate;
isDetail.value = !data?.showFooter;
if (data?.record) {
await setFieldsValue({ ...data.record });
}
setProps({ disabled: !data?.showFooter });
});
const title = computed(() => (!unref(isUpdate) ? '新增' : unref(isDetail) ? '详情' : '编辑'));
async function handleSubmit() {
try {
const values = await validate();
setModalProps({ confirmLoading: true });
await saveOrUpdate(values, true);
createMessage.success('编辑成功');
closeModal();
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>

View File

@@ -18,7 +18,10 @@ export const getExportUrl = Api.exportXls;
export const getImportUrl = Api.importExcel;
export const list = (params) => defHttp.get({ url: Api.list, params });
export const queryById = (params) => defHttp.get({ url: Api.queryById, params });
export const saveOrUpdate = (params, isUpdate) => defHttp.post({ url: isUpdate ? Api.edit : Api.save, params });
//update-begin---author:cursor ---date:20260525 for【XSLMES-20260525-A45】混炼示方主子表保存延长超时避免误报失败-----------
export const saveOrUpdate = (params, isUpdate) =>
defHttp.post({ url: isUpdate ? Api.edit : Api.save, params, timeout: 60 * 1000 });
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A45】混炼示方主子表保存延长超时避免误报失败-----------
export const queryIssueNumberOptions = (params) => defHttp.get({ url: Api.queryIssueNumberOptions, params });
export const queryPurposeOptions = (params) => defHttp.get({ url: Api.queryPurposeOptions, params });

View File

@@ -91,10 +91,27 @@ export const materialColumns: JVxeColumn[] = [
{ title: '物料大类', key: 'materialMajor', type: JVxeTypes.input, width: 100, minWidth: MIXING_MATERIAL_MIN_COLUMN_WIDTH },
{ title: '物料小类', key: 'materialMinor', type: JVxeTypes.input, width: 120, minWidth: MIXING_MATERIAL_MIN_COLUMN_WIDTH },
{ title: '种类', key: 'materialKind', type: JVxeTypes.input, width: 80, minWidth: MIXING_MATERIAL_MIN_COLUMN_WIDTH },
{ title: '密炼物料名称', key: 'mixerMaterialName', type: JVxeTypes.input, width: 160, minWidth: MIXING_MATERIAL_MIN_COLUMN_WIDTH },
{
title: '密炼物料名称',
key: 'mixerMaterialName',
type: JVxeTypes.slot,
slotName: 'mixerMaterialNameSlot',
width: 160,
minWidth: MIXING_MATERIAL_MIN_COLUMN_WIDTH,
},
{ title: '密炼物料描述', key: 'mixerMaterialDesc', type: JVxeTypes.input, width: 220, minWidth: MIXING_MATERIAL_MIN_COLUMN_WIDTH },
{ title: '单重', key: 'unitWeight', type: JVxeTypes.inputNumber, width: 72, minWidth: MIXING_MATERIAL_MIN_COLUMN_WIDTH, align: 'center' },
{ title: '累计', key: 'accumWeight', type: JVxeTypes.inputNumber, width: 72, minWidth: MIXING_MATERIAL_MIN_COLUMN_WIDTH, align: 'center' },
//update-begin---author:cursor ---date:20260525 for【XSLMES-20260525-A41】累计列按种类分组合计只读展示-----------
{
title: '累计',
key: 'accumWeight',
type: JVxeTypes.inputNumber,
width: 72,
minWidth: MIXING_MATERIAL_MIN_COLUMN_WIDTH,
align: 'center',
disabled: true,
},
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A41】累计列按种类分组合计只读展示-----------
{ title: '顺序', key: 'seqNo', type: JVxeTypes.inputNumber, width: 64, minWidth: MIXING_MATERIAL_MIN_COLUMN_WIDTH, align: 'center' },
];
@@ -196,6 +213,280 @@ export function calcMixingMaterialTableWidth(columns: JVxeColumn[], widthMap: Re
}
//update-end---author:cursor ---date:20260522 for【XSLMES-20260522-A17】橡胶及配合剂明细列展示设置-----------
//update-begin---author:cursor ---date:20260525 for【XSLMES-20260525-A41】橡胶及配合剂明细累计按种类分组合计-----------
/** 是否为有效明细行(参与种类分组) */
function isMixingMaterialDataRow(row: Recordable): boolean {
if (!row) {
return false;
}
return !!(row.mixerMaterialName || row.materialKind || row.unitWeight != null && row.unitWeight !== '');
}
/** 规范化种类字段,用于连续行分组 */
function normalizeMixingMaterialKind(row: Recordable): string {
const kind = row?.materialKind;
return kind != null && String(kind).trim() !== '' ? String(kind).trim() : '';
}
/** 按种类连续分组,累计写入每组最后一行 */
export function fillMixingMaterialAccumWeight(rows: Recordable[] = []): Recordable[] {
if (!rows?.length) {
return rows;
}
let index = 0;
while (index < rows.length) {
const current = rows[index];
if (!isMixingMaterialDataRow(current)) {
current.accumWeight = null;
index++;
continue;
}
const kind = normalizeMixingMaterialKind(current);
let groupEnd = index;
let sum = 0;
while (groupEnd < rows.length) {
const row = rows[groupEnd];
if (!isMixingMaterialDataRow(row) || normalizeMixingMaterialKind(row) !== kind) {
break;
}
const weight = toMixingMaterialNumber(row.unitWeight);
if (weight != null) {
sum += weight;
}
groupEnd++;
}
for (let rowIndex = index; rowIndex < groupEnd; rowIndex++) {
rows[rowIndex].accumWeight =
rowIndex === groupEnd - 1 && sum !== 0 ? roundMixingMaterialNumber(sum) : null;
}
index = groupEnd;
}
return rows;
}
/** 安全解析明细数值,避免字符串拼接 */
function toMixingMaterialNumber(value: unknown): number | null {
if (value == null || value === '') {
return null;
}
const num = Number(value);
return Number.isFinite(num) ? num : null;
}
/** 混炼示方重量小数位(与后端 BigDecimal 精度一致) */
const MIXING_MATERIAL_WEIGHT_SCALE = 6;
/** 重量四舍五入,消除浮点累加误差 */
function roundMixingMaterialNumber(value: number): number {
return Number(value.toFixed(MIXING_MATERIAL_WEIGHT_SCALE));
}
/** 格式化重量展示文本 */
function formatMixingMaterialWeight(value: unknown): string {
const num = toMixingMaterialNumber(value);
if (num == null) {
return '';
}
return String(roundMixingMaterialNumber(num));
}
//update-begin---author:cursor ---date:20260525 for【XSLMES-20260525-A43】换算系数联动明细单重实时计算-----------
/** 规范化换算系数,空值或非正数按 1 处理 */
export function normalizeMixingConvertFactor(factor: unknown): number {
const num = toMixingMaterialNumber(factor);
if (num == null || num <= 0) {
return 1;
}
return num;
}
/** 基准单重 × 换算系数 */
export function calcMixingMaterialConvertedWeight(base: unknown, factor: unknown): number | null {
const baseNum = toMixingMaterialNumber(base);
if (baseNum == null) {
return null;
}
return Number((baseNum * normalizeMixingConvertFactor(factor)).toFixed(MIXING_MATERIAL_WEIGHT_SCALE));
}
/** 从当前显示单重反推基准单重 */
export function syncMaterialBaseUnitWeightFromDisplay(row: Recordable, factor: unknown) {
if (!row) {
return;
}
const unit = toMixingMaterialNumber(row.unitWeight);
if (unit == null) {
row.baseUnitWeight = null;
return;
}
row.baseUnitWeight = Number((unit / normalizeMixingConvertFactor(factor)).toFixed(MIXING_MATERIAL_WEIGHT_SCALE));
}
/** 初始化明细行基准单重(编辑加载时由已保存单重反推) */
export function initMaterialBaseUnitWeight(row: Recordable, factor: unknown, force = false) {
if (!isMixingMaterialDataRow(row)) {
row.baseUnitWeight = null;
return;
}
if (!force && toMixingMaterialNumber(row.baseUnitWeight) != null) {
return;
}
syncMaterialBaseUnitWeightFromDisplay(row, factor);
}
/** 批量初始化基准单重 */
export function initMaterialBaseUnitWeights(rows: Recordable[] = [], factor: unknown, force = false) {
for (const row of rows) {
initMaterialBaseUnitWeight(row, factor, force);
}
return rows;
}
/** 按换算系数重算所有明细单重 */
export function applyConvertFactorToMaterialRows(
rows: Recordable[] = [],
factor: unknown,
prevFactor?: unknown,
): Recordable[] {
const nextFactor = normalizeMixingConvertFactor(factor);
const oldFactor = prevFactor != null ? normalizeMixingConvertFactor(prevFactor) : nextFactor;
for (const row of rows) {
if (!isMixingMaterialDataRow(row)) {
continue;
}
let base = toMixingMaterialNumber(row.baseUnitWeight);
if (base == null) {
const unit = toMixingMaterialNumber(row.unitWeight);
if (unit == null) {
continue;
}
base = roundMixingMaterialNumber(unit / oldFactor);
row.baseUnitWeight = base;
}
row.unitWeight = calcMixingMaterialConvertedWeight(base, nextFactor);
}
return rows;
}
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A43】换算系数联动明细单重实时计算-----------
//update-begin---author:cursor ---date:20260525 for【XSLMES-20260525-A46】填充体积按单重/比重/机台有效体积自动计算-----------
/** 解析设备有效体积(支持纯数字或带单位字符串) */
export function parseMixingEffectiveVolume(raw: unknown): number | null {
if (raw == null || raw === '') {
return null;
}
const text = String(raw).trim();
if (!text) {
return null;
}
const direct = toMixingMaterialNumber(text);
if (direct != null && direct > 0) {
return direct;
}
const matched = text.match(/([0-9]+(?:\.[0-9]+)?)/);
if (!matched) {
return null;
}
const parsed = Number(matched[1]);
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}
/** 按段数与比重字段选择本段计算用比重 */
export function resolveMixingSpecificGravity(form: Recordable = {}): number | null {
const motherSg = toMixingMaterialNumber(form.motherRubberSg);
const finalSg = toMixingMaterialNumber(form.finalRubberSg);
const stageCount = String(form.stageCount || '').trim();
const stageMatch = stageCount.match(/^(\d+)\/(\d+)$/);
const isFinalStage = stageMatch ? stageMatch[1] === stageMatch[2] : false;
if (isFinalStage && finalSg != null && finalSg > 0) {
return finalSg;
}
if (motherSg != null && motherSg > 0) {
return motherSg;
}
if (finalSg != null && finalSg > 0) {
return finalSg;
}
return null;
}
/**
* 填充体积(%) = 单重合计 ÷ 比重 ÷ 机台有效体积(L) × 100
* 单重合计已含换算系数,此处不再重复乘换算系数
*/
export function calcMixingFillVolume(totalWeight: unknown, specificGravity: unknown, effectiveVolume: unknown): number | null {
const weight = toMixingMaterialNumber(totalWeight);
const sg = toMixingMaterialNumber(specificGravity);
const volume = parseMixingEffectiveVolume(effectiveVolume);
if (weight == null || weight <= 0 || sg == null || sg <= 0 || volume == null || volume <= 0) {
return null;
}
const materialVolume = weight / sg;
return Number(((materialVolume / volume) * 100).toFixed(6));
}
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A46】填充体积按单重/比重/机台有效体积自动计算-----------
/** 汇总有效明细行的单重合计 */
export function calcMixingMaterialUnitWeightTotal(rows: Recordable[] = []): number | null {
let sum = 0;
let hasAny = false;
for (const row of rows) {
if (!isMixingMaterialDataRow(row)) {
continue;
}
const weight = toMixingMaterialNumber(row.unitWeight);
if (weight != null) {
sum += weight;
hasAny = true;
}
}
return hasAny ? roundMixingMaterialNumber(sum) : null;
}
/** 汇总有效明细行的累计合计(与单重合计一致) */
export function calcMixingMaterialAccumWeightTotal(rows: Recordable[] = []): number | null {
return calcMixingMaterialUnitWeightTotal(rows);
}
export interface MixingMaterialFooterCell {
key: string;
width: number;
text: string;
align?: 'left' | 'center' | 'right';
isLabel?: boolean;
isTotal?: boolean;
}
/** 构建橡胶及配合剂明细底部合计行单元格(列宽与明细表同步) */
export function buildMixingMaterialFooterCells(
columns: JVxeColumn[],
widthMap: Record<string, number>,
totals: { unitWeight?: number | null; accumWeight?: number | null },
): MixingMaterialFooterCell[] {
const unitWeightIndex = columns.findIndex((col) => String(col.key) === 'unitWeight');
const formatTotal = (value: number | null | undefined) => formatMixingMaterialWeight(value);
return columns.map((col, index) => {
const key = String(col.key);
const width = widthMap[key] ?? Number(col.width) ?? 80;
if (key === 'unitWeight') {
return { key, width, text: formatTotal(totals.unitWeight), align: 'center', isTotal: true };
}
if (key === 'accumWeight') {
return { key, width, text: formatTotal(totals.accumWeight), align: 'center', isTotal: true };
}
const isLabelCol = unitWeightIndex > 0 && index === unitWeightIndex - 1;
return {
key,
width,
text: isLabelCol ? '合计' : '',
align: 'center',
isLabel: isLabelCol,
};
});
}
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A41】橡胶及配合剂明细累计按种类分组合计-----------
//update-begin---author:cursor ---date:20260522 for【XSLMES-20260522-A20】明细表默认列宽对齐参考图-----------
/** 混合步骤/下密炼机明细列可缩小到的最小宽度 */
export const MIXING_STEP_MIN_COLUMN_WIDTH = 48;
@@ -412,6 +703,9 @@ export const MIXING_VXE_MINI_HEADER_HEIGHT = 36;
/** vxe mini 行高 */
export const MIXING_VXE_MINI_ROW_HEIGHT = 32;
/** 橡胶及配合剂明细合计行高度(含边框) */
export const MIXING_MATERIAL_FOOTER_ROW_HEIGHT = MIXING_VXE_MINI_ROW_HEIGHT + 1;
//update-begin---author:cursor ---date:20260522 for【XSLMES-20260522-A29】胶料/混合步骤表格高度按行数完整展示-----------
/** 计算橡胶及配合剂明细表格展示高度 */
export function calcMixingMaterialTableHeight(rowCount = MIXING_MATERIAL_VISIBLE_ROW_COUNT) {
@@ -637,3 +931,99 @@ export function ensureMixingDetailRows(rows: Recordable[] = [], defaultCount: nu
}
//update-end---author:cursor ---date:20260522 for【XSLMES-20260522-A39】编辑页明细补齐默认空行与新增一致-----------
//update-end---author:cursor ---date:20260522 for【XSLMES-20260522-A22】明细表默认空行数-----------
//update-begin---author:cursor ---date:20260525 for【XSLMES-20260525-A50】混炼示方密炼物料选料弹窗与种类解析-----------
/** 混炼示方选料弹窗:隐藏的小类 ID 偏好 localStorage 键 */
export const MIXING_MATERIAL_PICKER_HIDDEN_CATEGORY_CACHE_KEY = 'mes_xsl_mixing_spec_material_picker_hidden_categories';
export interface MixingMaterialPickerCategoryItem {
id: string;
name: string;
majorId: string;
majorName: string;
label: string;
}
const mixingMaterialPickerStorage = createLocalStorage();
export function loadMixingMaterialPickerHiddenCategoryIds(): string[] {
const raw = mixingMaterialPickerStorage.get(MIXING_MATERIAL_PICKER_HIDDEN_CATEGORY_CACHE_KEY);
return Array.isArray(raw) ? raw.map(String) : [];
}
export function saveMixingMaterialPickerHiddenCategoryIds(ids: string[]) {
mixingMaterialPickerStorage.set(MIXING_MATERIAL_PICKER_HIDDEN_CATEGORY_CACHE_KEY, ids || []);
}
//update-begin---author:cursor ---date:20260525 for【XSLMES-20260525-A50】选料弹窗小类树为空时重置隐藏配置-----------
/** 过滤无效隐藏项;若全部小类被隐藏则自动重置,避免左侧树只剩「全部小类」 */
export function sanitizeMixingMaterialPickerHiddenCategoryIds(allMinorIds: string[], hidden: string[]) {
const allSet = new Set((allMinorIds || []).map(String));
const filtered = (hidden || []).map(String).filter((id) => allSet.has(id));
if (allSet.size > 0 && filtered.length >= allSet.size) {
return [];
}
return filtered;
}
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A50】选料弹窗小类树为空时重置隐藏配置-----------
/** 解析混炼示方明细种类:小类勾选胶料则显示「胶料」,否则显示小类名 */
export function resolveMixingMaterialKindFromCategory(isRubber?: unknown, minorName?: string) {
if (isRubber === '1' || isRubber === 1 || isRubber === true) {
return '胶料';
}
return minorName != null && String(minorName).trim() !== '' ? String(minorName).trim() : '';
}
//update-begin---author:cursor ---date:20260525 for【XSLMES-20260525-A50】选料弹窗自动/人工称量列与种类映射-----------
/** 与配合示方「自动/人工」列相同字典 */
export const MIXING_MATERIAL_PICKER_WEIGH_MODE_DICT = 'xslmes_formula_spec_weigh_mode';
/** 选料弹窗表格列(隐藏 ERP 编号,新增仅本次有效的自动/人工称量) */
export const mixingMaterialPickerTableColumns: BasicColumn[] = [
{ title: '物料编码', align: 'center', width: 120, dataIndex: 'materialCode' },
{ title: '物料名称', align: 'center', width: 160, dataIndex: 'materialName' },
{ title: '自动/人工称量', align: 'center', width: 132, dataIndex: 'pickerWeighMode' },
{ title: '物料大类', align: 'center', width: 120, dataIndex: 'majorCategoryId_dictText' },
{ title: '物料小类', align: 'center', width: 120, dataIndex: 'minorCategoryId_dictText' },
{ title: '物料描述', align: 'center', width: 180, ellipsis: true, dataIndex: 'materialDesc' },
];
/** 配合示方称量方式 -> 混炼示方种类(与后端 resolveWeighModeMaterialKind 一致) */
export function resolveMixingMaterialKindFromWeighMode(weighMode?: string) {
if (weighMode == null || String(weighMode).trim() === '') {
return '';
}
const normalized = String(weighMode).trim();
const lower = normalized.toLowerCase();
if (lower.startsWith('auto') || normalized.includes('自动')) {
return '自动';
}
if (lower === 'manual' || normalized.includes('人工')) {
return '人工';
}
return '';
}
/** 选料确认时种类:称量方式优先,否则按小类胶料/小类名 */
export function resolveMixingMaterialKindForPicker(weighMode: string | undefined, isRubber?: unknown, minorName?: string) {
const fromWeighMode = resolveMixingMaterialKindFromWeighMode(weighMode);
if (fromWeighMode) {
return fromWeighMode;
}
return resolveMixingMaterialKindFromCategory(isRubber, minorName);
}
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A50】选料弹窗自动/人工称量列与种类映射-----------
/** 选择密炼物料后回填混炼示方橡胶及配合剂明细行 */
export function applyMixingMaterialFromSelection(row: Recordable, material: Recordable, materialKind: string) {
if (!row || !material) {
return;
}
row.mixerMaterialName = material.materialName || material.materialCode || '';
row.mixerMaterialDesc = material.materialDesc || material.materialName || material.materialCode || '';
row.materialMajor = material.majorCategoryId_dictText || '';
row.materialMinor = material.minorCategoryId_dictText || '';
row.materialKind = materialKind || row.materialMinor || '';
}
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A50】混炼示方密炼物料选料弹窗与种类解析-----------

View File

@@ -0,0 +1,221 @@
<template>
<Popover
v-model:open="popoverOpen"
trigger="click"
placement="bottomRight"
:overlayClassName="`${prefixCls}__popover`"
@open-change="handleOpenChange"
>
<template #title>
<div :class="`${prefixCls}__title`">
<Checkbox :indeterminate="indeterminate" :checked="checkAll" :disabled="loading || !allCategoryIds.length" @change="onCheckAllChange">
小类展示
</Checkbox>
</div>
</template>
<template #content>
<Spin :spinning="loading">
<div v-if="!loading && !groupedCategories.length" :class="`${prefixCls}__empty`">
暂无物料小类请确认分类字典 XSLMES_MATERIAL 已配置
</div>
<div v-else :class="`${prefixCls}__list`">
<div v-for="group in groupedCategories" :key="group.majorId" :class="`${prefixCls}__group`">
<div :class="`${prefixCls}__group-title`">{{ group.majorName }}</div>
<div :class="`${prefixCls}__group-options`">
<Checkbox
v-for="opt in group.options"
:key="opt.value"
:checked="isMinorVisible(opt.value)"
@change="(e) => onMinorVisibleChange(opt.value, e.target.checked)"
>
{{ opt.label }}
</Checkbox>
</div>
</div>
</div>
</Spin>
<div :class="`${prefixCls}__footer`">
<a-button size="small" :disabled="loading || !allCategoryIds.length" @click="handleReset">重置</a-button>
<a-button size="small" type="primary" :disabled="loading || !allCategoryIds.length" @click="handleSave">保存</a-button>
</div>
</template>
<a-tooltip title="小类展示设置">
<a-button size="small" class="mixing-material-category-setting-btn" :disabled="loading" @click.stop>
<Icon icon="ant-design:setting-outlined" />
</a-button>
</a-tooltip>
</Popover>
</template>
<script lang="ts" setup>
import { computed, ref, watch, type PropType } from 'vue';
import { Popover, Checkbox, Spin } from 'ant-design-vue';
import type { CheckboxChangeEvent } from 'ant-design-vue/lib/checkbox/interface';
import { Icon } from '/@/components/Icon';
import { useMessage } from '/@/hooks/web/useMessage';
import { saveMixingMaterialPickerHiddenCategoryIds, type MixingMaterialPickerCategoryItem } from '../MesXslMixingSpec.data';
const prefixCls = 'mixing-material-category-setting';
const { createMessage } = useMessage();
const props = defineProps({
categories: {
type: Array as PropType<MixingMaterialPickerCategoryItem[]>,
default: () => [],
},
hiddenCategoryIds: {
type: Array as PropType<string[]>,
default: () => [],
},
loading: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['update:hiddenCategoryIds', 'change']);
const popoverOpen = ref(false);
const draftVisibleIds = ref<string[]>([]);
const allCategoryIds = computed(() => (props.categories || []).map((item) => item.id));
//update-begin---author:cursor ---date:20260525 for【XSLMES-20260525-A50】选料弹窗小类设置按大类分组展示-----------
const groupedCategories = computed(() => {
const groupMap = new Map<string, { majorId: string; majorName: string; options: { label: string; value: string }[] }>();
(props.categories || []).forEach((item) => {
const majorId = item.majorId || 'unknown';
const majorName = item.majorName || '其他';
const group = groupMap.get(majorId) || { majorId, majorName, options: [] };
group.options.push({
label: item.name,
value: item.id,
});
groupMap.set(majorId, group);
});
return Array.from(groupMap.values());
});
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A50】选料弹窗小类设置按大类分组展示-----------
const checkAll = computed(() => {
const all = allCategoryIds.value;
if (!all.length) {
return false;
}
return draftVisibleIds.value.length === all.length;
});
const indeterminate = computed(() => {
const total = allCategoryIds.value.length;
const checked = draftVisibleIds.value.length;
return checked > 0 && checked < total;
});
watch(
() => [props.hiddenCategoryIds, props.categories],
() => {
syncDraftFromHidden();
},
{ deep: true, immediate: true },
);
function syncDraftFromHidden() {
const hidden = new Set((props.hiddenCategoryIds || []).map(String));
draftVisibleIds.value = allCategoryIds.value.filter((id) => !hidden.has(String(id)));
}
function handleOpenChange(open: boolean) {
popoverOpen.value = open;
if (open) {
syncDraftFromHidden();
}
}
function onCheckAllChange(e: CheckboxChangeEvent) {
draftVisibleIds.value = e.target.checked ? [...allCategoryIds.value] : [];
}
//update-begin---author:cursor ---date:20260525 for【XSLMES-20260525-A50】小类展示分组勾选互不覆盖-----------
function isMinorVisible(id: string) {
return draftVisibleIds.value.map(String).includes(String(id));
}
function onMinorVisibleChange(id: string, checked: boolean) {
const visibleSet = new Set(draftVisibleIds.value.map(String));
const key = String(id);
if (checked) {
visibleSet.add(key);
} else {
visibleSet.delete(key);
}
draftVisibleIds.value = allCategoryIds.value.filter((itemId) => visibleSet.has(String(itemId)));
}
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A50】小类展示分组勾选互不覆盖-----------
function handleReset() {
draftVisibleIds.value = [...allCategoryIds.value];
}
function handleSave() {
const visibleSet = new Set(draftVisibleIds.value.map(String));
const hidden = allCategoryIds.value.filter((id) => !visibleSet.has(String(id)));
saveMixingMaterialPickerHiddenCategoryIds(hidden);
emit('update:hiddenCategoryIds', hidden);
emit('change', hidden);
popoverOpen.value = false;
createMessage.success('小类展示设置已保存');
}
</script>
<style lang="less" scoped>
.mixing-material-category-setting-btn {
padding-inline: 8px;
}
</style>
<style lang="less">
.mixing-material-category-setting__popover {
.ant-popover-inner-content {
width: 320px;
max-width: 80vw;
}
}
.mixing-material-category-setting__empty {
padding: 12px 0;
color: var(--text-color-secondary, rgba(0, 0, 0, 0.45));
text-align: center;
}
.mixing-material-category-setting__list {
max-height: 320px;
overflow: auto;
padding: 4px 0 8px;
}
.mixing-material-category-setting__group + .mixing-material-category-setting__group {
margin-top: 12px;
}
.mixing-material-category-setting__group-title {
font-weight: 600;
margin-bottom: 6px;
color: var(--text-color, rgba(0, 0, 0, 0.88));
}
.mixing-material-category-setting__group {
.mixing-material-category-setting__group-options {
display: flex;
flex-direction: column;
gap: 8px;
}
}
.mixing-material-category-setting__footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 8px;
border-top: 1px solid var(--border-color-base, #f0f0f0);
}
</style>

View File

@@ -0,0 +1,406 @@
<template>
<BasicModal
v-bind="$attrs"
title="选择密炼物料"
:width="1180"
:getContainer="getModalContainer"
@register="registerModal"
@ok="handleOk"
>
<div class="mixing-material-picker">
<div class="mixing-material-picker-toolbar">
<a-input
v-model:value="keyword"
allow-clear
placeholder="关键字(物料编码/名称/描述)"
style="width: 280px"
@pressEnter="reloadTable"
/>
<a-button type="primary" @click="reloadTable">搜索</a-button>
<MesXslMixingMaterialCategorySetting
v-model:hiddenCategoryIds="hiddenCategoryIds"
:categories="allMinorCategories"
:loading="treeLoading"
@change="handleCategoryVisibilityChange"
/>
</div>
<div class="mixing-material-picker-body">
<aside class="mixing-material-picker-sider">
<div class="mixing-material-picker-sider-title">物料小类</div>
<Spin :spinning="treeLoading">
<BasicTree
:treeData="visibleCategoryTree"
:selectedKeys="selectedCategoryKeys"
:expandedKeys="expandedCategoryKeys"
defaultExpandLevel="2"
@update:selectedKeys="onCategorySelect"
@update:expandedKeys="onExpandedKeysChange"
/>
</Spin>
</aside>
<div class="mixing-material-picker-main">
<BasicTable @register="registerTable">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'pickerWeighMode'">
<div class="mixing-material-picker-weigh-mode" @click.stop>
<JDictSelectTag
:value="getPickerWeighMode(record.id)"
:dictCode="MIXING_MATERIAL_PICKER_WEIGH_MODE_DICT"
:getPopupContainer="getSelectPopupContainer"
:showChooseOption="false"
placeholder="请选择"
popupClassName="mixing-material-picker-weigh-mode-dropdown"
style="width: 100%"
@change="(val) => setPickerWeighMode(record.id, val)"
/>
</div>
</template>
</template>
</BasicTable>
</div>
</div>
</div>
</BasicModal>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue';
import { Spin } from 'ant-design-vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicTable, useTable } from '/@/components/Table';
import { BasicTree } from '/@/components/Tree';
import { defHttp } from '/@/utils/http/axios';
import { loadMesMaterialCategoryTreeData } from '/@/views/system/category/category.constants';
import { list as mixerList, queryById as queryMixerById } from '/@/views/mes/material/MesMixerMaterial.api';
import { useMessage } from '/@/hooks/web/useMessage';
import JDictSelectTag from '/@/components/Form/src/jeecg/components/JDictSelectTag.vue';
import MesXslMixingMaterialCategorySetting from './MesXslMixingMaterialCategorySetting.vue';
import {
applyMixingMaterialFromSelection,
loadMixingMaterialPickerHiddenCategoryIds,
MIXING_MATERIAL_PICKER_WEIGH_MODE_DICT,
mixingMaterialPickerTableColumns,
resolveMixingMaterialKindForPicker,
sanitizeMixingMaterialPickerHiddenCategoryIds,
saveMixingMaterialPickerHiddenCategoryIds,
type MixingMaterialPickerCategoryItem,
} from '../MesXslMixingSpec.data';
import type { KeyType } from '/@/components/Tree/src/types/tree';
const TREE_ALL = 'ALL';
const emit = defineEmits(['register', 'select']);
const { createMessage } = useMessage();
const keyword = ref('');
const treeLoading = ref(false);
const rawCategoryTree = ref<Recordable[]>([]);
const allMajorCategories = ref<Array<{ id: string; name: string; minors: MixingMaterialPickerCategoryItem[] }>>([]);
const allMinorCategories = ref<MixingMaterialPickerCategoryItem[]>([]);
const hiddenCategoryIds = ref<string[]>(loadMixingMaterialPickerHiddenCategoryIds());
const selectedCategoryKeys = ref<KeyType[]>([TREE_ALL]);
const expandedCategoryKeys = ref<KeyType[]>([TREE_ALL]);
const selectedRow = ref<Recordable | null>(null);
const categoryRubberMap = ref<Record<string, boolean>>({});
const pickerWeighModeMap = ref<Record<string, string>>({});
const hiddenCategoryIdSet = computed(() => new Set(hiddenCategoryIds.value.map(String)));
function getModalContainer() {
return document.body;
}
function getSelectPopupContainer() {
return document.body;
}
function filterHiddenCategoryTree(nodes: Recordable[], hidden: Set<string>): Recordable[] {
return (nodes || [])
.map((major) => {
const children = (major.children || [])
.filter((minor) => !hidden.has(String(minor.key)))
.map((minor) => ({
key: minor.key,
title: minor.title,
}));
if (!children.length) {
return null;
}
return {
key: major.key,
title: major.title,
children,
};
})
.filter(Boolean) as Recordable[];
}
const visibleCategoryTree = computed(() => [
{
key: TREE_ALL,
title: '全部小类',
children: filterHiddenCategoryTree(rawCategoryTree.value, hiddenCategoryIdSet.value),
},
]);
function syncExpandedCategoryKeys() {
const keys: KeyType[] = [TREE_ALL];
for (const major of visibleCategoryTree.value[0]?.children || []) {
keys.push(major.key);
}
expandedCategoryKeys.value = keys;
}
watch(
visibleCategoryTree,
() => {
syncExpandedCategoryKeys();
},
{ deep: true },
);
const selectedCategoryFilter = computed(() => {
const key = selectedCategoryKeys.value[0];
if (!key || key === TREE_ALL) {
return {};
}
const keyStr = String(key);
const major = allMajorCategories.value.find((item) => item.id === keyStr);
if (major) {
return { majorCategoryId: major.id };
}
return { minorCategoryId: keyStr };
});
const [registerTable, { reload, getSelectRowKeys, getSelectRows, clearSelectedRowKeys }] = useTable({
api: mixerList,
columns: mixingMaterialPickerTableColumns,
rowKey: 'id',
useSearchForm: false,
pagination: { pageSize: 10 },
canResize: false,
showIndexColumn: true,
immediate: true,
beforeFetch: (params) => {
const next = { ...params, ...selectedCategoryFilter.value };
const kw = keyword.value?.trim();
if (kw) {
next.materialName = `*${kw}*`;
}
return next;
},
rowSelection: {
type: 'radio',
columnWidth: 48,
onChange: (_keys, rows) => {
selectedRow.value = rows?.[0] ?? null;
},
},
clickToRowSelect: true,
});
//update-begin---author:cursor ---date:20260525 for【XSLMES-20260525-A50】选料弹窗打开时初始化对齐其他SelectModal-----------
const [registerModal, { setModalProps, closeModal }] = useModalInner(async () => {
await initPickerModal();
});
async function initPickerModal() {
selectedRow.value = null;
keyword.value = '';
pickerWeighModeMap.value = {};
clearSelectedRowKeys?.();
hiddenCategoryIds.value = loadMixingMaterialPickerHiddenCategoryIds();
selectedCategoryKeys.value = [TREE_ALL];
setModalProps({ confirmLoading: false });
await loadMaterialCategoryTree();
reloadTable();
}
onMounted(() => {
loadMaterialCategoryTree();
});
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A50】选料弹窗打开时初始化对齐其他SelectModal-----------
async function loadMaterialCategoryTree() {
treeLoading.value = true;
try {
const { majors, minors, treeNodes } = await loadMesMaterialCategoryTreeData();
rawCategoryTree.value = treeNodes;
allMajorCategories.value = majors;
allMinorCategories.value = minors;
const sanitizedHidden = sanitizeMixingMaterialPickerHiddenCategoryIds(
minors.map((item) => item.id),
hiddenCategoryIds.value,
);
if (sanitizedHidden.length !== hiddenCategoryIds.value.length) {
hiddenCategoryIds.value = sanitizedHidden;
saveMixingMaterialPickerHiddenCategoryIds(sanitizedHidden);
}
categoryRubberMap.value = {};
syncExpandedCategoryKeys();
if (!minors.length) {
createMessage.warning('未加载到物料小类,请确认分类字典根编码 XSLMES_MATERIAL 及其下级分类已配置。');
}
} catch {
rawCategoryTree.value = [];
allMajorCategories.value = [];
allMinorCategories.value = [];
createMessage.warning('加载物料分类树失败,请检查分类根编码 XSLMES_MATERIAL 是否存在。');
} finally {
treeLoading.value = false;
}
}
function reloadTable() {
reload();
}
function getPickerWeighMode(materialId?: string) {
if (!materialId) {
return undefined;
}
return pickerWeighModeMap.value[String(materialId)];
}
function setPickerWeighMode(materialId: string | undefined, value?: string) {
if (!materialId) {
return;
}
const key = String(materialId);
const next = { ...pickerWeighModeMap.value };
if (value == null || value === '') {
delete next[key];
} else {
next[key] = String(value);
}
pickerWeighModeMap.value = next;
}
function onCategorySelect(keys: KeyType[]) {
selectedCategoryKeys.value = keys?.length ? keys : [TREE_ALL];
reloadTable();
}
function onExpandedKeysChange(keys: KeyType[]) {
expandedCategoryKeys.value = keys?.length ? keys : [TREE_ALL];
}
function handleCategoryVisibilityChange() {
const key = selectedCategoryKeys.value[0];
if (!key || key === TREE_ALL) {
syncExpandedCategoryKeys();
reloadTable();
return;
}
const keyStr = String(key);
const hidden = hiddenCategoryIdSet.value;
const major = allMajorCategories.value.find((item) => item.id === keyStr);
if (major) {
const hasVisibleMinor = major.minors.some((minor) => !hidden.has(String(minor.id)));
if (!hasVisibleMinor) {
selectedCategoryKeys.value = [TREE_ALL];
}
} else if (hidden.has(keyStr)) {
selectedCategoryKeys.value = [TREE_ALL];
}
syncExpandedCategoryKeys();
reloadTable();
}
async function resolveKindForMaterial(material: Recordable, weighMode?: string) {
const minorId = material?.minorCategoryId ? String(material.minorCategoryId) : '';
const minorName = material?.minorCategoryId_dictText || '';
if (!minorId) {
return resolveMixingMaterialKindForPicker(weighMode, false, minorName);
}
if (categoryRubberMap.value[minorId] === undefined) {
try {
const cat = await defHttp.get<Recordable>({ url: '/sys/category/queryById', params: { id: minorId } });
categoryRubberMap.value[minorId] = cat?.isRubber === '1' || cat?.isRubber === 1;
} catch {
categoryRubberMap.value[minorId] = false;
}
}
return resolveMixingMaterialKindForPicker(
weighMode,
categoryRubberMap.value[minorId] ? '1' : '0',
minorName,
);
}
async function handleOk() {
const keys = (getSelectRowKeys?.() || []) as string[];
let row = selectedRow.value || ((getSelectRows?.() || []) as Recordable[])[0];
if (!row && keys.length) {
try {
const raw = await queryMixerById({ id: keys[0] });
row = (raw as any)?.id != null ? raw : (raw as any)?.result;
} catch {
// ignore
}
}
if (!row?.id) {
createMessage.warning('请选择一条密炼物料');
return;
}
const weighMode = getPickerWeighMode(row.id);
const payload: Recordable = { ...row, pickerWeighMode: weighMode };
const materialKind = await resolveKindForMaterial(row, weighMode);
applyMixingMaterialFromSelection(payload, row, materialKind);
emit('select', payload);
closeModal();
}
</script>
<style lang="less" scoped>
.mixing-material-picker {
display: flex;
flex-direction: column;
gap: 12px;
min-height: 520px;
}
.mixing-material-picker-toolbar {
display: flex;
align-items: center;
gap: 8px;
}
.mixing-material-picker-body {
display: flex;
gap: 12px;
min-height: 480px;
}
.mixing-material-picker-sider {
width: 240px;
flex-shrink: 0;
border: 1px solid var(--border-color-base, #f0f0f0);
border-radius: 4px;
padding: 8px;
overflow: auto;
}
.mixing-material-picker-sider-title {
font-weight: 600;
margin-bottom: 8px;
}
.mixing-material-picker-main {
flex: 1;
min-width: 0;
}
.mixing-material-picker-weigh-mode {
min-width: 108px;
}
</style>
<style lang="less">
/* 下拉挂到 body避免表格 overflow 裁剪;层级高于 Modal */
.mixing-material-picker-weigh-mode-dropdown {
z-index: 2100 !important;
}
</style>

View File

@@ -55,7 +55,15 @@
<tr>
<th class="formTitle" colspan="1">换算系数</th>
<td class="formValue" colspan="2">
<a-input-number v-model:value="sheetForm.convertFactor" :disabled="!showFooter" :precision="6" :bordered="false" class="form-input" style="width: 100%" />
<a-input-number
v-model:value="sheetForm.convertFactor"
:disabled="!showFooter"
:precision="6"
:bordered="false"
class="form-input"
style="width: 100%"
@update:value="handleConvertFactorChange"
/>
</td>
<th class="formTitle" colspan="1">填充体积</th>
<td class="formValue" colspan="1">
@@ -80,7 +88,7 @@
</td>
<th class="formTitle" colspan="1">母胶比重</th>
<td class="formValue">
<a-input-number v-model:value="sheetForm.motherRubberSg" :disabled="!showFooter" :precision="6" :bordered="false" class="form-input" style="width: 100%" />
<a-input-number v-model:value="sheetForm.motherRubberSg" :disabled="!showFooter" :precision="6" :bordered="false" class="form-input" style="width: 100%" @update:value="recalcFillVolume" />
</td>
<th class="formTitle">段数</th>
<td class="formValue" colspan="2">
@@ -104,7 +112,7 @@
<tr>
<th class="formTitle" colspan="1">终炼胶比重</th>
<td class="formValue">
<a-input-number v-model:value="sheetForm.finalRubberSg" :disabled="!showFooter" :precision="6" :bordered="false" class="form-input" style="width: 100%" />
<a-input-number v-model:value="sheetForm.finalRubberSg" :disabled="!showFooter" :precision="6" :bordered="false" class="form-input" style="width: 100%" @update:value="recalcFillVolume" />
</td>
<th class="formTitle" colspan="1">适用工厂</th>
<td class="formValue" colspan="2">
@@ -181,25 +189,60 @@
</div>
</div>
<div class="material-table-wrap" :style="{ height: `${materialMainTableHeight}px` }">
<JVxeTable
:key="materialTableLayoutKey"
ref="materialRef"
row-number
keep-source
bordered
:fit="false"
:column-config="{ resizable: true }"
:row-config="materialRowConfig"
size="mini"
:height="materialMainTableHeight"
:scroll-x="{ enabled: false }"
:scroll-y="{ enabled: true }"
:columns="visibleMaterialColumns"
:dataSource="materialData"
:disabled="!showFooter"
@resizable-change="handleMaterialColumnResize"
@column-resizable-change="handleMaterialColumnResize"
/>
<!--update-begin---author:cursor ---date:20260525 forXSLMES-20260525-A42橡胶及配合剂明细底部固定合计行----------- -->
<div class="material-table-stack" :style="{ width: `${materialTableWidth}px` }">
<div class="material-table-body" :style="{ height: `${materialBodyTableHeight}px` }">
<JVxeTable
:key="materialTableLayoutKey"
ref="materialRef"
row-number
keep-source
bordered
:fit="false"
:column-config="{ resizable: true }"
:row-config="materialRowConfig"
size="mini"
:height="materialBodyTableHeight"
:scroll-x="{ enabled: false }"
:scroll-y="{ enabled: true }"
:columns="visibleMaterialColumns"
:dataSource="materialData"
:disabled="!showFooter"
@value-change="handleMaterialValueChange"
@resizable-change="handleMaterialColumnResize"
@column-resizable-change="handleMaterialColumnResize"
>
<template #mixerMaterialNameSlot="{ row }">
<div
class="mixing-material-name-cell"
:class="{ 'is-disabled': !showFooter }"
:style="{ minHeight: `${materialHeightPref.rowHeight}px` }"
@click.stop="openMixingMaterialPicker(row)"
>
<span v-if="row.mixerMaterialName" class="mixing-material-name-text">{{ row.mixerMaterialName }}</span>
</div>
</template>
</JVxeTable>
</div>
<div class="material-table-footer">
<div class="material-table-footer-row">
<div
class="material-footer-seq"
:style="{ width: `${MIXING_MATERIAL_ROW_NUMBER_WIDTH}px`, height: `${materialHeightPref.rowHeight}px` }"
></div>
<div
v-for="cell in materialFooterCells"
:key="cell.key"
class="material-footer-cell"
:class="{ 'is-label': cell.isLabel, 'is-total': cell.isTotal }"
:style="{ width: `${cell.width}px`, height: `${materialHeightPref.rowHeight}px` }"
>
{{ cell.text }}
</div>
</div>
</div>
</div>
<!--update-end---author:cursor ---date:20260525 forXSLMES-20260525-A42橡胶及配合剂明细底部固定合计行----------- -->
</div>
<!--update-begin---author:cursor ---date:20260522 forXSLMES-20260522-A18TCU温度条件表移至橡胶及配合剂下方----------- -->
<div class="left-panel-section left-panel-section-tcu">
@@ -368,11 +411,11 @@
</div>
<BasicForm v-show="false" @register="registerForm" />
<MesXslEquipmentLedgerSelectModal @register="registerMachineModal" @select="onMachineSelect" />
<MesXslMixerPsCompileSelectModal @register="registerIssueNumberModal" @select="onIssueNumberSelect" />
</div>
</BasicModal>
<MesXslEquipmentLedgerSelectModal @register="registerMachineModal" @select="onMachineSelect" />
<MesXslMixerPsCompileSelectModal @register="registerIssueNumberModal" @select="onIssueNumberSelect" />
<MesXslMixingMaterialSelectModal @register="registerMixingMaterialModal" @select="onMixingMaterialSelect" />
</template>
<script lang="ts" setup>
@@ -411,6 +454,19 @@ import {
DEFAULT_MIXING_STEP_ROW_COUNT,
DEFAULT_MIXING_DOWN_STEP_ROW_COUNT,
buildDefaultMixingTcuRows,
applyMixingMaterialFromSelection,
fillMixingMaterialAccumWeight,
calcMixingMaterialUnitWeightTotal,
calcMixingMaterialAccumWeightTotal,
buildMixingMaterialFooterCells,
normalizeMixingConvertFactor,
initMaterialBaseUnitWeights,
applyConvertFactorToMaterialRows,
syncMaterialBaseUnitWeightFromDisplay,
calcMixingFillVolume,
resolveMixingSpecificGravity,
MIXING_MATERIAL_ROW_NUMBER_WIDTH,
MIXING_MATERIAL_FOOTER_ROW_HEIGHT,
MIXING_MATERIAL_MIN_COLUMN_WIDTH,
MIXING_TCU_MIN_COLUMN_WIDTH,
MIXING_STEP_MIN_COLUMN_WIDTH,
@@ -422,7 +478,9 @@ import MesXslMixingStepSelectCell from './MesXslMixingStepSelectCell.vue';
import { list as mixerActionList } from '/@/views/xslmes/mesXslMixerAction/MesXslMixerAction.api';
import { list as mixerConditionList } from '/@/views/xslmes/mesXslMixerCondition/MesXslMixerCondition.api';
import MesXslEquipmentLedgerSelectModal from '/@/views/xslmes/mesXslEquipInspectConfig/components/MesXslEquipmentLedgerSelectModal.vue';
import { queryById as queryEquipmentById } from '/@/views/xslmes/mesXslEquipmentLedger/MesXslEquipmentLedger.api';
import MesXslMixerPsCompileSelectModal from '/@/views/xslmes/mesXslMixerPsCompile/components/MesXslMixerPsCompileSelectModal.vue';
import MesXslMixingMaterialSelectModal from './MesXslMixingMaterialSelectModal.vue';
const emit = defineEmits(['register', 'success']);
const { createMessage } = useMessage();
@@ -437,6 +495,7 @@ const mixerConditionOptions = ref<{ title: string; value: string }[]>([]);
//update-end---author:cursor ---date:20260522 for【XSLMES-20260522-A34】混合步骤动作/组合下拉选项-----------
const materialRef = ref();
const materialPickerRow = ref<Recordable | null>(null);
const stepRef = ref();
const downStepRef = ref();
const tcuRef = ref();
@@ -465,6 +524,22 @@ const stepHeightPref = ref(loadMixingTableHeightPreference('step'));
const downStepHeightPref = ref(loadMixingTableHeightPreference('downStep'));
const materialMainTableHeight = computed(() => calcMixingDetailTableViewportHeight('material', materialHeightPref.value));
//update-begin---author:cursor ---date:20260525 for【XSLMES-20260525-A42】橡胶及配合剂明细底部固定合计行-----------
const materialBodyTableHeight = computed(() =>
Math.max(materialMainTableHeight.value - MIXING_MATERIAL_FOOTER_ROW_HEIGHT, 80),
);
const materialUnitWeightTotal = ref<number | null>(null);
//update-begin---author:cursor ---date:20260525 for【XSLMES-20260525-A46】换算系数/单重/机台有效体积联动填充体积-----------
const machineEffectiveVolume = ref('');
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A46】换算系数/单重/机台有效体积联动填充体积-----------
const materialAccumWeightTotal = ref<number | null>(null);
const materialFooterCells = computed(() =>
buildMixingMaterialFooterCells(visibleMaterialColumns.value, materialColumnWidths.value, {
unitWeight: materialUnitWeightTotal.value,
accumWeight: materialAccumWeightTotal.value,
}),
);
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A42】橡胶及配合剂明细底部固定合计行-----------
const stepMainTableHeight = computed(() => calcMixingDetailTableViewportHeight('step', stepHeightPref.value));
const tcuTableHeight = computed(() => calcMixingDetailTableViewportHeight('tcu', tcuHeightPref.value));
const downStepTableHeight = computed(() => calcMixingDetailTableViewportHeight('downStep', downStepHeightPref.value));
@@ -509,6 +584,105 @@ function handleMaterialColumnResize(params: Recordable) {
};
saveMixingMaterialColumnWidths(materialColumnWidths.value);
}
//update-begin---author:cursor ---date:20260525 for【XSLMES-20260525-A41】橡胶及配合剂明细累计按种类分组合计-----------
/** JVxe getTableData 为浅拷贝,批量改值需用 fullData 原行并强制 refresh */
function resolveMaterialTableRawRows(): Recordable[] {
const fullData = materialRef.value?.getXTable?.()?.getTableData?.()?.fullData as Recordable[] | undefined;
if (Array.isArray(fullData) && fullData.length) {
return fullData;
}
return (materialRef.value?.getTableData?.() || materialData.value || []) as Recordable[];
}
function refreshMaterialTableView() {
materialRef.value?.getXTable?.()?.updateData?.();
}
function applyMaterialAccumWeight(rows?: Recordable[]) {
const targetRows = rows || resolveMaterialTableRawRows();
fillMixingMaterialAccumWeight(targetRows);
materialUnitWeightTotal.value = calcMixingMaterialUnitWeightTotal(targetRows);
materialAccumWeightTotal.value = calcMixingMaterialAccumWeightTotal(targetRows);
refreshMaterialTableView();
recalcFillVolume();
}
//update-begin---author:cursor ---date:20260525 for【XSLMES-20260525-A46】换算系数/单重/机台有效体积联动填充体积-----------
async function loadMachineEffectiveVolume(machineId?: string) {
const id = machineId || sheetForm.machineId;
if (!id) {
machineEffectiveVolume.value = '';
return;
}
try {
const raw = await queryEquipmentById({ id });
const row = (raw as Recordable)?.id != null ? raw : (raw as Recordable)?.result;
machineEffectiveVolume.value = row?.effectiveVolume || '';
} catch {
machineEffectiveVolume.value = '';
}
}
function recalcFillVolume() {
if (!showFooter.value) {
return;
}
const totalWeight =
materialUnitWeightTotal.value ?? calcMixingMaterialUnitWeightTotal(resolveMaterialTableRawRows());
const specificGravity = resolveMixingSpecificGravity(sheetForm);
const next = calcMixingFillVolume(totalWeight, specificGravity, machineEffectiveVolume.value);
if (next != null) {
sheetForm.fillVolume = next;
}
}
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A46】换算系数/单重/机台有效体积联动填充体积-----------
//update-begin---author:cursor ---date:20260525 for【XSLMES-20260525-A43】换算系数联动明细单重实时计算-----------
const lastConvertFactor = ref<number>(1);
const convertFactorApplying = ref(false);
function applyConvertFactorToMaterials(factor: unknown) {
const rows = resolveMaterialTableRawRows();
applyConvertFactorToMaterialRows(rows, factor, lastConvertFactor.value);
lastConvertFactor.value = normalizeMixingConvertFactor(factor);
applyMaterialAccumWeight(rows);
}
function handleConvertFactorChange(value: unknown) {
if (!showFooter.value || convertFactorApplying.value) {
return;
}
applyConvertFactorToMaterials(value);
recalcFillVolume();
}
function stripMaterialRowForSave(row: Recordable) {
if (!row) {
return row;
}
const { baseUnitWeight: _baseUnitWeight, ...rest } = row;
return rest;
}
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A43】换算系数联动明细单重实时计算-----------
function recalcMaterialAccumWeight() {
applyMaterialAccumWeight();
}
function handleMaterialValueChange(event) {
const key = event?.column?.key;
const row = event?.row;
//update-begin---author:cursor ---date:20260525 for【XSLMES-20260525-A43】换算系数联动明细单重实时计算-----------
if (key === 'unitWeight' && row) {
syncMaterialBaseUnitWeightFromDisplay(row, sheetForm.convertFactor);
}
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A43】换算系数联动明细单重实时计算-----------
if (key === 'unitWeight' || key === 'materialKind') {
recalcMaterialAccumWeight();
}
}
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A41】橡胶及配合剂明细累计按种类分组合计-----------
//update-end---author:cursor ---date:20260522 for【XSLMES-20260522-A17】橡胶及配合剂明细列展示设置-----------
//update-begin---author:cursor ---date:20260522 for【XSLMES-20260522-A19】TCU温度条件表列宽可调且表头换行-----------
@@ -710,6 +884,7 @@ const [registerForm, { resetFields, setFieldsValue, validate, setProps }] = useF
//update-begin---author:cursor ---date:20260522 for【XSLMES-20260522-A33】混炼示方主表选择弹窗-----------
const [registerMachineModal, { openModal: openMachineModalInner }] = useModal();
const [registerIssueNumberModal, { openModal: openIssueNumberModalInner }] = useModal();
const [registerMixingMaterialModal, { openModal: openMixingMaterialModalInner }] = useModal();
function openMachinePicker() {
if (!showFooter.value) {
@@ -721,6 +896,11 @@ function openMachinePicker() {
async function onMachineSelect(payload: Recordable | null) {
sheetForm.machineId = payload?.equipmentLedgerId || '';
sheetForm.machineName = payload?.equipmentName || '';
machineEffectiveVolume.value = payload?.effectiveVolume || '';
if (sheetForm.machineId && !machineEffectiveVolume.value) {
await loadMachineEffectiveVolume(sheetForm.machineId);
}
recalcFillVolume();
await loadMixerStepOptions(sheetForm.machineId);
}
@@ -738,6 +918,27 @@ function onIssueNumberSelect(payload: Recordable | null) {
mixerPsCompilePickerId.value = payload.psCompileId || '';
sheetForm.issueNumber = payload.psCode || '';
}
function openMixingMaterialPicker(row: Recordable) {
if (!showFooter.value || !row) {
return;
}
materialPickerRow.value = row;
openMixingMaterialModalInner(true, { picker: true, ts: Date.now() });
}
function onMixingMaterialSelect(payload: Recordable | null) {
if (!payload || !materialPickerRow.value) {
return;
}
applyMixingMaterialFromSelection(
materialPickerRow.value,
payload,
payload.materialKind || payload.materialMinor || '',
);
recalcMaterialAccumWeight();
materialPickerRow.value = null;
}
//update-end---author:cursor ---date:20260522 for【XSLMES-20260522-A33】混炼示方主表选择弹窗-----------
function ensureTcuDefaultRows(rows: Recordable[] = []) {
@@ -783,6 +984,7 @@ function resetSheetForm() {
sheetForm.approveTime = '';
sheetForm.changeDate = '';
mixerPsCompilePickerId.value = '';
machineEffectiveVolume.value = '';
refreshSignDisplay({});
}
@@ -806,6 +1008,9 @@ const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data
await resetFields();
resetSheetForm();
materialData.value = [];
materialUnitWeightTotal.value = null;
materialAccumWeightTotal.value = null;
lastConvertFactor.value = 1;
stepData.value = [];
downStepData.value = [];
tcuData.value = ensureTcuDefaultRows([]);
@@ -819,10 +1024,16 @@ const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data
const row = raw?.result || raw;
Object.assign(sheetForm, row || {});
refreshSignDisplay(row || {});
await loadMachineEffectiveVolume(sheetForm.machineId);
await loadMixerStepOptions(sheetForm.machineId);
await syncSheetToForm();
//update-begin---author:cursor ---date:20260522 for【XSLMES-20260522-A39】编辑页明细补齐默认空行与新增一致-----------
materialData.value = ensureMixingDetailRows(row?.materialList || [], DEFAULT_MIXING_MATERIAL_ROW_COUNT);
convertFactorApplying.value = true;
initMaterialBaseUnitWeights(materialData.value, sheetForm.convertFactor, true);
lastConvertFactor.value = normalizeMixingConvertFactor(sheetForm.convertFactor);
applyMaterialAccumWeight(materialData.value);
convertFactorApplying.value = false;
stepData.value = ensureMixingDetailRows(row?.stepList || [], DEFAULT_MIXING_STEP_ROW_COUNT);
downStepData.value = ensureMixingDetailRows(row?.downStepList || [], DEFAULT_MIXING_DOWN_STEP_ROW_COUNT);
//update-end---author:cursor ---date:20260522 for【XSLMES-20260522-A39】编辑页明细补齐默认空行与新增一致-----------
@@ -838,6 +1049,7 @@ const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data
});
//update-begin---author:cursor ---date:20260522 for【XSLMES-20260522-A22】明细表默认空行数-----------
materialData.value = createEmptyMaterialRows();
lastConvertFactor.value = normalizeMixingConvertFactor(sheetForm.convertFactor);
stepData.value = createEmptyStepRows();
downStepData.value = createEmptyDownStepRows();
//update-end---author:cursor ---date:20260522 for【XSLMES-20260522-A22】明细表默认空行数-----------
@@ -850,14 +1062,15 @@ const title = computed(() => (!showFooter.value && unref(isUpdate) ? '混炼示
async function handleSubmit() {
await syncSheetToForm();
const formValues = await validate();
const materialList = materialRef.value?.getTableData?.() || materialData.value;
const materialList = resolveMaterialTableRawRows();
applyMaterialAccumWeight(materialList);
const stepList = stepRef.value?.getTableData?.() || stepData.value;
const downStepList = downStepRef.value?.getTableData?.() || downStepData.value;
const tcuList = ensureTcuDefaultRows((tcuRef.value?.getTableData?.() || tcuData.value) as Recordable[]);
const cleanRows = (rows: Recordable[]) => (rows || []).filter((row) => Object.values(row || {}).some((v) => v != null && v !== ''));
const payload = {
...formValues,
materialList: cleanRows(materialList),
materialList: cleanRows(materialList).map(stripMaterialRowForSave),
stepList: cleanRows(stepList),
downStepList: cleanRows(downStepList),
tcuList: tcuList.map((row) => ({
@@ -1008,6 +1221,25 @@ async function handleSubmit() {
color: #262626;
}
:deep(.mixing-material-name-cell) {
display: flex;
align-items: center;
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 2px 4px;
cursor: pointer;
}
:deep(.mixing-material-name-cell.is-disabled) {
cursor: not-allowed;
opacity: 0.65;
}
:deep(.mixing-material-name-text) {
color: #262626;
}
}
//update-end---author:cursor ---date:20260522 for【XSLMES-20260522-A17】顶部施工表对齐旧系统13列表格-----------
@@ -1027,6 +1259,9 @@ async function handleSubmit() {
.material-table-wrap {
overflow-x: auto;
overflow-y: hidden;
display: flex;
flex-direction: column;
:deep(.jeecg-j-vxe-table),
:deep(.j-vxe-table-box),
@@ -1040,6 +1275,76 @@ async function handleSubmit() {
}
}
//update-begin---author:cursor ---date:20260525 for【XSLMES-20260525-A42】橡胶及配合剂明细底部固定合计行-----------
.material-table-stack {
display: flex;
flex-direction: column;
flex-shrink: 0;
min-width: min-content;
}
.material-table-body {
flex: 0 0 auto;
overflow: hidden;
:deep(.vxe-table--footer-wrapper) {
display: none;
}
:deep(.col--mixerMaterialName .vxe-cell) {
padding: 0;
height: 100%;
}
:deep(.col--mixerMaterialName .vxe-cell > div) {
height: 100%;
}
}
.material-table-footer {
flex-shrink: 0;
border: 1px solid #e8e8e8;
border-top: none;
background: #fafafa;
box-sizing: border-box;
}
.material-table-footer-row {
display: flex;
align-items: stretch;
box-sizing: border-box;
font-size: 12px;
}
.material-footer-seq,
.material-footer-cell {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
border-right: 1px solid #e8e8e8;
box-sizing: border-box;
padding: 0 2px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&:last-child {
border-right: none;
}
}
.material-footer-seq {
background: #fafafa;
}
.material-footer-cell.is-label,
.material-footer-cell.is-total {
font-weight: 600;
color: #262626;
}
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A42】橡胶及配合剂明细底部固定合计行-----------
//update-begin---author:cursor ---date:20260522 for【XSLMES-20260522-A26】TCU紧凑两行且胶料表向下扩展-----------
.material-table-wrap--fill {
flex: 1;