生产环节优化

This commit is contained in:
2026-06-11 10:06:26 +08:00
parent b9be88ae3f
commit 3431cc6b17
32 changed files with 2237 additions and 52 deletions

View File

@@ -0,0 +1,7 @@
<template>
<MesXslMixingProductionPlanList />
</template>
<script lang="ts" setup>
import MesXslMixingProductionPlanList from '../../xslmes/mesXslMixingProductionPlan/MesXslMixingProductionPlanList.vue';
</script>

View File

@@ -0,0 +1,7 @@
<template>
<MesXslRawMaterialDemandPlanList />
</template>
<script lang="ts" setup>
import MesXslRawMaterialDemandPlanList from '../../xslmes/mesXslRawMaterialDemandPlan/MesXslRawMaterialDemandPlanList.vue';
</script>

View File

@@ -0,0 +1,13 @@
import { defHttp } from '/@/utils/http/axios';
enum Api {
list = '/xslmes/mesXslMixingProductionPlan/list',
saveAll = '/xslmes/mesXslMixingProductionPlan/saveAll',
orderOptionPage = '/xslmes/mesXslMixingProductionPlan/orderOptionPage',
}
export const list = (params) => defHttp.get({ url: Api.list, params });
export const saveAll = (params) => defHttp.post({ url: Api.saveAll, params });
export const orderOptionPage = (params) => defHttp.get({ url: Api.orderOptionPage, params }, { successMessageMode: 'none' });

View File

