feat: 增加LMS日计划作业页面

This commit is contained in:
zhouz
2026-08-14 15:42:45 +08:00
parent d8a0f63500
commit db273e3e1d
5 changed files with 486 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { LmsDailyPlanApi } from '#/api/lms/dailyplan';
import { DICT_TYPE } from '@vben/constants';
const DAILY_PLAN_DICT_TYPE = DICT_TYPE as typeof DICT_TYPE & {
LMS_DAILY_PLAN_ORDER_STATUS: string;
LMS_DAILY_PLAN_WORK_STATUS: string;
};
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'planOrOrderCode',
label: '计划/订单号',
component: 'Input',
componentProps: { allowClear: true, placeholder: '请输入计划或订单号' },
},
{
fieldName: 'materialKeyword',
label: '管芯',
component: 'Input',
componentProps: { allowClear: true, placeholder: '请输入管芯编码或名称' },
},
];
}
export function useGridColumns(showWeight: boolean): VxeTableGridOptions<LmsDailyPlanApi.DailyPlan>['columns'] {
return [
{ field: 'sortSeq', title: '顺序', width: 80, fixed: 'left' },
...(showWeight ? [{ field: 'weight', title: '权重', width: 80 }] : []),
{ field: 'planCode', title: '日计划编号', minWidth: 150 },
{ field: 'orderCode', title: '订单号', minWidth: 150, slots: { default: 'orderCode' } },
{ field: 'planDate', title: '计划日期', minWidth: 115 },
{ field: 'materialCode', title: '管芯编码', minWidth: 130 },
{ field: 'materialName', title: '管芯名称', minWidth: 140 },
{ field: 'totalQty', title: '需求总量', minWidth: 95 },
{ field: 'stockingProgress', title: '备货进度', minWidth: 115, slots: { default: 'stockingProgress' } },
{
field: 'stockingStatus', title: '备货状态', minWidth: 105,
cellRender: { name: 'CellDict', props: { type: DAILY_PLAN_DICT_TYPE.LMS_DAILY_PLAN_WORK_STATUS } },
},
{ field: 'sleevingProgress', title: '套管进度', minWidth: 115, slots: { default: 'sleevingProgress' } },
{
field: 'sleevingStatus', title: '套管状态', minWidth: 105,
cellRender: { name: 'CellDict', props: { type: DAILY_PLAN_DICT_TYPE.LMS_DAILY_PLAN_WORK_STATUS } },
},
{
field: 'orderStatus', title: '订单状态', minWidth: 105,
cellRender: { name: 'CellDict', props: { type: DAILY_PLAN_DICT_TYPE.LMS_DAILY_PLAN_ORDER_STATUS } },
},
{ field: 'updateTime', title: '修改时间', minWidth: 170, formatter: 'formatDateTime' },
{ title: '操作', width: 310, fixed: 'right', slots: { default: 'actions' } },
];
}

View File

