407 lines
13 KiB
Vue
407 lines
13 KiB
Vue
|
|
<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>
|