Merge branch '20260519-3.9.2版本-葛昊天分支'
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" title="选择机台(可多选)" :width="1000" @register="registerModal" @ok="handleOk">
|
||||
<BasicTable @register="registerTable" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicTable, useTable } from '/@/components/Table';
|
||||
import { list } from '/@/views/xslmes/mesXslEquipmentLedger/MesXslEquipmentLedger.api';
|
||||
|
||||
const emit = defineEmits(['register', 'select']);
|
||||
|
||||
const selectedRows = ref<Recordable[]>([]);
|
||||
|
||||
function handleSelectionChange(_keys: string[], rows: Recordable[]) {
|
||||
selectedRows.value = rows || [];
|
||||
}
|
||||
|
||||
const [registerTable, { reload, getSelectRows, setSelectedRowKeys, clearSelectedRowKeys }] = useTable({
|
||||
api: list,
|
||||
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 },
|
||||
],
|
||||
rowKey: 'id',
|
||||
useSearchForm: true,
|
||||
formConfig: {
|
||||
labelWidth: 90,
|
||||
schemas: [
|
||||
{ label: '设备名称', field: 'equipmentName', component: 'Input', colProps: { span: 8 } },
|
||||
{ label: '设备编号', field: 'equipmentCode', component: 'Input', colProps: { span: 8 } },
|
||||
],
|
||||
},
|
||||
pagination: { pageSize: 10 },
|
||||
canResize: false,
|
||||
showIndexColumn: false,
|
||||
immediate: true,
|
||||
rowSelection: {
|
||||
type: 'checkbox',
|
||||
columnWidth: 48,
|
||||
onChange: handleSelectionChange,
|
||||
},
|
||||
clickToRowSelect: true,
|
||||
});
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
selectedRows.value = [];
|
||||
clearSelectedRowKeys?.();
|
||||
setModalProps({ confirmLoading: false });
|
||||
const preset = (data?.selectedMachines || []) as Array<{ machineId?: string }>;
|
||||
const keys = preset.map((m) => m.machineId).filter(Boolean) as string[];
|
||||
if (keys.length) {
|
||||
setSelectedRowKeys?.(keys);
|
||||
}
|
||||
reload();
|
||||
});
|
||||
|
||||
async function handleOk() {
|
||||
const rows = ((getSelectRows?.() || []) as Recordable[]).length
|
||||
? (getSelectRows?.() || []) as Recordable[]
|
||||
: selectedRows.value;
|
||||
const machines = rows
|
||||
.filter((r) => r?.id)
|
||||
.map((r) => ({
|
||||
machineId: r.id,
|
||||
machineName: r.equipmentName || '',
|
||||
machineCode: r.equipmentCode || '',
|
||||
}));
|
||||
emit('select', machines);
|
||||
closeModal();
|
||||
}
|
||||
</script>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ enum Api {
|
||||
generateRubberCode = '/xslmes/mesXslFormulaSpec/generateRubberCode',
|
||||
getRubberContentSetting = '/xslmes/mesXslFormulaSpec/getRubberContentSetting',
|
||||
saveRubberContentSetting = '/xslmes/mesXslFormulaSpec/saveRubberContentSetting',
|
||||
buildMixingGeneratePreview = '/xslmes/mesXslFormulaSpec/buildMixingGeneratePreview',
|
||||
generateMixingSpec = '/xslmes/mesXslFormulaSpec/generateMixingSpec',
|
||||
}
|
||||
|
||||
export const getExportUrl = Api.exportXls;
|
||||
@@ -41,3 +43,12 @@ export const batchDelete = (params, handleSuccess) => {
|
||||
};
|
||||
|
||||
export const saveOrUpdate = (params, isUpdate) => defHttp.post({ url: isUpdate ? Api.edit : Api.save, params });
|
||||
|
||||
//update-begin---author:cursor ---date:20260522 for:【XSLMES-20260522-A38】配合示方生成混炼示方-----------
|
||||
export const buildMixingGeneratePreview = (params) =>
|
||||
defHttp.get({ url: Api.buildMixingGeneratePreview, params }, { successMessageMode: 'none' });
|
||||
//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】配合示方生成混炼示方-----------
|
||||
|
||||
@@ -36,27 +36,40 @@
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
|
||||
<a-button
|
||||
type="primary"
|
||||
v-auth="'xslmes:mes_xsl_mixing_spec:add'"
|
||||
preIcon="ant-design:thunderbolt-outlined"
|
||||
@click="handleGenerateMixing"
|
||||
>
|
||||
生成混炼示方
|
||||
</a-button>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<TableAction :actions="getTableActions(record)" :dropDownActions="getDropDownAction(record)" />
|
||||
</template>
|
||||
</BasicTable>
|
||||
<MesXslFormulaSpecModal @register="registerModal" @success="handleSuccess" />
|
||||
<MesXslFormulaGenerateMixingModal @register="registerGenerateMixingModal" @success="handleGenerateMixingSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="xslmes-mesXslFormulaSpec" setup>
|
||||
import { reactive } from 'vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { BasicTable, TableAction } from '/@/components/Table';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
import Icon from '/@/components/Icon';
|
||||
import MesXslFormulaSpecModal from './components/MesXslFormulaSpecModal.vue';
|
||||
import MesXslFormulaGenerateMixingModal from './components/MesXslFormulaGenerateMixingModal.vue';
|
||||
import { columns, searchFormSchema, superQuerySchema } from './MesXslFormulaSpec.data';
|
||||
import { list, deleteOne, batchDelete, getExportUrl, getImportUrl } from './MesXslFormulaSpec.api';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const queryParam = reactive<any>({});
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
const [registerGenerateMixingModal, { openModal: openGenerateMixingModal }] = useModal();
|
||||
|
||||
const { tableContext, onExportXls, onImportXls } = useListPage({
|
||||
tableProps: {
|
||||
@@ -125,6 +138,19 @@
|
||||
selectedRowKeys.value = [];
|
||||
}
|
||||
|
||||
function handleGenerateMixing() {
|
||||
const keys = selectedRowKeys.value || [];
|
||||
if (keys.length !== 1) {
|
||||
createMessage.warning('请勾选一条配合示方数据');
|
||||
return;
|
||||
}
|
||||
openGenerateMixingModal(true, { formulaSpecId: keys[0] });
|
||||
}
|
||||
|
||||
function handleGenerateMixingSuccess() {
|
||||
selectedRowKeys.value = [];
|
||||
}
|
||||
|
||||
function canEdit(record: Recordable) {
|
||||
return !record?.status || record.status === 'compile';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
v-bind="$attrs"
|
||||
title="生成混炼示方"
|
||||
:width="880"
|
||||
destroyOnClose
|
||||
@register="registerModal"
|
||||
@ok="handleConfirm"
|
||||
>
|
||||
<a-spin :spinning="loading">
|
||||
<div v-if="rubberName" class="generate-mixing-tip">
|
||||
胶料名称:<strong>{{ rubberName }}</strong>,混合段数:<strong>{{ mixingStages }}</strong>,确认后将按「预览行 × 机台」生成混炼示方。
|
||||
</div>
|
||||
<a-table
|
||||
:columns="tableColumns"
|
||||
:data-source="tableRows"
|
||||
:pagination="false"
|
||||
row-key="rowKey"
|
||||
size="small"
|
||||
bordered
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'machines'">
|
||||
<div class="machine-cell">
|
||||
<a-input
|
||||
:value="formatMachineNames(record.machines)"
|
||||
readonly
|
||||
placeholder="请点击选择机台(可多选)"
|
||||
class="machine-picker-input"
|
||||
@click="openMachinePicker(record)"
|
||||
/>
|
||||
<a-button type="link" size="small" @click="openMachinePicker(record)">选择</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<div v-if="totalGenerateCount > 0" class="generate-mixing-summary">
|
||||
预计生成 <strong>{{ totalGenerateCount }}</strong> 条混炼示方
|
||||
</div>
|
||||
</a-spin>
|
||||
<MesXslEquipmentLedgerMultiSelectModal @register="registerMachineModal" @select="onMachineSelect" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { BasicModal, useModal, useModalInner } from '/@/components/Modal';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import MesXslEquipmentLedgerMultiSelectModal from '/@/views/xslmes/mesXslEquipInspectConfig/components/MesXslEquipmentLedgerMultiSelectModal.vue';
|
||||
import { buildMixingGeneratePreview, generateMixingSpec } from '../MesXslFormulaSpec.api';
|
||||
|
||||
const emit = defineEmits(['register', 'success']);
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const loading = ref(false);
|
||||
const formulaSpecId = ref('');
|
||||
const rubberName = ref('');
|
||||
const mixingStages = ref(0);
|
||||
const tableRows = ref<Recordable[]>([]);
|
||||
const editingRowKey = ref<number | null>(null);
|
||||
|
||||
function formatStageCountText(record: Recordable) {
|
||||
const current = record?.stageIndex;
|
||||
const total = mixingStages.value;
|
||||
if (total > 0) {
|
||||
return `${current ?? ''}/${total}`;
|
||||
}
|
||||
return current != null ? String(current) : '';
|
||||
}
|
||||
|
||||
const tableColumns = [
|
||||
{ title: '示方编号', dataIndex: 'specCode', width: 220 },
|
||||
{ title: '段数', dataIndex: 'stageIndex', width: 72, customRender: ({ record }) => formatStageCountText(record) },
|
||||
{ title: '机台', dataIndex: 'machines' },
|
||||
];
|
||||
|
||||
const totalGenerateCount = computed(() =>
|
||||
tableRows.value.reduce((sum, row) => sum + (row.machines?.length || 0), 0),
|
||||
);
|
||||
|
||||
const [registerMachineModal, { openModal: openMachineModalInner }] = useModal();
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
formulaSpecId.value = data?.formulaSpecId || '';
|
||||
tableRows.value = [];
|
||||
rubberName.value = '';
|
||||
mixingStages.value = 0;
|
||||
setModalProps({ confirmLoading: false });
|
||||
if (!formulaSpecId.value) {
|
||||
createMessage.warning('未指定配合示方');
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const preview = await buildMixingGeneratePreview({ formulaSpecId: formulaSpecId.value });
|
||||
rubberName.value = preview?.rubberName || '';
|
||||
mixingStages.value = preview?.mixingStages || 0;
|
||||
tableRows.value = (preview?.items || []).map((item: Recordable) => ({
|
||||
rowKey: item.rowKey,
|
||||
specCode: item.specCode,
|
||||
stageIndex: item.stageIndex,
|
||||
aSegmentIndex: item.aSegmentIndex,
|
||||
stepType: item.stepType,
|
||||
machines: [],
|
||||
}));
|
||||
if (!tableRows.value.length) {
|
||||
createMessage.warning('无可生成的混炼段,请检查混合段数');
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
function formatMachineNames(machines: Recordable[] = []) {
|
||||
return machines.map((m) => m.machineName).filter(Boolean).join('、');
|
||||
}
|
||||
|
||||
function openMachinePicker(record: Recordable) {
|
||||
editingRowKey.value = record.rowKey;
|
||||
openMachineModalInner(true, { selectedMachines: record.machines || [] });
|
||||
}
|
||||
|
||||
function onMachineSelect(machines: Recordable[]) {
|
||||
const key = editingRowKey.value;
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
const row = tableRows.value.find((r) => r.rowKey === key);
|
||||
if (row) {
|
||||
row.machines = machines || [];
|
||||
}
|
||||
editingRowKey.value = null;
|
||||
}
|
||||
|
||||
async function handleConfirm() {
|
||||
if (!formulaSpecId.value) {
|
||||
createMessage.warning('未指定配合示方');
|
||||
return;
|
||||
}
|
||||
if (!tableRows.value.length) {
|
||||
createMessage.warning('无可生成的混炼段');
|
||||
return;
|
||||
}
|
||||
const emptyMachine = tableRows.value.find((r) => !r.machines?.length);
|
||||
if (emptyMachine) {
|
||||
createMessage.warning(`请为示方「${emptyMachine.specCode}」选择机台`);
|
||||
return;
|
||||
}
|
||||
setModalProps({ confirmLoading: true });
|
||||
try {
|
||||
const res = await generateMixingSpec({
|
||||
formulaSpecId: formulaSpecId.value,
|
||||
rows: tableRows.value.map((r) => ({
|
||||
specCode: r.specCode,
|
||||
stageIndex: r.stageIndex,
|
||||
aSegmentIndex: r.aSegmentIndex,
|
||||
stepType: r.stepType,
|
||||
machines: (r.machines || []).map((m: Recordable) => ({
|
||||
machineId: m.machineId,
|
||||
machineName: m.machineName,
|
||||
})),
|
||||
})),
|
||||
});
|
||||
const count = Number(res?.count ?? 0);
|
||||
createMessage.success(count > 0 ? `成功生成 ${count} 条混炼示方` : '生成成功');
|
||||
closeModal();
|
||||
emit('success');
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.generate-mixing-tip {
|
||||
margin-bottom: 12px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
.machine-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
.machine-picker-input {
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.generate-mixing-summary {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
</style>
|
||||
@@ -8,6 +8,17 @@
|
||||
@register="registerModal"
|
||||
@ok="handleSubmit"
|
||||
>
|
||||
<template #insertFooter>
|
||||
<a-button
|
||||
v-if="showGenerateMixingBtn"
|
||||
type="primary"
|
||||
v-auth="'xslmes:mes_xsl_mixing_spec:add'"
|
||||
preIcon="ant-design:thunderbolt-outlined"
|
||||
@click="handleGenerateMixing"
|
||||
>
|
||||
生成混炼示方
|
||||
</a-button>
|
||||
</template>
|
||||
<template #title>
|
||||
<span class="formula-spec-modal-title">{{ title }}</span>
|
||||
<a-tooltip title="含胶率物料小类设置">
|
||||
@@ -390,6 +401,7 @@
|
||||
@getSelectResult="onIssueDeptSelect"
|
||||
/>
|
||||
<MesXslFormulaRubberContentSettingModal @register="registerRubberContentSettingModal" @success="onRubberContentSettingSaved" />
|
||||
<MesXslFormulaGenerateMixingModal @register="registerGenerateMixingModal" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
@@ -439,6 +451,7 @@
|
||||
} from '../MesXslFormulaSpec.data';
|
||||
import { saveOrUpdate, queryById, generateRubberCode as generateRubberCodeApi, getRubberContentSetting } from '../MesXslFormulaSpec.api';
|
||||
import MesXslFormulaRubberContentSettingModal from './MesXslFormulaRubberContentSettingModal.vue';
|
||||
import MesXslFormulaGenerateMixingModal from './MesXslFormulaGenerateMixingModal.vue';
|
||||
import MesXslFormulaLineColumnSetting from './MesXslFormulaLineColumnSetting.vue';
|
||||
import { queryById as queryMixerById } from '/@/views/mes/material/MesMixerMaterial.api';
|
||||
import { buildUUID } from '/@/utils/uuid';
|
||||
@@ -518,6 +531,7 @@
|
||||
const [registerIssueNumberModal, { openModal: openIssueNumberModalInner }] = useModal();
|
||||
const [registerIssueDeptModal, { openModal: openIssueDeptModalInner }] = useModal();
|
||||
const [registerRubberContentSettingModal, { openModal: openRubberContentSettingModal }] = useModal();
|
||||
const [registerGenerateMixingModal, { openModal: openGenerateMixingModal }] = useModal();
|
||||
const issueDeptPickerValue = ref<string>('');
|
||||
const rubberMaterialPickerId = ref<string>('');
|
||||
const mixerPsCompilePickerId = ref<string>('');
|
||||
@@ -1172,6 +1186,19 @@
|
||||
return !unref(isUpdate) ? '新增配合示方' : '编辑配合示方';
|
||||
});
|
||||
|
||||
const showGenerateMixingBtn = computed(
|
||||
() => !showFooterFlag.value && unref(isUpdate) && !!loadedMainRecord.value?.id,
|
||||
);
|
||||
|
||||
function handleGenerateMixing() {
|
||||
const id = loadedMainRecord.value?.id;
|
||||
if (!id) {
|
||||
createMessage.warning('请先保存配合示方');
|
||||
return;
|
||||
}
|
||||
openGenerateMixingModal(true, { formulaSpecId: id });
|
||||
}
|
||||
|
||||
async function handleLineValueChange(event) {
|
||||
const { row, column } = event || {};
|
||||
if (!row || !column) {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" title="选择密炼机动作" :width="960" @register="registerModal" @ok="handleOk">
|
||||
<BasicTable @register="registerTable" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicTable, useTable } from '/@/components/Table';
|
||||
import { list, queryById } from '../MesXslMixerAction.api';
|
||||
import { columns as actionColumns, searchFormSchema } from '../MesXslMixerAction.data';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
const emit = defineEmits(['register', 'select']);
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const equipmentId = ref('');
|
||||
const selectedRow = ref<Recordable | null>(null);
|
||||
|
||||
const [registerTable, { reload, getSelectRowKeys, getSelectRows, setSelectedRowKeys, clearSelectedRowKeys }] = useTable({
|
||||
api: list,
|
||||
columns: actionColumns,
|
||||
rowKey: 'id',
|
||||
useSearchForm: true,
|
||||
formConfig: {
|
||||
labelWidth: 90,
|
||||
schemas: searchFormSchema,
|
||||
},
|
||||
pagination: { pageSize: 10 },
|
||||
canResize: false,
|
||||
showIndexColumn: false,
|
||||
immediate: true,
|
||||
beforeFetch: (params) => ({
|
||||
...params,
|
||||
equipmentId: equipmentId.value || undefined,
|
||||
}),
|
||||
rowSelection: {
|
||||
type: 'radio',
|
||||
columnWidth: 48,
|
||||
onChange: (_keys, rows) => {
|
||||
selectedRow.value = rows?.[0] ?? null;
|
||||
},
|
||||
},
|
||||
clickToRowSelect: true,
|
||||
});
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
equipmentId.value = (data?.equipmentId as string) || '';
|
||||
selectedRow.value = null;
|
||||
clearSelectedRowKeys?.();
|
||||
setModalProps({ confirmLoading: false });
|
||||
const actionId = data?.actionId as string | undefined;
|
||||
if (actionId) {
|
||||
setSelectedRowKeys?.([actionId]);
|
||||
try {
|
||||
const raw = await queryById({ id: actionId });
|
||||
const row = (raw as Recordable)?.id != null ? raw : (raw as Recordable)?.result;
|
||||
if (row) {
|
||||
selectedRow.value = row;
|
||||
}
|
||||
} catch {
|
||||
// 忽略
|
||||
}
|
||||
}
|
||||
reload();
|
||||
});
|
||||
|
||||
async function handleOk() {
|
||||
const keys = (getSelectRowKeys?.() || []) as string[];
|
||||
let row = selectedRow.value || ((getSelectRows?.() || []) as Recordable[])[0];
|
||||
if (!row && keys.length) {
|
||||
try {
|
||||
const raw = await queryById({ id: keys[0] });
|
||||
row = (raw as Recordable)?.id != null ? raw : (raw as Recordable)?.result;
|
||||
} catch {
|
||||
// 忽略
|
||||
}
|
||||
}
|
||||
if (!row?.id) {
|
||||
createMessage.warning('请选择一条密炼机动作');
|
||||
return;
|
||||
}
|
||||
emit('select', {
|
||||
actionId: row.id,
|
||||
actionName: row.actionName || '',
|
||||
actionCode: row.actionCode || '',
|
||||
});
|
||||
closeModal();
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" title="选择密炼机条件" :width="960" @register="registerModal" @ok="handleOk">
|
||||
<BasicTable @register="registerTable" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicTable, useTable } from '/@/components/Table';
|
||||
import { list, queryById } from '../MesXslMixerCondition.api';
|
||||
import { columns as conditionColumns, searchFormSchema } from '../MesXslMixerCondition.data';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
const emit = defineEmits(['register', 'select']);
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const equipmentId = ref('');
|
||||
const selectedRow = ref<Recordable | null>(null);
|
||||
|
||||
const [registerTable, { reload, getSelectRowKeys, getSelectRows, setSelectedRowKeys, clearSelectedRowKeys }] = useTable({
|
||||
api: list,
|
||||
columns: conditionColumns.slice(0, 4),
|
||||
rowKey: 'id',
|
||||
useSearchForm: true,
|
||||
formConfig: {
|
||||
labelWidth: 90,
|
||||
schemas: searchFormSchema,
|
||||
},
|
||||
pagination: { pageSize: 10 },
|
||||
canResize: false,
|
||||
showIndexColumn: false,
|
||||
immediate: true,
|
||||
beforeFetch: (params) => ({
|
||||
...params,
|
||||
equipmentId: equipmentId.value || undefined,
|
||||
}),
|
||||
rowSelection: {
|
||||
type: 'radio',
|
||||
columnWidth: 48,
|
||||
onChange: (_keys, rows) => {
|
||||
selectedRow.value = rows?.[0] ?? null;
|
||||
},
|
||||
},
|
||||
clickToRowSelect: true,
|
||||
});
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
equipmentId.value = (data?.equipmentId as string) || '';
|
||||
selectedRow.value = null;
|
||||
clearSelectedRowKeys?.();
|
||||
setModalProps({ confirmLoading: false });
|
||||
const conditionId = data?.conditionId as string | undefined;
|
||||
if (conditionId) {
|
||||
setSelectedRowKeys?.([conditionId]);
|
||||
try {
|
||||
const raw = await queryById({ id: conditionId });
|
||||
const row = (raw as Recordable)?.id != null ? raw : (raw as Recordable)?.result;
|
||||
if (row) {
|
||||
selectedRow.value = row;
|
||||
}
|
||||
} catch {
|
||||
// 忽略
|
||||
}
|
||||
}
|
||||
reload();
|
||||
});
|
||||
|
||||
async function handleOk() {
|
||||
const keys = (getSelectRowKeys?.() || []) as string[];
|
||||
let row = selectedRow.value || ((getSelectRows?.() || []) as Recordable[])[0];
|
||||
if (!row && keys.length) {
|
||||
try {
|
||||
const raw = await queryById({ id: keys[0] });
|
||||
row = (raw as Recordable)?.id != null ? raw : (raw as Recordable)?.result;
|
||||
} catch {
|
||||
// 忽略
|
||||
}
|
||||
}
|
||||
if (!row?.id) {
|
||||
createMessage.warning('请选择一条密炼机条件');
|
||||
return;
|
||||
}
|
||||
emit('select', {
|
||||
conditionId: row.id,
|
||||
conditionName: row.conditionName || '',
|
||||
conditionCode: row.conditionCode || '',
|
||||
});
|
||||
closeModal();
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,42 @@
|
||||
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',
|
||||
kindLookup = '/xslmes/mesXslMixerMaterialKindCfg/kindLookup',
|
||||
resolveKind = '/xslmes/mesXslMixerMaterialKindCfg/resolveKind',
|
||||
}
|
||||
|
||||
export const list = (params) => defHttp.get({ url: Api.list, params });
|
||||
|
||||
export const queryById = (params) => defHttp.get({ url: Api.queryById, params });
|
||||
|
||||
export const loadKindLookup = (params?) => defHttp.get({ url: Api.kindLookup, params });
|
||||
|
||||
export const resolveKind = (params) => defHttp.get({ url: Api.resolveKind, 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;
|
||||
@@ -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' },
|
||||
};
|
||||
@@ -0,0 +1,168 @@
|
||||
<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';
|
||||
//update-begin---author:cursor ---date:20260525 for:【XSLMES-20260525-A53】种类配置变更后清除前端 lookup 缓存-----------
|
||||
import { clearMixingMaterialKindLookupCache } from '../mesXslMixingSpec/MesXslMixingSpec.data';
|
||||
//update-end---author:cursor ---date:20260525 for:【XSLMES-20260525-A53】种类配置变更后清除前端 lookup 缓存-----------
|
||||
|
||||
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() {
|
||||
//update-begin---author:cursor ---date:20260525 for:【XSLMES-20260525-A53】种类配置变更后清除前端 lookup 缓存-----------
|
||||
clearMixingMaterialKindLookupCache();
|
||||
//update-end---author:cursor ---date:20260525 for:【XSLMES-20260525-A53】种类配置变更后清除前端 lookup 缓存-----------
|
||||
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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,40 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
|
||||
enum Api {
|
||||
list = '/xslmes/mesXslMixingSpec/list',
|
||||
save = '/xslmes/mesXslMixingSpec/add',
|
||||
edit = '/xslmes/mesXslMixingSpec/edit',
|
||||
deleteOne = '/xslmes/mesXslMixingSpec/delete',
|
||||
deleteBatch = '/xslmes/mesXslMixingSpec/deleteBatch',
|
||||
importExcel = '/xslmes/mesXslMixingSpec/importExcel',
|
||||
exportXls = '/xslmes/mesXslMixingSpec/exportXls',
|
||||
queryById = '/xslmes/mesXslMixingSpec/queryById',
|
||||
queryIssueNumberOptions = '/xslmes/mesXslMixingSpec/queryIssueNumberOptions',
|
||||
queryPurposeOptions = '/xslmes/mesXslMixingSpec/queryPurposeOptions',
|
||||
}
|
||||
|
||||
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 });
|
||||
//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 });
|
||||
|
||||
export const deleteOne = (params, handleSuccess) =>
|
||||
defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => handleSuccess());
|
||||
|
||||
export const batchDelete = (params, handleSuccess) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '是否删除选中数据',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () =>
|
||||
defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => handleSuccess()),
|
||||
});
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div>
|
||||
<BasicTable @register="registerTable" :rowSelection="rowSelection">
|
||||
<template #tableTitle>
|
||||
<a-button type="primary" v-auth="'xslmes:mes_xsl_mixing_spec:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增 </a-button>
|
||||
<a-button type="primary" v-auth="'xslmes:mes_xsl_mixing_spec:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出 </a-button>
|
||||
<j-upload-button type="primary" v-auth="'xslmes:mes_xsl_mixing_spec: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_mixing_spec:deleteBatch'">
|
||||
批量操作
|
||||
<Icon icon="mdi:chevron-down" />
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<TableAction :actions="getTableActions(record)" />
|
||||
</template>
|
||||
</BasicTable>
|
||||
<MesXslMixingSpecModal @register="registerModal" @success="handleSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="xslmes-mesXslMixingSpec" 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 MesXslMixingSpecModal from './components/MesXslMixingSpecModal.vue';
|
||||
import { columns, searchFormSchema } from './MesXslMixingSpec.data';
|
||||
import { list, deleteOne, batchDelete, getExportUrl, getImportUrl } from './MesXslMixingSpec.api';
|
||||
|
||||
const queryParam = reactive<any>({});
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
|
||||
const { tableContext, onExportXls, onImportXls } = useListPage({
|
||||
tableProps: {
|
||||
title: '混炼示方',
|
||||
api: list,
|
||||
columns,
|
||||
canResize: true,
|
||||
formConfig: {
|
||||
schemas: searchFormSchema,
|
||||
labelWidth: 90,
|
||||
autoSubmitOnEnter: 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;
|
||||
|
||||
function handleAdd() {
|
||||
openModal(true, { isUpdate: false, showFooter: true });
|
||||
}
|
||||
|
||||
function handleEdit(record: Recordable) {
|
||||
openModal(true, { record, isUpdate: true, showFooter: true });
|
||||
}
|
||||
|
||||
function handleDetail(record: Recordable) {
|
||||
openModal(true, { record, isUpdate: true, showFooter: false });
|
||||
}
|
||||
|
||||
function handleDelete(record: Recordable) {
|
||||
deleteOne({ id: record.id }, handleSuccess);
|
||||
}
|
||||
|
||||
function batchHandleDelete() {
|
||||
batchDelete({ ids: selectedRowKeys.value.join(',') }, handleSuccess);
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reload();
|
||||
selectedRowKeys.value = [];
|
||||
}
|
||||
|
||||
function getTableActions(record: Recordable) {
|
||||
return [
|
||||
{ label: '编辑', onClick: handleEdit.bind(null, record), auth: 'xslmes:mes_xsl_mixing_spec:edit' },
|
||||
{ label: '详情', onClick: handleDetail.bind(null, record) },
|
||||
{
|
||||
label: '删除',
|
||||
auth: 'xslmes:mes_xsl_mixing_spec:delete',
|
||||
popConfirm: { title: '是否确认删除', confirm: handleDelete.bind(null, record) },
|
||||
},
|
||||
];
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,246 @@
|
||||
<template>
|
||||
<Popover
|
||||
v-model:open="popoverOpen"
|
||||
trigger="click"
|
||||
placement="bottomRight"
|
||||
:getPopupContainer="getPopoverContainer"
|
||||
: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 || refreshing">
|
||||
<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" :loading="refreshing" :disabled="loading" @click="handleRefresh">刷新</a-button>
|
||||
<div :class="`${prefixCls}__footer-actions`">
|
||||
<a-button size="small" :disabled="loading || refreshing || !allCategoryIds.length" @click="handleReset">重置</a-button>
|
||||
<a-button size="small" type="primary" :disabled="loading || refreshing || !allCategoryIds.length" @click="handleSave">保存</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-button size="small" class="mixing-material-category-setting-btn" :disabled="loading" title="小类展示设置">
|
||||
<Icon icon="ant-design:setting-outlined" />
|
||||
</a-button>
|
||||
</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,
|
||||
},
|
||||
refreshing: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:hiddenCategoryIds', 'change', 'refresh']);
|
||||
|
||||
const popoverOpen = ref(false);
|
||||
const draftVisibleIds = ref<string[]>([]);
|
||||
|
||||
function getPopoverContainer() {
|
||||
return document.body;
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
//update-begin---author:cursor ---date:20260525 for:【XSLMES-20260525-A52】小类设置刷新分类字典-----------
|
||||
function handleRefresh() {
|
||||
emit('refresh');
|
||||
}
|
||||
//update-end---author:cursor ---date:20260525 for:【XSLMES-20260525-A52】小类设置刷新分类字典-----------
|
||||
|
||||
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 {
|
||||
z-index: 1600 !important;
|
||||
|
||||
.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;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--border-color-base, #f0f0f0);
|
||||
}
|
||||
|
||||
.mixing-material-category-setting__footer-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,166 @@
|
||||
<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" @change="onCheckAllChange">列展示</Checkbox>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div :class="`${prefixCls}__list`">
|
||||
<CheckboxGroup v-model:value="draftCheckedList" :options="columnOptions" />
|
||||
</div>
|
||||
<div :class="`${prefixCls}__footer`">
|
||||
<a-button size="small" @click="handleReset">重置</a-button>
|
||||
<a-button size="small" type="primary" @click="handleSave">保存</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<a-tooltip title="列设置">
|
||||
<a-button size="small" class="mixing-material-column-setting-btn" @click.stop>
|
||||
<Icon icon="ant-design:setting-outlined" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, type PropType } from 'vue';
|
||||
import { Popover, Checkbox } 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 {
|
||||
MIXING_MATERIAL_LOCKED_COLUMN_KEYS,
|
||||
getMixingMaterialColumnSettingItems,
|
||||
saveMixingMaterialHiddenColumnKeys,
|
||||
type MixingMaterialColumnSettingItem,
|
||||
} from '../MesXslMixingSpec.data';
|
||||
|
||||
const CheckboxGroup = Checkbox.Group;
|
||||
const prefixCls = 'mixing-material-column-setting';
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const props = defineProps({
|
||||
hiddenKeys: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:hiddenKeys', value: string[]): void;
|
||||
(e: 'change', value: string[]): void;
|
||||
}>();
|
||||
|
||||
const popoverOpen = ref(false);
|
||||
const columnItems = ref<MixingMaterialColumnSettingItem[]>(getMixingMaterialColumnSettingItems());
|
||||
const allKeys = computed(() => columnItems.value.map((item) => item.key));
|
||||
const lockableKeys = computed(() => columnItems.value.filter((item) => !item.locked).map((item) => item.key));
|
||||
const draftCheckedList = ref<string[]>([]);
|
||||
|
||||
const columnOptions = computed(() =>
|
||||
columnItems.value.map((item) => ({
|
||||
label: item.title,
|
||||
value: item.key,
|
||||
disabled: item.locked,
|
||||
})),
|
||||
);
|
||||
|
||||
const checkAll = computed(() => {
|
||||
const keys = lockableKeys.value;
|
||||
return keys.length > 0 && keys.every((key) => draftCheckedList.value.includes(key));
|
||||
});
|
||||
|
||||
const indeterminate = computed(() => {
|
||||
const keys = lockableKeys.value;
|
||||
const checkedCount = keys.filter((key) => draftCheckedList.value.includes(key)).length;
|
||||
return checkedCount > 0 && checkedCount < keys.length;
|
||||
});
|
||||
|
||||
function ensureLockedChecked() {
|
||||
const next = new Set(draftCheckedList.value);
|
||||
MIXING_MATERIAL_LOCKED_COLUMN_KEYS.forEach((key) => next.add(key));
|
||||
draftCheckedList.value = Array.from(next);
|
||||
}
|
||||
|
||||
function syncDraftFromHidden(hiddenKeys: string[]) {
|
||||
const hiddenSet = new Set(hiddenKeys || []);
|
||||
draftCheckedList.value = allKeys.value.filter((key) => !hiddenSet.has(key));
|
||||
ensureLockedChecked();
|
||||
}
|
||||
|
||||
function buildHiddenKeysFromDraft() {
|
||||
ensureLockedChecked();
|
||||
return allKeys.value.filter((key) => !draftCheckedList.value.includes(key));
|
||||
}
|
||||
|
||||
function handleOpenChange(open: boolean) {
|
||||
if (open) {
|
||||
syncDraftFromHidden(props.hiddenKeys);
|
||||
}
|
||||
}
|
||||
|
||||
function onCheckAllChange(e: CheckboxChangeEvent) {
|
||||
draftCheckedList.value = e.target.checked ? [...allKeys.value] : [...MIXING_MATERIAL_LOCKED_COLUMN_KEYS];
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
draftCheckedList.value = [...allKeys.value];
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
const hiddenKeys = buildHiddenKeysFromDraft();
|
||||
saveMixingMaterialHiddenColumnKeys(hiddenKeys);
|
||||
emit('update:hiddenKeys', hiddenKeys);
|
||||
emit('change', hiddenKeys);
|
||||
createMessage.success('保存成功');
|
||||
popoverOpen.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.mixing-material-column-setting-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-inline: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.mixing-material-column-setting__popover {
|
||||
.mixing-material-column-setting__title {
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.mixing-material-column-setting__list {
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.ant-checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ant-checkbox-group-item {
|
||||
margin-inline-start: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.mixing-material-column-setting__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 4px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,462 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
v-bind="$attrs"
|
||||
title="选择密炼物料"
|
||||
:width="1280"
|
||||
:zIndex="1500"
|
||||
wrapClassName="mixing-material-picker-modal-wrap"
|
||||
@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"
|
||||
:refreshing="categoryRefreshing"
|
||||
@change="handleCategoryVisibilityChange"
|
||||
@refresh="handleRefreshCategoryTree"
|
||||
/>
|
||||
</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 v-else-if="column.dataIndex === 'pickerMaterialKind'">
|
||||
{{ resolvePickerMaterialKind(record) }}
|
||||
</template>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, 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 { loadMesMaterialCategoryTreeData, hasMesMaterialCategoryTreeCache } 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,
|
||||
EMPTY_MIXER_MATERIAL_KIND_LOOKUP,
|
||||
loadMixingMaterialKindLookup,
|
||||
loadMixingMaterialPickerHiddenCategoryIds,
|
||||
MIXING_MATERIAL_PICKER_WEIGH_MODE_DICT,
|
||||
mixingMaterialPickerTableColumns,
|
||||
resolveMixingMaterialKindForPicker,
|
||||
sanitizeMixingMaterialPickerHiddenCategoryIds,
|
||||
saveMixingMaterialPickerHiddenCategoryIds,
|
||||
type MixingMaterialPickerCategoryItem,
|
||||
type MixerMaterialKindLookup,
|
||||
} 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 kindLookup = ref<MixerMaterialKindLookup>(EMPTY_MIXER_MATERIAL_KIND_LOOKUP);
|
||||
const pickerWeighModeMap = ref<Record<string, string>>({});
|
||||
const pickerInitializing = ref(false);
|
||||
const categoryRefreshing = ref(false);
|
||||
|
||||
const hiddenCategoryIdSet = computed(() => new Set(hiddenCategoryIds.value.map(String)));
|
||||
|
||||
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: false,
|
||||
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 () => {
|
||||
setModalProps({ zIndex: 1500 });
|
||||
await initPickerModal();
|
||||
});
|
||||
|
||||
async function initPickerModal() {
|
||||
pickerInitializing.value = true;
|
||||
selectedRow.value = null;
|
||||
keyword.value = '';
|
||||
pickerWeighModeMap.value = {};
|
||||
clearSelectedRowKeys?.();
|
||||
hiddenCategoryIds.value = loadMixingMaterialPickerHiddenCategoryIds();
|
||||
selectedCategoryKeys.value = [TREE_ALL];
|
||||
setModalProps({ confirmLoading: false });
|
||||
//update-begin---author:cursor ---date:20260525 for:【XSLMES-20260525-A52】选料弹窗先加载左侧树再查右侧列表-----------
|
||||
try {
|
||||
await loadMaterialCategoryTree();
|
||||
kindLookup.value = await loadMixingMaterialKindLookup(false);
|
||||
reloadTable();
|
||||
} finally {
|
||||
pickerInitializing.value = false;
|
||||
}
|
||||
//update-end---author:cursor ---date:20260525 for:【XSLMES-20260525-A52】选料弹窗先加载左侧树再查右侧列表-----------
|
||||
}
|
||||
//update-end---author:cursor ---date:20260525 for:【XSLMES-20260525-A50】选料弹窗打开时初始化(对齐其他SelectModal)-----------
|
||||
|
||||
async function loadMaterialCategoryTree(forceReload = false) {
|
||||
const hasCachedTree = !forceReload && (hasMesMaterialCategoryTreeCache() || rawCategoryTree.value.length > 0);
|
||||
if (forceReload || !hasCachedTree) {
|
||||
treeLoading.value = true;
|
||||
}
|
||||
try {
|
||||
const { majors, minors, treeNodes } = await loadMesMaterialCategoryTreeData(forceReload);
|
||||
applyMaterialCategoryTreeData(majors, minors, treeNodes);
|
||||
} catch {
|
||||
rawCategoryTree.value = [];
|
||||
allMajorCategories.value = [];
|
||||
allMinorCategories.value = [];
|
||||
createMessage.warning('加载物料分类树失败,请检查分类根编码 XSLMES_MATERIAL 是否存在。');
|
||||
} finally {
|
||||
treeLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function applyMaterialCategoryTreeData(
|
||||
majors: Array<{ id: string; name: string; minors: MixingMaterialPickerCategoryItem[] }>,
|
||||
minors: MixingMaterialPickerCategoryItem[],
|
||||
treeNodes: Recordable[],
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
||||
syncExpandedCategoryKeys();
|
||||
|
||||
if (!minors.length) {
|
||||
createMessage.warning('未加载到物料小类,请确认分类字典根编码 XSLMES_MATERIAL 及其下级分类已配置。');
|
||||
}
|
||||
}
|
||||
|
||||
//update-begin---author:cursor ---date:20260525 for:【XSLMES-20260525-A52】小类设置刷新分类字典-----------
|
||||
/** 刷新分类字典:重新拉取小类列表,新增小类默认隐藏,勾选保存后展示到左侧树 */
|
||||
async function handleRefreshCategoryTree() {
|
||||
if (categoryRefreshing.value) {
|
||||
return;
|
||||
}
|
||||
const previousIds = new Set(allMinorCategories.value.map((item) => String(item.id)));
|
||||
categoryRefreshing.value = true;
|
||||
try {
|
||||
await loadMaterialCategoryTree(true);
|
||||
const newMinorIds = allMinorCategories.value
|
||||
.filter((item) => !previousIds.has(String(item.id)))
|
||||
.map((item) => String(item.id));
|
||||
if (newMinorIds.length) {
|
||||
const nextHidden = sanitizeMixingMaterialPickerHiddenCategoryIds(
|
||||
allMinorCategories.value.map((item) => item.id),
|
||||
[...hiddenCategoryIds.value.map(String), ...newMinorIds],
|
||||
);
|
||||
hiddenCategoryIds.value = nextHidden;
|
||||
createMessage.success(`已刷新,发现 ${newMinorIds.length} 个新小类,请勾选后点击保存`);
|
||||
} else {
|
||||
createMessage.success('已刷新,分类无变化');
|
||||
}
|
||||
} finally {
|
||||
categoryRefreshing.value = false;
|
||||
}
|
||||
}
|
||||
//update-end---author:cursor ---date:20260525 for:【XSLMES-20260525-A52】小类设置刷新分类字典-----------
|
||||
|
||||
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];
|
||||
if (pickerInitializing.value) {
|
||||
return;
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
function resolvePickerMaterialKind(material: Recordable) {
|
||||
const weighMode = getPickerWeighMode(material?.id);
|
||||
const minorId = material?.minorCategoryId ? String(material.minorCategoryId) : '';
|
||||
const minorName = material?.minorCategoryId_dictText || '';
|
||||
return resolveMixingMaterialKindForPicker(kindLookup.value, weighMode, minorId, minorName);
|
||||
}
|
||||
|
||||
function resolveKindForMaterial(material: Recordable, weighMode?: string) {
|
||||
const minorId = material?.minorCategoryId ? String(material.minorCategoryId) : '';
|
||||
const minorName = material?.minorCategoryId_dictText || '';
|
||||
return resolveMixingMaterialKindForPicker(kindLookup.value, weighMode, minorId, 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 = resolveKindForMaterial(row, weighMode);
|
||||
//update-begin---author:cursor ---date:20260525 for:【XSLMES-20260525-A53】选料未匹配种类时提示检查配置-----------
|
||||
if (!materialKind) {
|
||||
createMessage.warning('未匹配到种类,请检查密炼物料种类配置');
|
||||
return;
|
||||
}
|
||||
//update-end---author:cursor ---date:20260525 for:【XSLMES-20260525-A53】选料未匹配种类时提示检查配置-----------
|
||||
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">
|
||||
/* 嵌套在全屏混炼示方弹窗之上,避免偶发被父弹窗遮挡 */
|
||||
.mixing-material-picker-modal-wrap {
|
||||
z-index: 1500 !important;
|
||||
}
|
||||
|
||||
/* 下拉挂到 body,避免表格 overflow 裁剪;层级高于 Modal */
|
||||
.mixing-material-picker-weigh-mode-dropdown {
|
||||
z-index: 2100 !important;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
v-bind="$attrs"
|
||||
title="选取混炼示方"
|
||||
:width="960"
|
||||
:zIndex="1500"
|
||||
:showOkBtn="false"
|
||||
:showCancelBtn="false"
|
||||
wrapClassName="mixing-spec-picker-modal-wrap"
|
||||
@register="registerModal"
|
||||
>
|
||||
<div class="mixing-spec-picker">
|
||||
<div class="mixing-spec-picker-toolbar">
|
||||
<span class="mixing-spec-picker-label">关键字</span>
|
||||
<a-input
|
||||
v-model:value="keyword"
|
||||
allow-clear
|
||||
placeholder="规格/用途/发行编号/机台"
|
||||
style="width: 240px"
|
||||
@pressEnter="reloadTable"
|
||||
/>
|
||||
<span class="mixing-spec-picker-label">机台</span>
|
||||
<a-select
|
||||
v-model:value="machineId"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
placeholder="==请选择=="
|
||||
style="width: 160px"
|
||||
:options="machineOptions"
|
||||
:loading="machineLoading"
|
||||
:getPopupContainer="getSelectPopupContainer"
|
||||
popupClassName="mixing-spec-picker-machine-dropdown"
|
||||
@click.stop
|
||||
/>
|
||||
<a-button type="primary" @click="reloadTable">搜索</a-button>
|
||||
</div>
|
||||
<BasicTable @register="registerTable" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<a-button @click="handleClose">关闭</a-button>
|
||||
<a-button type="primary" :loading="confirmLoading" @click="handleConfirm">确认</a-button>
|
||||
<a-button type="primary" :loading="referenceLoading" @click="handleReferenceAdd">参照选中示方新增</a-button>
|
||||
</template>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicTable, useTable } from '/@/components/Table';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { list as mixingSpecList, queryById as queryMixingSpecById } from '../MesXslMixingSpec.api';
|
||||
import { mixingSpecHistorySelectColumns } from '../MesXslMixingSpec.data';
|
||||
import { list as equipmentList } from '/@/views/xslmes/mesXslEquipmentLedger/MesXslEquipmentLedger.api';
|
||||
|
||||
const emit = defineEmits(['register', 'edit', 'referenceAdd']);
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const keyword = ref('');
|
||||
const machineId = ref<string | undefined>(undefined);
|
||||
const machineOptions = ref<Array<{ label: string; value: string }>>([]);
|
||||
const machineLoading = ref(false);
|
||||
const confirmLoading = ref(false);
|
||||
const referenceLoading = ref(false);
|
||||
const selectedRow = ref<Recordable | null>(null);
|
||||
|
||||
function getSelectPopupContainer() {
|
||||
return document.body;
|
||||
}
|
||||
|
||||
const [registerTable, { reload, getSelectRowKeys, getSelectRows, setSelectedRowKeys, clearSelectedRowKeys }] = useTable({
|
||||
api: mixingSpecList,
|
||||
columns: mixingSpecHistorySelectColumns,
|
||||
rowKey: 'id',
|
||||
useSearchForm: false,
|
||||
pagination: { pageSize: 30 },
|
||||
canResize: false,
|
||||
showIndexColumn: true,
|
||||
immediate: false,
|
||||
beforeFetch: (params) => ({
|
||||
...params,
|
||||
keyword: keyword.value?.trim() || undefined,
|
||||
machineId: machineId.value || undefined,
|
||||
}),
|
||||
rowSelection: {
|
||||
type: 'radio',
|
||||
columnWidth: 48,
|
||||
onChange: (_keys, rows) => {
|
||||
selectedRow.value = rows?.[0] ?? null;
|
||||
},
|
||||
},
|
||||
clickToRowSelect: true,
|
||||
});
|
||||
|
||||
async function loadMachineOptions() {
|
||||
machineLoading.value = true;
|
||||
try {
|
||||
const optionMap = new Map<string, { label: string; value: string }>();
|
||||
const raw = await mixingSpecList({ pageNo: 1, pageSize: 500 });
|
||||
const page = (raw as Recordable)?.records != null ? raw : (raw as Recordable)?.result;
|
||||
const specRecords = ((page?.records || page || []) as Recordable[]).filter(Boolean);
|
||||
specRecords.forEach((row) => {
|
||||
if (!row?.machineId) {
|
||||
return;
|
||||
}
|
||||
const value = String(row.machineId);
|
||||
optionMap.set(value, {
|
||||
value,
|
||||
label: row.machineName || value,
|
||||
});
|
||||
});
|
||||
if (!optionMap.size) {
|
||||
const eqRaw = await equipmentList({ pageNo: 1, pageSize: 500 });
|
||||
const eqPage = (eqRaw as Recordable)?.records != null ? eqRaw : (eqRaw as Recordable)?.result;
|
||||
const eqRecords = ((eqPage?.records || eqPage || []) as Recordable[]).filter(Boolean);
|
||||
eqRecords.forEach((row) => {
|
||||
if (!row?.id) {
|
||||
return;
|
||||
}
|
||||
const value = String(row.id);
|
||||
optionMap.set(value, {
|
||||
value,
|
||||
label: row.equipmentName || row.equipmentCode || value,
|
||||
});
|
||||
});
|
||||
}
|
||||
machineOptions.value = Array.from(optionMap.values()).sort((a, b) =>
|
||||
a.label.localeCompare(b.label, 'zh-CN'),
|
||||
);
|
||||
} catch {
|
||||
machineOptions.value = [];
|
||||
} finally {
|
||||
machineLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadTable() {
|
||||
reload();
|
||||
}
|
||||
|
||||
async function resolveSelectedRow(): Promise<Recordable | null> {
|
||||
const keys = (getSelectRowKeys?.() || []) as string[];
|
||||
let row = selectedRow.value || ((getSelectRows?.() || []) as Recordable[])[0];
|
||||
if (!row?.id && keys.length) {
|
||||
try {
|
||||
const raw = await queryMixingSpecById({ id: keys[0] });
|
||||
row = (raw as Recordable)?.specName != null ? raw : (raw as Recordable)?.result;
|
||||
} catch {
|
||||
row = null;
|
||||
}
|
||||
}
|
||||
return row?.id ? row : null;
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
async function handleConfirm() {
|
||||
const row = await resolveSelectedRow();
|
||||
if (!row) {
|
||||
createMessage.warning('请选择一条混炼示方');
|
||||
return;
|
||||
}
|
||||
confirmLoading.value = true;
|
||||
try {
|
||||
emit('edit', {
|
||||
mixingSpecId: row.id,
|
||||
specName: row.specName || '',
|
||||
});
|
||||
closeModal();
|
||||
} finally {
|
||||
confirmLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReferenceAdd() {
|
||||
const row = await resolveSelectedRow();
|
||||
if (!row) {
|
||||
createMessage.warning('请选择一条混炼示方');
|
||||
return;
|
||||
}
|
||||
referenceLoading.value = true;
|
||||
try {
|
||||
emit('referenceAdd', {
|
||||
mixingSpecId: row.id,
|
||||
specName: row.specName || '',
|
||||
});
|
||||
closeModal();
|
||||
} finally {
|
||||
referenceLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
keyword.value = '';
|
||||
machineId.value = data?.machineId || undefined;
|
||||
selectedRow.value = null;
|
||||
confirmLoading.value = false;
|
||||
referenceLoading.value = false;
|
||||
clearSelectedRowKeys?.();
|
||||
setModalProps({ confirmLoading: false });
|
||||
await loadMachineOptions();
|
||||
reload();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.mixing-spec-picker-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mixing-spec-picker-label {
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.mixing-spec-picker-modal-wrap {
|
||||
.ant-modal {
|
||||
top: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.mixing-spec-picker-machine-dropdown {
|
||||
z-index: 2100 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
v-bind="$attrs"
|
||||
title="选取混合示方(双击表格可选择)"
|
||||
:width="960"
|
||||
:zIndex="1500"
|
||||
wrapClassName="mixing-step-history-picker-modal-wrap"
|
||||
@register="registerModal"
|
||||
@ok="handleOk"
|
||||
>
|
||||
<div class="mixing-step-history-picker">
|
||||
<div class="mixing-step-history-picker-toolbar">
|
||||
<span class="mixing-step-history-picker-label">关键字</span>
|
||||
<a-input
|
||||
v-model:value="keyword"
|
||||
allow-clear
|
||||
placeholder="规格/用途/发行编号/机台"
|
||||
style="width: 240px"
|
||||
@pressEnter="reloadTable"
|
||||
/>
|
||||
<span class="mixing-step-history-picker-label">机台</span>
|
||||
<a-select
|
||||
v-model:value="machineId"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
placeholder="==请选择=="
|
||||
style="width: 160px"
|
||||
:options="machineOptions"
|
||||
:loading="machineLoading"
|
||||
:getPopupContainer="getSelectPopupContainer"
|
||||
popupClassName="mixing-step-history-picker-machine-dropdown"
|
||||
@click.stop
|
||||
/>
|
||||
<a-button type="primary" @click="reloadTable">搜索</a-button>
|
||||
</div>
|
||||
<BasicTable @register="registerTable" @row-dbClick="handleRowDbClick" />
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicTable, useTable } from '/@/components/Table';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { list as mixingSpecList, queryById as queryMixingSpecById } from '../MesXslMixingSpec.api';
|
||||
import { mixingSpecHistorySelectColumns } from '../MesXslMixingSpec.data';
|
||||
import { list as equipmentList } from '/@/views/xslmes/mesXslEquipmentLedger/MesXslEquipmentLedger.api';
|
||||
|
||||
const emit = defineEmits(['register', 'select']);
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const keyword = ref('');
|
||||
const machineId = ref<string | undefined>(undefined);
|
||||
const machineOptions = ref<Array<{ label: string; value: string }>>([]);
|
||||
const machineLoading = ref(false);
|
||||
const excludeSpecId = ref('');
|
||||
const selectedRow = ref<Recordable | null>(null);
|
||||
|
||||
function getSelectPopupContainer() {
|
||||
return document.body;
|
||||
}
|
||||
|
||||
const [registerTable, { reload, getSelectRowKeys, getSelectRows, setSelectedRowKeys, clearSelectedRowKeys }] = useTable({
|
||||
api: mixingSpecList,
|
||||
columns: mixingSpecHistorySelectColumns,
|
||||
rowKey: 'id',
|
||||
useSearchForm: false,
|
||||
pagination: { pageSize: 30 },
|
||||
canResize: false,
|
||||
showIndexColumn: true,
|
||||
immediate: false,
|
||||
beforeFetch: (params) => ({
|
||||
...params,
|
||||
keyword: keyword.value?.trim() || undefined,
|
||||
machineId: machineId.value || undefined,
|
||||
}),
|
||||
afterFetch: (rows) => {
|
||||
if (!excludeSpecId.value) {
|
||||
return rows;
|
||||
}
|
||||
return (rows || []).filter((row) => row?.id !== excludeSpecId.value);
|
||||
},
|
||||
rowSelection: {
|
||||
type: 'radio',
|
||||
columnWidth: 48,
|
||||
onChange: (_keys, rows) => {
|
||||
selectedRow.value = rows?.[0] ?? null;
|
||||
},
|
||||
},
|
||||
clickToRowSelect: true,
|
||||
});
|
||||
|
||||
async function loadMachineOptions() {
|
||||
machineLoading.value = true;
|
||||
try {
|
||||
const optionMap = new Map<string, { label: string; value: string }>();
|
||||
const raw = await mixingSpecList({ pageNo: 1, pageSize: 500 });
|
||||
const page = (raw as Recordable)?.records != null ? raw : (raw as Recordable)?.result;
|
||||
const specRecords = ((page?.records || page || []) as Recordable[]).filter(Boolean);
|
||||
specRecords.forEach((row) => {
|
||||
if (!row?.machineId) {
|
||||
return;
|
||||
}
|
||||
const value = String(row.machineId);
|
||||
optionMap.set(value, {
|
||||
value,
|
||||
label: row.machineName || value,
|
||||
});
|
||||
});
|
||||
if (!optionMap.size) {
|
||||
const eqRaw = await equipmentList({ pageNo: 1, pageSize: 500 });
|
||||
const eqPage = (eqRaw as Recordable)?.records != null ? eqRaw : (eqRaw as Recordable)?.result;
|
||||
const eqRecords = ((eqPage?.records || eqPage || []) as Recordable[]).filter(Boolean);
|
||||
eqRecords.forEach((row) => {
|
||||
if (!row?.id) {
|
||||
return;
|
||||
}
|
||||
const value = String(row.id);
|
||||
optionMap.set(value, {
|
||||
value,
|
||||
label: row.equipmentName || row.equipmentCode || value,
|
||||
});
|
||||
});
|
||||
}
|
||||
machineOptions.value = Array.from(optionMap.values()).sort((a, b) =>
|
||||
a.label.localeCompare(b.label, 'zh-CN'),
|
||||
);
|
||||
} catch {
|
||||
machineOptions.value = [];
|
||||
} finally {
|
||||
machineLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadTable() {
|
||||
reload();
|
||||
}
|
||||
|
||||
async function resolveSelectedRow(): Promise<Recordable | null> {
|
||||
const keys = (getSelectRowKeys?.() || []) as string[];
|
||||
let row = selectedRow.value || ((getSelectRows?.() || []) as Recordable[])[0];
|
||||
if (!row?.id && keys.length) {
|
||||
try {
|
||||
const raw = await queryMixingSpecById({ id: keys[0] });
|
||||
row = (raw as Recordable)?.specName != null ? raw : (raw as Recordable)?.result;
|
||||
} catch {
|
||||
row = null;
|
||||
}
|
||||
}
|
||||
return row?.id ? row : null;
|
||||
}
|
||||
|
||||
async function confirmSelect(row: Recordable) {
|
||||
if (!row?.id) {
|
||||
createMessage.warning('请选择一条混炼示方');
|
||||
return;
|
||||
}
|
||||
if (row.id === excludeSpecId.value) {
|
||||
createMessage.warning('不能参照当前正在编辑的混炼示方');
|
||||
return;
|
||||
}
|
||||
emit('select', {
|
||||
mixingSpecId: row.id,
|
||||
specName: row.specName || '',
|
||||
machineName: row.machineName || '',
|
||||
issueNumber: row.issueNumber || '',
|
||||
});
|
||||
closeModal();
|
||||
}
|
||||
|
||||
async function handleOk() {
|
||||
const row = await resolveSelectedRow();
|
||||
if (!row) {
|
||||
createMessage.warning('请选择一条混炼示方');
|
||||
return;
|
||||
}
|
||||
await confirmSelect(row);
|
||||
}
|
||||
|
||||
async function handleRowDbClick(record: Recordable) {
|
||||
if (!record?.id) {
|
||||
return;
|
||||
}
|
||||
setSelectedRowKeys?.([record.id]);
|
||||
selectedRow.value = record;
|
||||
await confirmSelect(record);
|
||||
}
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
keyword.value = '';
|
||||
machineId.value = data?.machineId || undefined;
|
||||
excludeSpecId.value = data?.excludeSpecId || '';
|
||||
selectedRow.value = null;
|
||||
clearSelectedRowKeys?.();
|
||||
setModalProps({ confirmLoading: false });
|
||||
await loadMachineOptions();
|
||||
reload();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.mixing-step-history-picker-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mixing-step-history-picker-label {
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
/* 嵌套在全屏混炼示方弹窗之上,避免被父弹窗遮挡 */
|
||||
.mixing-step-history-picker-modal-wrap {
|
||||
.ant-modal {
|
||||
top: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 机台下拉挂到 body,避免弹窗 overflow 裁剪;层级高于 Modal */
|
||||
.mixing-step-history-picker-machine-dropdown {
|
||||
z-index: 2100 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<a-select
|
||||
:value="row[field]"
|
||||
:options="options"
|
||||
:disabled="disabled"
|
||||
:open="dropdownOpen"
|
||||
allow-clear
|
||||
show-search
|
||||
:bordered="false"
|
||||
size="small"
|
||||
class="mixing-step-select"
|
||||
popup-class-name="mixing-step-select-dropdown"
|
||||
:list-height="listHeight"
|
||||
:placeholder="placeholder"
|
||||
:get-popup-container="getPopupContainer"
|
||||
@update:value="handleChange"
|
||||
@dropdown-visible-change="handleDropdownVisibleChange"
|
||||
@click.stop
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
/** 下拉单项高度(与 ant-design-vue small Select 选项行高一致) */
|
||||
const STEP_SELECT_ITEM_HEIGHT = 32;
|
||||
/** 下拉最多可见条数 */
|
||||
const STEP_SELECT_MAX_VISIBLE = 20;
|
||||
|
||||
const props = defineProps<{
|
||||
row: Recordable;
|
||||
field: string;
|
||||
options: { label: string; value: string }[];
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
machineId?: string;
|
||||
}>();
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const dropdownOpen = ref(false);
|
||||
|
||||
//update-begin---author:cursor ---date:20260525 for:【XSLMES-20260525-A57】混合步骤动作/组合下拉最多展示20条-----------
|
||||
const listHeight = computed(() => {
|
||||
const count = props.options?.length ?? 0;
|
||||
const visibleCount = count > 0 ? Math.min(count, STEP_SELECT_MAX_VISIBLE) : STEP_SELECT_MAX_VISIBLE;
|
||||
return visibleCount * STEP_SELECT_ITEM_HEIGHT;
|
||||
});
|
||||
//update-end---author:cursor ---date:20260525 for:【XSLMES-20260525-A57】混合步骤动作/组合下拉最多展示20条-----------
|
||||
|
||||
function handleChange(value: string | undefined) {
|
||||
props.row[props.field] = value;
|
||||
}
|
||||
|
||||
//update-begin---author:cursor ---date:20260522 for:【XSLMES-20260522-A37】未选机台点击动作/组合提示请先选择机台-----------
|
||||
function handleDropdownVisibleChange(visible: boolean) {
|
||||
if (props.disabled) {
|
||||
dropdownOpen.value = false;
|
||||
return;
|
||||
}
|
||||
if (visible && !props.machineId) {
|
||||
createMessage.warning('请先选择机台');
|
||||
dropdownOpen.value = false;
|
||||
return;
|
||||
}
|
||||
dropdownOpen.value = visible;
|
||||
}
|
||||
//update-end---author:cursor ---date:20260522 for:【XSLMES-20260522-A37】未选机台点击动作/组合提示请先选择机台-----------
|
||||
|
||||
//update-begin---author:cursor ---date:20260522 for:【XSLMES-20260522-A36】动作/组合下拉挂到body避免被表格裁切-----------
|
||||
function getPopupContainer() {
|
||||
return document.body;
|
||||
}
|
||||
//update-end---author:cursor ---date:20260522 for:【XSLMES-20260522-A36】动作/组合下拉挂到body避免被表格裁切-----------
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.mixing-step-select {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
:deep(.ant-select) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-select-selector) {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
background: transparent !important;
|
||||
padding: 0 18px 0 2px !important;
|
||||
min-height: 24px !important;
|
||||
height: 100% !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
:deep(.ant-select-selection-item),
|
||||
:deep(.ant-select-selection-placeholder) {
|
||||
line-height: 1.2 !important;
|
||||
text-align: center;
|
||||
padding-inline-end: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.ant-select-selection-search-input) {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
:deep(.ant-select-arrow) {
|
||||
right: 2px;
|
||||
margin-top: -3px;
|
||||
color: #666;
|
||||
font-size: 10px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.ant-select-clear) {
|
||||
right: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<Popover
|
||||
v-model:open="popoverOpen"
|
||||
trigger="click"
|
||||
placement="bottomRight"
|
||||
:overlayClassName="`${prefixCls}__popover`"
|
||||
@open-change="handleOpenChange"
|
||||
>
|
||||
<template #title>
|
||||
<div :class="`${prefixCls}__title`">{{ meta.label }} - 行高设置</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div :class="`${prefixCls}__form`">
|
||||
<div :class="`${prefixCls}__field`">
|
||||
<span class="field-label">行高(px)</span>
|
||||
<InputNumber
|
||||
v-model:value="draftRowHeight"
|
||||
:min="MIXING_TABLE_ROW_HEIGHT_MIN"
|
||||
:max="MIXING_TABLE_ROW_HEIGHT_MAX"
|
||||
:step="1"
|
||||
size="small"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
<div :class="`${prefixCls}__field`">
|
||||
<span class="field-label">展示行数</span>
|
||||
<InputNumber
|
||||
v-model:value="draftVisibleRowCount"
|
||||
:min="meta.minVisibleRowCount"
|
||||
:max="meta.maxVisibleRowCount"
|
||||
:step="1"
|
||||
size="small"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
<div :class="`${prefixCls}__hint`">展示行数为列表可视区域高度,数据超出时可滚动查看。</div>
|
||||
</div>
|
||||
<div :class="`${prefixCls}__footer`">
|
||||
<a-button size="small" @click="handleReset">重置</a-button>
|
||||
<a-button size="small" type="primary" @click="handleSave">保存</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<a-tooltip title="行高设置">
|
||||
<a-button size="small" :class="`${prefixCls}-btn`" @click.stop>
|
||||
<Icon icon="ant-design:column-height-outlined" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, type PropType } from 'vue';
|
||||
import { Popover, InputNumber } from 'ant-design-vue';
|
||||
import { Icon } from '/@/components/Icon';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import {
|
||||
MIXING_TABLE_HEIGHT_SETTING_META,
|
||||
MIXING_TABLE_ROW_HEIGHT_MAX,
|
||||
MIXING_TABLE_ROW_HEIGHT_MIN,
|
||||
getMixingTableHeightDefault,
|
||||
normalizeMixingTableHeightPreference,
|
||||
saveMixingTableHeightPreference,
|
||||
type MixingDetailTableKey,
|
||||
type MixingTableHeightPreference,
|
||||
} from '../MesXslMixingSpec.data';
|
||||
|
||||
const prefixCls = 'mixing-table-row-height-setting';
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const props = defineProps({
|
||||
tableKey: {
|
||||
type: String as PropType<MixingDetailTableKey>,
|
||||
required: true,
|
||||
},
|
||||
preference: {
|
||||
type: Object as PropType<MixingTableHeightPreference>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:preference', value: MixingTableHeightPreference): void;
|
||||
(e: 'change', value: MixingTableHeightPreference): void;
|
||||
}>();
|
||||
|
||||
const popoverOpen = ref(false);
|
||||
const meta = computed(() => MIXING_TABLE_HEIGHT_SETTING_META[props.tableKey]);
|
||||
const draftRowHeight = ref(meta.value.defaultRowHeight);
|
||||
const draftVisibleRowCount = ref(meta.value.defaultVisibleRowCount);
|
||||
|
||||
function syncDraftFromPreference(preference: MixingTableHeightPreference) {
|
||||
const normalized = normalizeMixingTableHeightPreference(props.tableKey, preference);
|
||||
draftRowHeight.value = normalized.rowHeight;
|
||||
draftVisibleRowCount.value = normalized.visibleRowCount;
|
||||
}
|
||||
|
||||
function handleOpenChange(open: boolean) {
|
||||
if (open) {
|
||||
syncDraftFromPreference(props.preference);
|
||||
}
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
syncDraftFromPreference(getMixingTableHeightDefault(props.tableKey));
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
const next = normalizeMixingTableHeightPreference(props.tableKey, {
|
||||
rowHeight: draftRowHeight.value,
|
||||
visibleRowCount: draftVisibleRowCount.value,
|
||||
});
|
||||
saveMixingTableHeightPreference(props.tableKey, next);
|
||||
emit('update:preference', next);
|
||||
emit('change', next);
|
||||
createMessage.success('保存成功');
|
||||
popoverOpen.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.mixing-table-row-height-setting-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-inline: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.mixing-table-row-height-setting__popover {
|
||||
.mixing-table-row-height-setting__title {
|
||||
min-width: 180px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mixing-table-row-height-setting__form {
|
||||
width: 220px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mixing-table-row-height-setting__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.field-label {
|
||||
font-size: 12px;
|
||||
color: #595959;
|
||||
}
|
||||
}
|
||||
|
||||
.mixing-table-row-height-setting__hint {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.mixing-table-row-height-setting__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 4px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user