@@ -0,0 +1,203 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { LmsDailyPlanApi } from '#/api/lms/dailyplan';
import { computed, onMounted, ref } from 'vue';
import { confirm, Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
import { formatDateTime } from '@vben/utils';
import { Card, message, TabPane, Tabs, Tag } from 'antdv-next';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
finishDailyPlanOrder,
getDailyPlanStrategies,
getDailyPlanWorkPage,
startDailyPlanSleeving,
startDailyPlanStocking,
} from '#/api/lms/dailyplan';
import Detail from '../management/modules/detail.vue';
import { useGridColumns, useGridFormSchema } from './data';
import Operation from './modules/operation.vue';
import Sort from './modules/sort.vue';
import Strategy from './modules/strategy.vue';
const ORDER_IN_PROGRESS = 1;
const ORDER_CANCELED = 3;
const WORK_NOT_STARTED = 0;
const WORK_IN_PROGRESS = 1;
const WORK_CANCELED = 3;
const STRATEGY_WEIGHT = 2;
const activeOrderStatus = ref(String(ORDER_IN_PROGRESS));
const strategies = ref<LmsDailyPlanApi.Strategy[]>([]);
const showWeight = computed(() => strategies.value.some((item) => item.strategyMode === STRATEGY_WEIGHT));
const [DetailDrawer, detailDrawerApi] = useVbenDrawer({ connectedComponent: Detail, destroyOnClose: true });
const [OperationModal, operationModalApi] = useVbenModal({ connectedComponent: Operation, destroyOnClose: true });
const [StrategyModal, strategyModalApi] = useVbenModal({ connectedComponent: Strategy, destroyOnClose: true });
const [SortModal, sortModalApi] = useVbenModal({ connectedComponent: Sort, destroyOnClose: true });
function strategyOf(type: number) {
return strategies.value.find((item) => item.strategyType === type);
}
function strategyName(mode?: number) {
return mode === STRATEGY_WEIGHT ? '权重轮转' : '顺序模式';
}
function updateColumns() {
gridApi.setGridOptions({ columns: useGridColumns(showWeight.value) });
}
async function loadStrategies() {
strategies.value = await getDailyPlanStrategies();
updateColumns();
}
function handleRefresh() {
gridApi.query();
}
function handleSuccess() {
handleRefresh();
}
function handleStrategySuccess() {
loadStrategies();
handleRefresh();
}
function handleTabChange() {
handleRefresh();
}
function handleDetail(row: LmsDailyPlanApi.DailyPlan) {
detailDrawerApi.setData(row).open();
}
function handleOperation(row: LmsDailyPlanApi.DailyPlan, type: string) {
operationModalApi.setData({ row, type }).open();
}
function handleStrategy(type: number) {
const strategy = strategyOf(type);
if (strategy) strategyModalApi.setData(strategy).open();
}
function handleSort() {
const rows = gridApi.grid?.getData() as LmsDailyPlanApi.DailyPlan[];
if (!rows?.length) {
message.warning('当前页没有可调整的日计划');
return;
}
sortModalApi.setData(rows).open();
}
async function handleStartWork(row: LmsDailyPlanApi.DailyPlan, target: 'sleeving' | 'stocking') {
const label = target === 'stocking' ? '备货' : '套管';
await confirm(`确认开始${label}吗?本操作只改变日计划状态,实际任务由定时器后续生成。`);
const api = target === 'stocking' ? startDailyPlanStocking : startDailyPlanSleeving;
await api({ id: row.dailyPlanId, version: row.version });
message.success(`${label}已开始`);
handleRefresh();
}
async function handleFinish(row: LmsDailyPlanApi.DailyPlan) {
await confirm('确认结束该订单吗?未完成的备货和套管将停止并标记为已取消,结束后不可恢复。');
await finishDailyPlanOrder({ id: row.dailyPlanId, version: row.version });
message.success('订单已结束');
handleRefresh();
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
wrapperClass: 'grid grid-cols-1 gap-x-3 md:grid-cols-2 xl:grid-cols-4',
},
gridOptions: {
columns: useGridColumns(false),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => getDailyPlanWorkPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
orderStatus: Number(activeOrderStatus.value),
}),
},
},
rowConfig: { keyField: 'dailyPlanId', isHover: true },
sortConfig: { defaultSort: { field: 'sortSeq', order: 'asc' }, remote: false },
toolbarConfig: { refresh: true, search: true },
} as VxeTableGridOptions<LmsDailyPlanApi.DailyPlan>,
});
onMounted(loadStrategies);
</script>
<template>
<Page auto-content-height>
<DetailDrawer />
<OperationModal @success="handleSuccess" />
<StrategyModal @success="handleStrategySuccess" />
<SortModal @success="handleSuccess" />
<div class="mb-4 grid grid-cols-1 gap-4 lg:grid-cols-2">
<Card v-for="type in [1, 2]" :key="type" size="small">
<div class="flex items-center justify-between gap-4">
<div>
<div class="mb-2 font-medium">{{ type === 1 ? '备货策略' : '套管策略' }}</div>
<div class="flex items-center gap-3 text-sm text-gray-500">
<Tag :color="strategyOf(type)?.strategyMode === STRATEGY_WEIGHT ? 'blue' : 'green'">
{{ strategyName(strategyOf(type)?.strategyMode) }}
</Tag>
<span>修改时间{{ strategyOf(type)?.updateTime ? formatDateTime(strategyOf(type)?.updateTime) : '-' }}</span>
</div>
</div>
<TableAction
:actions="[{ label: '切换策略', type: 'link', auth: ['lms:daily-plan:strategy'], onClick: () => handleStrategy(type) }]"
/>
</div>
</Card>
</div>
<Tabs v-model:active-key="activeOrderStatus" class="mb-2" @change="handleTabChange">
<TabPane :key="String(ORDER_IN_PROGRESS)" tab="进行中" />
<TabPane :key="String(ORDER_CANCELED)" tab="已取消" />
</Tabs>
<Grid table-title="日计划作业">
<template #toolbar-tools>
<TableAction
:actions="[{ label: '调整顺序', type: 'primary', auth: ['lms:daily-plan:schedule'], ifShow: Number(activeOrderStatus) === ORDER_IN_PROGRESS, onClick: handleSort }]"
/>
</template>
<template #orderCode="{ row }">{{ row.erpOrderCode || row.manualOrderCode || '-' }}</template>
<template #stockingProgress="{ row }">{{ row.stockedQty }} / {{ row.totalQty }}</template>
<template #sleevingProgress="{ row }">{{ row.sleevedQty }} / {{ row.totalQty }}</template>
<template #actions="{ row }">
<TableAction
:actions="[
{ label: '详情', type: 'link', auth: ['lms:daily-plan:query'], onClick: handleDetail.bind(null, row) },
{ label: '追加', type: 'link', auth: ['lms:daily-plan:operate'], ifShow: row.orderStatus === ORDER_IN_PROGRESS, onClick: handleOperation.bind(null, row, 'append') },
{ label: '调权重', type: 'link', auth: ['lms:daily-plan:schedule'], ifShow: row.orderStatus === ORDER_IN_PROGRESS && showWeight, onClick: handleOperation.bind(null, row, 'weight') },
{ label: '开始备货', type: 'link', auth: ['lms:daily-plan:operate'], ifShow: row.orderStatus === ORDER_IN_PROGRESS && row.stockingStatus === WORK_NOT_STARTED, onClick: handleStartWork.bind(null, row, 'stocking') },
{ label: '取消备货', type: 'link', danger: true, auth: ['lms:daily-plan:operate'], ifShow: row.orderStatus === ORDER_IN_PROGRESS && [WORK_NOT_STARTED, WORK_IN_PROGRESS].includes(row.stockingStatus), onClick: handleOperation.bind(null, row, 'cancelStocking') },
{ label: '恢复备货', type: 'link', auth: ['lms:daily-plan:operate'], ifShow: row.orderStatus === ORDER_IN_PROGRESS && row.stockingStatus === WORK_CANCELED, onClick: handleOperation.bind(null, row, 'restoreStocking') },
{ label: '开始套管', type: 'link', auth: ['lms:daily-plan:operate'], ifShow: row.orderStatus === ORDER_IN_PROGRESS && row.sleevingStatus === WORK_NOT_STARTED, onClick: handleStartWork.bind(null, row, 'sleeving') },
{ label: '取消套管', type: 'link', danger: true, auth: ['lms:daily-plan:operate'], ifShow: row.orderStatus === ORDER_IN_PROGRESS && [WORK_NOT_STARTED, WORK_IN_PROGRESS].includes(row.sleevingStatus), onClick: handleOperation.bind(null, row, 'cancelSleeving') },
{ label: '恢复套管', type: 'link', auth: ['lms:daily-plan:operate'], ifShow: row.orderStatus === ORDER_IN_PROGRESS && row.sleevingStatus === WORK_CANCELED, onClick: handleOperation.bind(null, row, 'restoreSleeving') },
{ label: '结束订单', type: 'link', auth: ['lms:daily-plan:operate'], ifShow: row.orderStatus === ORDER_IN_PROGRESS, onClick: handleFinish.bind(null, row) },
{ label: '取消订单', type: 'link', danger: true, auth: ['lms:daily-plan:operate'], ifShow: row.orderStatus === ORDER_IN_PROGRESS, onClick: handleOperation.bind(null, row, 'cancelOrder') },
{ label: '恢复订单', type: 'link', auth: ['lms:daily-plan:operate'], ifShow: row.orderStatus === ORDER_CANCELED, onClick: handleOperation.bind(null, row, 'restoreOrder') },
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,108 @@
<script lang="ts" setup>
import type { LmsDailyPlanApi } from '#/api/lms/dailyplan';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { InputNumber, message, TextArea } from 'antdv-next';
import {
appendDailyPlanQuantity,
cancelDailyPlanOrder,
cancelDailyPlanSleeving,
cancelDailyPlanStocking,
restoreDailyPlanOrder,
restoreDailyPlanSleeving,
restoreDailyPlanStocking,
updateDailyPlanWeight,
} from '#/api/lms/dailyplan';
type OperationType = 'append' | 'cancelOrder' | 'cancelSleeving' | 'cancelStocking'
| 'restoreOrder' | 'restoreSleeving' | 'restoreStocking' | 'weight';
interface ModalData {
row: LmsDailyPlanApi.DailyPlan;
type: OperationType;
}
const emit = defineEmits(['success']);
const data = ref<ModalData>();
const reason = ref('');
const appendQty = ref(1);
const isAppend = computed(() => data.value?.type === 'append');
const isNumberOperation = computed(() => ['append', 'weight'].includes(data.value?.type ?? ''));
const titleMap: Record<OperationType, string> = {
append: '追加数量', cancelOrder: '取消订单', cancelSleeving: '取消套管', cancelStocking: '取消备货',
restoreOrder: '恢复订单', restoreSleeving: '恢复套管', restoreStocking: '恢复备货',
weight: '修改权重',
};
const apiMap = {
cancelOrder: cancelDailyPlanOrder,
cancelSleeving: cancelDailyPlanSleeving,
cancelStocking: cancelDailyPlanStocking,
restoreOrder: restoreDailyPlanOrder,
restoreSleeving: restoreDailyPlanSleeving,
restoreStocking: restoreDailyPlanStocking,
};
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!data.value) return;
const { row, type } = data.value;
if (isNumberOperation.value) {
if (!Number.isInteger(appendQty.value) || appendQty.value <= 0) {
message.warning(`${type === 'append' ? '追加数量' : '权重'}必须为正整数`);
return;
}
} else if (!reason.value.trim()) {
message.warning('请填写操作原因');
return;
}
modalApi.lock();
try {
if (type === 'append') {
await appendDailyPlanQuantity({ id: row.dailyPlanId, version: row.version, appendQty: appendQty.value });
} else if (type === 'weight') {
await updateDailyPlanWeight({ id: row.dailyPlanId, version: row.version, weight: appendQty.value });
} else {
await apiMap[type]({ id: row.dailyPlanId, version: row.version, reason: reason.value.trim() });
}
await modalApi.close();
message.success(`${titleMap[type]}成功`);
emit('success');
} finally {
modalApi.unlock();
}
},
onOpenChange(isOpen: boolean) {
if (!isOpen) {
data.value = undefined;
reason.value = '';
appendQty.value = 1;
return;
}
data.value = modalApi.getData<ModalData>();
appendQty.value = data.value.type === 'weight' ? data.value.row.weight : 1;
},
});
</script>
<template>
<Modal class="w-[520px]" :title="data ? titleMap[data.type] : '日计划操作'">
<div v-if="data" class="space-y-4">
<div class="rounded bg-gray-50 p-3 dark:bg-gray-800">
{{ data.row.planCode }} · {{ data.row.materialCode }} / {{ data.row.materialName }}
</div>
<div v-if="isNumberOperation">
<div class="mb-2">{{ isAppend ? '本次追加数量' : '权重' }}</div>
<InputNumber v-model:value="appendQty" :min="1" :precision="0" class="w-full" />
<div v-if="isAppend" class="mt-2 text-sm text-gray-500">当前需求总量 {{ data.row.totalQty }} </div>
</div>
<div v-else>
<div class="mb-2">操作原因</div>
<TextArea v-model:value="reason" :maxlength="500" :rows="4" placeholder="请输入操作原因" show-count />
</div>
</div>
</Modal>
</template>