@@ -0,0 +1,339 @@
<template>
<div class="mixing-plan-page">
<div class="mixing-plan-toolbar">
<a-button type="primary" preIcon="ant-design:save-outlined" :loading="saving" @click="handleSaveAll">保存</a-button>
</div>
<a-table
:columns="columns"
:data-source="rows"
:pagination="false"
:scroll="{ x: 1560 }"
row-key="_rowKey"
size="small"
bordered
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'machineName'">
<a-input
:value="record.machineName"
readonly
placeholder="点击选择机台"
class="picker-input"
@click="openMachinePicker(index)"
/>
</template>
<template
v-else-if="
column.dataIndex === 'morningOrderNo' ||
column.dataIndex === 'noonOrderNo' ||
column.dataIndex === 'nightOrderNo'
"
>
<a-input
:value="record[column.dataIndex]"
readonly
placeholder="点击选择生产订单计划"
class="picker-input"
@click="openOrderPicker(index, shiftByOrderField(column.dataIndex as string))"
/>
</template>
<template
v-else-if="
column.dataIndex === 'morningFormulaName' ||
column.dataIndex === 'noonFormulaName' ||
column.dataIndex === 'nightFormulaName'
"
>
<a-input :value="record[column.dataIndex]" readonly />
</template>
<template
v-else-if="
column.dataIndex === 'morningPlanCount' ||
column.dataIndex === 'noonPlanCount' ||
column.dataIndex === 'nightPlanCount'
"
>
<a-input-number
:value="record[column.dataIndex]"
:min="0"
:precision="0"
:controls="false"
style="width: 100%"
@change="(v) => updateCell(index, column.dataIndex as string, v)"
/>
</template>
<template
v-else-if="
column.dataIndex === 'morningRemark' || column.dataIndex === 'noonRemark' || column.dataIndex === 'nightRemark'
"
>
<a-input :value="record[column.dataIndex]" @change="(e) => updateCell(index, column.dataIndex as string, e?.target?.value)" />
</template>
<template v-else-if="column.key === 'action'">
<a-button type="link" size="small" danger @click="removeRow(index)">删行</a-button>
</template>
<template v-else-if="column.key === 'insert'">
<a-button type="text" size="small" class="insert-plus-btn" title="新增" @click="insertBelow(index)">+</a-button>
</template>
</template>
</a-table>
<MesXslEquipmentLedgerSelectModal @register="registerEquipmentModal" @select="onEquipmentSelect" />
<MesXslMixingPlanOrderSelectModal @register="registerOrderModal" @select="onOrderSelect" />
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { useModal } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { buildUUID } from '/@/utils/uuid';
import MesXslEquipmentLedgerSelectModal from '/@/views/xslmes/mesXslEquipInspectConfig/components/MesXslEquipmentLedgerSelectModal.vue';
import MesXslMixingPlanOrderSelectModal from './components/MesXslMixingPlanOrderSelectModal.vue';
import { list, saveAll } from './MesXslMixingProductionPlan.api';
const { createMessage } = useMessage();
const saving = ref(false);
const rows = ref<Recordable[]>([]);
const pickerContext = ref<{ rowIndex: number; shift?: 'morning' | 'noon' | 'night' } | null>(null);
const [registerEquipmentModal, { openModal: openEquipmentModal }] = useModal();
const [registerOrderModal, { openModal: openOrderModal }] = useModal();
const columns = computed(() => [
{ title: '新增', key: 'insert', width: 64, fixed: 'left', align: 'center' },
{ title: '机台', dataIndex: 'machineName', width: 108, fixed: 'left', align: 'center' },
{
title: '早班',
align: 'center',
children: [
{ title: '生产订单', dataIndex: 'morningOrderNo', width: 120, align: 'center' },
{ title: '配方名称', dataIndex: 'morningFormulaName', width: 110, align: 'center' },
{ title: '计划', dataIndex: 'morningPlanCount', width: 72, align: 'center' },
{ title: '备注', dataIndex: 'morningRemark', width: 98, align: 'center' },
],
},
{
title: '中班',
align: 'center',
children: [
{ title: '生产订单', dataIndex: 'noonOrderNo', width: 120, align: 'center' },
{ title: '配方名称', dataIndex: 'noonFormulaName', width: 110, align: 'center' },
{ title: '计划', dataIndex: 'noonPlanCount', width: 72, align: 'center' },
{ title: '备注', dataIndex: 'noonRemark', width: 98, align: 'center' },
],
},
{
title: '晚班',
align: 'center',
children: [
{ title: '生产订单', dataIndex: 'nightOrderNo', width: 120, align: 'center' },
{ title: '配方名称', dataIndex: 'nightFormulaName', width: 110, align: 'center' },
{ title: '计划', dataIndex: 'nightPlanCount', width: 72, align: 'center' },
{ title: '备注', dataIndex: 'nightRemark', width: 98, align: 'center' },
],
},
{ title: '删除', key: 'action', width: 64, fixed: 'right', align: 'center' },
]);
function createEmptyRow(): Recordable {
return {
_rowKey: buildUUID(),
id: '',
machineId: '',
machineName: '',
morningPlanId: '',
morningPlanType: '',
morningOrderNo: '',
morningOrderDate: '',
morningFormulaName: '',
morningPlanWeight: null,
morningPlannedCarCount: null,
morningScheduledCarCount: null,
morningFinishedCarCount: null,
morningPlanCount: null,
morningRemark: '',
noonPlanId: '',
noonPlanType: '',
noonOrderNo: '',
noonOrderDate: '',
noonFormulaName: '',
noonPlanWeight: null,
noonPlannedCarCount: null,
noonScheduledCarCount: null,
noonFinishedCarCount: null,
noonPlanCount: null,
noonRemark: '',
nightPlanId: '',
nightPlanType: '',
nightOrderNo: '',
nightOrderDate: '',
nightFormulaName: '',
nightPlanWeight: null,
nightPlannedCarCount: null,
nightScheduledCarCount: null,
nightFinishedCarCount: null,
nightPlanCount: null,
nightRemark: '',
};
}
function shiftByOrderField(field: string): 'morning' | 'noon' | 'night' {
if (field.startsWith('morning')) return 'morning';
if (field.startsWith('noon')) return 'noon';
return 'night';
}
function updateCell(index: number, field: string, value: any) {
const row = rows.value[index];
if (!row) return;
row[field] = value;
}
function createBlankRows(count = 12) {
return Array.from({ length: count }, () => createEmptyRow());
}
function insertBelow(index: number) {
rows.value.splice(index + 1, 0, createEmptyRow());
}
function removeRow(index: number) {
rows.value.splice(index, 1);
if (!rows.value.length) {
rows.value = createBlankRows();
}
}
function openMachinePicker(rowIndex: number) {
pickerContext.value = { rowIndex };
const row = rows.value[rowIndex];
openEquipmentModal(true, { equipmentLedgerId: row?.machineId || '' });
}
function onEquipmentSelect(payload: Recordable) {
const ctx = pickerContext.value;
if (!ctx) return;
const row = rows.value[ctx.rowIndex];
if (!row) return;
row.machineId = payload?.equipmentLedgerId || '';
row.machineName = payload?.equipmentName || '';
pickerContext.value = null;
}
function openOrderPicker(rowIndex: number, shift: 'morning' | 'noon' | 'night') {
pickerContext.value = { rowIndex, shift };
const row = rows.value[rowIndex];
openOrderModal(true, {
planId: row?.[`${shift}PlanId`] || '',
machineId: row?.machineId || '',
machineName: row?.machineName || '',
});
}
function onOrderSelect(payload: Recordable | null) {
const ctx = pickerContext.value;
if (!ctx || !ctx.shift) return;
const row = rows.value[ctx.rowIndex];
if (!row) return;
const shift = ctx.shift;
row[`${shift}PlanId`] = payload?.planId || '';
row[`${shift}PlanType`] = payload?.planType || '';
row[`${shift}OrderNo`] = payload?.orderNo || '';
row[`${shift}OrderDate`] = payload?.orderDate || '';
row[`${shift}FormulaName`] = payload?.formulaName || '';
row[`${shift}PlanWeight`] = payload?.planWeight ?? null;
row[`${shift}PlannedCarCount`] = payload?.plannedCarCount ?? null;
row[`${shift}ScheduledCarCount`] = payload?.scheduledCarCount ?? null;
row[`${shift}FinishedCarCount`] = payload?.finishedCarCount ?? null;
pickerContext.value = null;
}
async function loadRows() {
const res = await list({ pageNo: 1, pageSize: 500 });
const records = (res?.records || res?.result?.records || []) as Recordable[];
rows.value = (records || []).map((item) => ({ ...createEmptyRow(), ...item, _rowKey: item.id || buildUUID() }));
if (!rows.value.length) {
rows.value = createBlankRows();
}
}
async function handleSaveAll() {
saving.value = true;
try {
const payload = rows.value.map((r) => {
const { _rowKey, ...rest } = r;
return rest;
});
await saveAll({ rows: payload });
createMessage.success('保存成功');
await loadRows();
} finally {
saving.value = false;
}
}
loadRows();
</script>
<style scoped>
.mixing-plan-page {
padding: 8px;
background: #fff;
}
.mixing-plan-toolbar {
margin-bottom: 12px;
display: flex;
gap: 8px;
}
.picker-input {
cursor: pointer;
}
:deep(.ant-table-thead > tr > th) {
text-align: center !important;
padding: 4px 6px !important;
font-size: 12px;
}
:deep(.ant-table-tbody > tr > td) {
padding: 2px 3px !important;
}
:deep(.ant-input),
:deep(.ant-input-number),
:deep(.ant-input-number-input) {
font-size: 12px;
}
:deep(.ant-input),
:deep(.ant-input-number) {
width: 100%;
min-width: 0;
}
:deep(.ant-input-number-input) {
padding: 0 4px;
}
.insert-plus-btn {
color: #52c41a;
font-size: 18px;
font-weight: 700;
line-height: 1;
padding: 0 4px;
}
.insert-plus-btn:hover,
.insert-plus-btn:focus {
color: #73d13d;
}
</style>