View File

@@ -0,0 +1,61 @@
<script lang="ts" setup>
import type { LmsDailyPlanApi } from '#/api/lms/dailyplan';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { InputNumber, message, Table } from 'antdv-next';
import { updateDailyPlanSort } from '#/api/lms/dailyplan';
const emit = defineEmits(['success']);
const rows = ref<LmsDailyPlanApi.DailyPlan[]>([]);
const columns = [
{ title: '日计划编号', dataIndex: 'planCode', width: 150 },
{ title: '管芯', dataIndex: 'material', ellipsis: true },
{ title: '顺序', dataIndex: 'sortSeq', width: 140 },
];
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (rows.value.some((row) => !Number.isInteger(row.sortSeq) || row.sortSeq <= 0)) {
message.warning('顺序必须为正整数');
return;
}
modalApi.lock();
try {
await updateDailyPlanSort({
items: rows.value.map((row) => ({ id: row.dailyPlanId, sortSeq: row.sortSeq, version: row.version })),
});
await modalApi.close();
message.success('顺序调整成功');
emit('success');
} finally {
modalApi.unlock();
}
},
onOpenChange(isOpen: boolean) {
rows.value = isOpen
? modalApi.getData<LmsDailyPlanApi.DailyPlan[]>().map((row) => ({ ...row }))
: [];
},
});
</script>
<template>
<Modal class="w-[720px]" title="调整当前页顺序">
<div class="mb-3 text-sm text-gray-500">列表始终按顺序号从小到大处理权重模式下也由顺序号决定订单轮转先后</div>
<Table :columns="columns" :data-source="rows" :pagination="false" row-key="dailyPlanId" size="small">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'material'">{{ record.materialCode }} / {{ record.materialName }}</template>
<InputNumber
v-else-if="column.dataIndex === 'sortSeq'"
v-model:value="record.sortSeq"
:min="1"
:precision="0"
/>
</template>
</Table>
</Modal>
</template>

View File

@@ -0,0 +1,58 @@
<script lang="ts" setup>
import type { LmsDailyPlanApi } from '#/api/lms/dailyplan';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Alert, message, RadioGroup } from 'antdv-next';
import { updateDailyPlanStrategy } from '#/api/lms/dailyplan';
const emit = defineEmits(['success']);
const strategy = ref<LmsDailyPlanApi.Strategy>();
const strategyMode = ref(1);
const options = [
{ label: '顺序模式', value: 1 },
{ label: '权重轮转', value: 2 },
];
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!strategy.value) return;
modalApi.lock();
try {
await updateDailyPlanStrategy({
strategyMode: strategyMode.value,
strategyType: strategy.value.strategyType,
version: strategy.value.version,
});
await modalApi.close();
message.success('策略切换成功');
emit('success');
} finally {
modalApi.unlock();
}
},
onOpenChange(isOpen: boolean) {
if (!isOpen) {
strategy.value = undefined;
return;
}
strategy.value = modalApi.getData<LmsDailyPlanApi.Strategy>();
strategyMode.value = strategy.value.strategyMode;
},
});
</script>
<template>
<Modal class="w-[520px]" :title="`切换${strategy?.strategyType === 1 ? '备货' : '套管'}策略`">
<RadioGroup v-model:value="strategyMode" :options="options" option-type="button" />
<Alert
class="mt-4"
message="策略切换后从下一轮调度立即生效;已经生成或下发的任务不会撤销。"
show-icon
type="warning"
/>
</Modal>
</template>