View File

@@ -0,0 +1,89 @@
<template>
<BasicModal v-bind="$attrs" title="选择生产订单计划" :width="1100" @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 { orderOptionPage } from '../MesXslMixingProductionPlan.api';
const emit = defineEmits(['register', 'select']);
const selectedRow = ref<Recordable | null>(null);
const machineId = ref('');
const machineName = ref('');
const [registerTable, { reload, getSelectRowKeys, getSelectRows, setSelectedRowKeys, clearSelectedRowKeys }] = useTable({
api: orderOptionPage,
columns: [
{ title: '类型', dataIndex: 'planType', width: 70, customRender: ({ text }) => (text === 'M' ? '母胶' : '终胶') },
{ title: '订单编号', dataIndex: 'orderNo', width: 150 },
{ title: '订单日期', dataIndex: 'orderDate', width: 120 },
{ title: '胶料名称', dataIndex: 'formulaName', width: 170 },
{ title: '计划重量', dataIndex: 'planWeight', width: 120 },
{ title: '计划车数', dataIndex: 'plannedCarCount', width: 100 },
{ title: '已排产车数', dataIndex: 'scheduledCarCount', width: 110 },
{ title: '完成车数', dataIndex: 'finishedCarCount', width: 100 },
],
rowKey: 'planId',
useSearchForm: true,
formConfig: {
labelWidth: 80,
schemas: [{ label: '关键字', field: 'keyword', component: 'Input', colProps: { span: 10 } }],
},
pagination: { pageSize: 10 },
canResize: false,
showIndexColumn: false,
immediate: true,
beforeFetch: (params) => {
return Object.assign(params, {
machineId: machineId.value || undefined,
machineName: machineName.value || undefined,
});
},
rowSelection: {
type: 'radio',
columnWidth: 48,
onChange: (_keys, rows) => {
selectedRow.value = rows?.[0] ?? null;
},
},
clickToRowSelect: true,
});
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
selectedRow.value = null;
clearSelectedRowKeys?.();
setModalProps({ confirmLoading: false });
machineId.value = data?.machineId || '';
machineName.value = data?.machineName || '';
if (data?.planId) {
setSelectedRowKeys?.([data.planId]);
}
reload();
});
function handleOk() {
const row = selectedRow.value || ((getSelectRows?.() || []) as Recordable[])[0];
if (!row?.planId) {
emit('select', null);
closeModal();
return;
}
emit('select', {
planId: row.planId,
planType: row.planType,
orderNo: row.orderNo || '',
orderDate: row.orderDate || '',
formulaName: row.formulaName || '',
planWeight: row.planWeight ?? null,
plannedCarCount: row.plannedCarCount ?? null,
scheduledCarCount: row.scheduledCarCount ?? null,
finishedCarCount: row.finishedCarCount ?? null,
});
closeModal();
}
</script>

View File

@@ -8,6 +8,7 @@ enum Api {
deleteBatch = '/xslmes/mesXslProductionOrder/deleteBatch',
queryById = '/xslmes/mesXslProductionOrder/queryById',
split = '/xslmes/mesXslProductionOrder/split',
splitBatch = '/xslmes/mesXslProductionOrder/splitBatch',
exportXls = '/xslmes/mesXslProductionOrder/exportXls',
}
@@ -26,4 +27,7 @@ export const queryById = (params) => defHttp.get({ url: Api.queryById, params })
export const splitToMasterBatchPlan = (params, handleSuccess) =>
defHttp.post({ url: Api.split, params }, { joinParamsToUrl: true }).then(() => handleSuccess());
export const splitToMasterBatchPlanBatch = (params, handleSuccess) =>
defHttp.post({ url: Api.splitBatch, params }, { joinParamsToUrl: true }).then(() => handleSuccess());
export const getExportUrl = Api.exportXls;

View File

@@ -11,6 +11,15 @@
>
导出
</a-button>
<a-button
type="primary"
v-auth="'xslmes:mes_xsl_production_order:split'"
preIcon="ant-design:split-cells-outlined"
:disabled="selectedRowKeys.length === 0"
@click="handleBatchSplit"
>
拆分
</a-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
@@ -32,11 +41,14 @@
import { BasicTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage';
import { useMessage } from '/@/hooks/web/useMessage';
import { Modal } from 'ant-design-vue';
import MesXslProductionOrderModal from './modules/MesXslProductionOrderModal.vue';
import { columns, searchFormSchema } from './MesXslProductionOrder.data';
import { batchDelete, deleteOne, getExportUrl, list, splitToMasterBatchPlan } from './MesXslProductionOrder.api';
import { batchDelete, deleteOne, getExportUrl, list, splitToMasterBatchPlanBatch } from './MesXslProductionOrder.api';
const [registerModal, { openModal }] = useModal();
const { createMessage } = useMessage();
const { tableContext, onExportXls } = useListPage({
tableProps: {
title: '生产订单',
@@ -65,8 +77,25 @@
async function batchHandleDelete() {
await batchDelete({ ids: selectedRowKeys.value.join(',') }, reload);
}
async function handleSplit(record) {
await splitToMasterBatchPlan({ id: record.id }, reload);
async function handleBatchSplit() {
if (!selectedRowKeys.value.length) {
createMessage.warning('请先勾选要拆分的生产订单');
return;
}
Modal.confirm({
title: '确认批量拆分',
content: `将拆分 ${selectedRowKeys.value.length} 条生产订单,是否继续?`,
okText: '确认',
cancelText: '取消',
onOk: async () => {
try {
await splitToMasterBatchPlanBatch({ ids: selectedRowKeys.value.join(',') }, reload);
createMessage.success('批量拆分成功');
} catch (e: any) {
createMessage.error(e?.message || '批量拆分失败');
}
},
});
}
function handleSuccess() {
reload();
@@ -74,12 +103,6 @@
function getTableAction(record) {
return [
{ label: '编辑', onClick: handleEdit.bind(null, record), auth: 'xslmes:mes_xsl_production_order:edit' },
{
label: '拆分',
onClick: handleSplit.bind(null, record),
auth: 'xslmes:mes_xsl_production_order:split',
ifShow: () => record.splitStatus !== 1,
},
];
}
function getDropDownAction(record) {

View File

@@ -0,0 +1,10 @@
import { defHttp } from '/@/utils/http/axios';
enum Api {
list = '/xslmes/mesXslRawMaterialDemandPlan/list',
exportXls = '/xslmes/mesXslRawMaterialDemandPlan/exportXls',
}
export const getExportUrl = Api.exportXls;
export const list = (params) => defHttp.get({ url: Api.list, params });

View File

@@ -0,0 +1,28 @@
import { BasicColumn, FormSchema } from '/@/components/Table';
export const baseColumns: BasicColumn[] = [
{ title: 'ERP编号', align: 'center', dataIndex: 'erpCode', width: 180 },
{ title: '原材料名称', align: 'center', dataIndex: 'rawMaterialName', width: 220, ellipsis: true },
{ title: '需求重量', align: 'center', dataIndex: 'demandWeight', width: 140 },
{ title: '标准重量', align: 'center', dataIndex: 'standardWeight', width: 140 },
{ title: '实际重量', align: 'center', dataIndex: 'actualWeight', width: 140 },
];
export const machineColumns: BasicColumn[] = [
{ title: '机台', align: 'center', dataIndex: 'machineName', width: 160 },
...baseColumns,
];
export const searchFormSchema: FormSchema[] = [
{ label: 'ERP编号', field: 'erpCode', component: 'JInput', colProps: { span: 6 } },
{ label: '原材料名称', field: 'rawMaterialName', component: 'JInput', colProps: { span: 6 } },
];
export const superQuerySchema = {
machineName: { title: '机台', order: 0, view: 'text' },
erpCode: { title: 'ERP编号', order: 1, view: 'text' },
rawMaterialName: { title: '原材料名称', order: 2, view: 'text' },
demandWeight: { title: '需求重量', order: 3, view: 'number' },
standardWeight: { title: '标准重量', order: 4, view: 'number' },
actualWeight: { title: '实际重量', order: 5, view: 'number' },
};

View File

@@ -0,0 +1,83 @@
<template>
<div>
<BasicTable @register="registerTable">
<template #tableTitle>
<a-checkbox v-model:checked="groupByMachine" @change="onGroupByMachineChange">按机台统计</a-checkbox>
<a-button
style="margin-left: 8px"
type="primary"
v-auth="'xslmes:mes_xsl_raw_material_demand_plan:exportXls'"
preIcon="ant-design:export-outlined"
@click="onExportXls"
>
导出
</a-button>
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
</template>
</BasicTable>
</div>
</template>
<script lang="ts" name="xslmes-mesXslRawMaterialDemandPlan" setup>
import { onMounted, reactive, ref } from 'vue';
import { BasicTable } from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage';
import {
baseColumns,
machineColumns,
searchFormSchema,
superQuerySchema,
} from './MesXslRawMaterialDemandPlan.data';
import { list, getExportUrl } from './MesXslRawMaterialDemandPlan.api';
const queryParam = reactive<any>({ groupByMachine: 0 });
const groupByMachine = ref(false);
const { tableContext, onExportXls } = useListPage({
tableProps: {
title: '原材料需求计划',
api: list,
columns: baseColumns,
canResize: true,
formConfig: {
schemas: searchFormSchema,
autoSubmitOnEnter: true,
showAdvancedButton: true,
},
beforeFetch: (params) => {
return Object.assign(params, queryParam, {
groupByMachine: groupByMachine.value ? 1 : 0,
});
},
},
exportConfig: {
name: '原材料需求计划',
url: getExportUrl,
params: queryParam,
},
});
const [registerTable, { reload, setColumns }] = tableContext;
const superQueryConfig = reactive(superQuerySchema);
onMounted(() => {
applyColumns();
});
function applyColumns() {
setColumns(groupByMachine.value ? machineColumns : baseColumns);
}
function onGroupByMachineChange() {
queryParam.groupByMachine = groupByMachine.value ? 1 : 0;
applyColumns();
reload();
}
function handleSuperQuery(params) {
Object.keys(params).forEach((k) => {
queryParam[k] = params[k];
});
reload();
}
</script>