fix: 完善日计划作业视图与权限

This commit is contained in:
zhouz
2026-08-14 15:49:54 +08:00
parent db273e3e1d
commit 54d6faffbc
5 changed files with 141 additions and 41 deletions

View File

@@ -26,24 +26,30 @@ export function useGridFormSchema(): VbenFormSchema[] {
];
}
export function useGridColumns(showWeight: boolean): VxeTableGridOptions<LmsDailyPlanApi.DailyPlan>['columns'] {
export function useGridColumns(
workType: number,
showWeight: boolean,
): VxeTableGridOptions<LmsDailyPlanApi.DailyPlan>['columns'] {
const isStocking = workType === 1;
return [
{ field: 'sortSeq', title: '顺序', width: 80, fixed: 'left' },
...(showWeight ? [{ field: 'weight', title: '权重', width: 80 }] : []),
...(showWeight ? [{ field: 'weight', title: '权重', width: 150, slots: { default: 'weight' } }] : []),
{ 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: isStocking ? 'stockingProgress' : 'sleevingProgress',
title: isStocking ? '备货进度' : '套管进度',
minWidth: 115,
slots: { default: isStocking ? 'stockingProgress' : 'sleevingProgress' },
},
{ field: 'sleevingProgress', title: '套管进度', minWidth: 115, slots: { default: 'sleevingProgress' } },
{
field: 'sleevingStatus', title: '套管状态', minWidth: 105,
field: isStocking ? 'stockingStatus' : 'sleevingStatus',
title: isStocking ? '备货状态' : '套管状态',
minWidth: 105,
cellRender: { name: 'CellDict', props: { type: DAILY_PLAN_DICT_TYPE.LMS_DAILY_PLAN_WORK_STATUS } },
},
{

View File

@@ -7,7 +7,7 @@ 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 { Card, InputNumber, message, TabPane, Tabs, Tag } from 'antdv-next';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
@@ -16,6 +16,7 @@ import {
getDailyPlanWorkPage,
startDailyPlanSleeving,
startDailyPlanStocking,
updateDailyPlanWeight,
} from '#/api/lms/dailyplan';
import Detail from '../management/modules/detail.vue';
@@ -32,8 +33,10 @@ const WORK_CANCELED = 3;
const STRATEGY_WEIGHT = 2;
const activeOrderStatus = ref(String(ORDER_IN_PROGRESS));
const activeWorkType = ref('1');
const strategies = ref<LmsDailyPlanApi.Strategy[]>([]);
const showWeight = computed(() => strategies.value.some((item) => item.strategyMode === STRATEGY_WEIGHT));
const showWeight = computed(() => strategyOf(Number(activeWorkType.value))?.strategyMode === STRATEGY_WEIGHT);
const weightDrafts = ref<Record<string, number>>({});
const [DetailDrawer, detailDrawerApi] = useVbenDrawer({ connectedComponent: Detail, destroyOnClose: true });
const [OperationModal, operationModalApi] = useVbenModal({ connectedComponent: Operation, destroyOnClose: true });
@@ -49,7 +52,7 @@ function strategyName(mode?: number) {
}
function updateColumns() {
gridApi.setGridOptions({ columns: useGridColumns(showWeight.value) });
gridApi.setGridOptions({ columns: useGridColumns(Number(activeWorkType.value), showWeight.value) });
}
async function loadStrategies() {
@@ -74,6 +77,11 @@ function handleTabChange() {
handleRefresh();
}
function handleWorkTypeChange() {
weightDrafts.value = {};
updateColumns();
}
function handleDetail(row: LmsDailyPlanApi.DailyPlan) {
detailDrawerApi.setData(row).open();
}
@@ -82,20 +90,59 @@ function handleOperation(row: LmsDailyPlanApi.DailyPlan, type: string) {
operationModalApi.setData({ row, type }).open();
}
function isCancelableWorkStatus(status: number) {
return status === WORK_NOT_STARTED || status === WORK_IN_PROGRESS;
}
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('当前页没有可调整的日计划');
async function handleSort() {
const filters = await gridApi.formApi.getValues();
const pageSize = 100;
let pageNo = 1;
const rows: LmsDailyPlanApi.DailyPlan[] = [];
while (true) {
const page = await getDailyPlanWorkPage({
...filters,
orderStatus: Number(activeOrderStatus.value),
pageNo,
pageSize,
});
rows.push(...page.list);
if (page.list.length === 0 || rows.length >= page.total) break;
pageNo += 1;
}
if (rows.length === 0) {
message.warning('当前状态没有可调整的日计划');
return;
}
sortModalApi.setData(rows).open();
}
function getWeightDraft(row: LmsDailyPlanApi.DailyPlan) {
return weightDrafts.value[String(row.dailyPlanId)] ?? row.weight;
}
function setWeightDraft(row: LmsDailyPlanApi.DailyPlan, value: null | number) {
weightDrafts.value[String(row.dailyPlanId)] = value ?? row.weight;
}
async function handleWeightSave(row: LmsDailyPlanApi.DailyPlan) {
const weight = getWeightDraft(row);
if (!Number.isInteger(weight) || weight <= 0) {
message.warning('权重必须为正整数');
return;
}
await confirm(`确认将 ${row.planCode} 的权重调整为 ${weight} 吗?`);
await updateDailyPlanWeight({ id: row.dailyPlanId, version: row.version, weight });
message.success('权重修改成功');
delete weightDrafts.value[String(row.dailyPlanId)];
handleRefresh();
}
async function handleStartWork(row: LmsDailyPlanApi.DailyPlan, target: 'sleeving' | 'stocking') {
const label = target === 'stocking' ? '备货' : '套管';
await confirm(`确认开始${label}吗?本操作只改变日计划状态,实际任务由定时器后续生成。`);
@@ -118,7 +165,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
wrapperClass: 'grid grid-cols-1 gap-x-3 md:grid-cols-2 xl:grid-cols-4',
},
gridOptions: {
columns: useGridColumns(false),
columns: useGridColumns(1, false),
height: 'auto',
keepSource: true,
proxyConfig: {
@@ -148,7 +195,12 @@ onMounted(loadStrategies);
<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">
<Card
v-for="type in [1, 2]"
:key="type"
:class="{ 'ring-2 ring-primary': Number(activeWorkType) === type }"
size="small"
>
<div class="flex items-center justify-between gap-4">
<div>
<div class="mb-2 font-medium">{{ type === 1 ? '备货策略' : '套管策略' }}</div>
@@ -166,6 +218,11 @@ onMounted(loadStrategies);
</Card>
</div>
<Tabs v-model:active-key="activeWorkType" class="mb-2" @change="handleWorkTypeChange">
<TabPane key="1" tab="备货作业" />
<TabPane key="2" tab="套管作业" />
</Tabs>
<Tabs v-model:active-key="activeOrderStatus" class="mb-2" @change="handleTabChange">
<TabPane :key="String(ORDER_IN_PROGRESS)" tab="进行中" />
<TabPane :key="String(ORDER_CANCELED)" tab="已取消" />
@@ -180,18 +237,31 @@ onMounted(loadStrategies);
<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 #weight="{ row }">
<div class="flex items-center gap-1">
<InputNumber
:min="1"
:precision="0"
:value="getWeightDraft(row)"
class="w-20"
@update:value="setWeightDraft(row, $event)"
/>
<TableAction
:actions="[{ label: '保存', type: 'link', auth: ['lms:daily-plan:schedule'], onClick: handleWeightSave.bind(null, row) }]"
/>
</div>
</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:start'], ifShow: row.orderStatus === ORDER_IN_PROGRESS, onClick: handleOperation.bind(null, row, 'append') },
{ label: '开始备货', type: 'link', auth: ['lms:daily-plan:operate'], ifShow: activeWorkType === '1' && 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: activeWorkType === '1' && row.orderStatus === ORDER_IN_PROGRESS && isCancelableWorkStatus(row.stockingStatus), onClick: handleOperation.bind(null, row, 'cancelStocking') },
{ label: '恢复备货', type: 'link', auth: ['lms:daily-plan:operate'], ifShow: activeWorkType === '1' && row.orderStatus === ORDER_IN_PROGRESS && row.stockingStatus === WORK_CANCELED, onClick: handleOperation.bind(null, row, 'restoreStocking') },
{ label: '开始套管', type: 'link', auth: ['lms:daily-plan:operate'], ifShow: activeWorkType === '2' && 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: activeWorkType === '2' && row.orderStatus === ORDER_IN_PROGRESS && isCancelableWorkStatus(row.sleevingStatus), onClick: handleOperation.bind(null, row, 'cancelSleeving') },
{ label: '恢复套管', type: 'link', auth: ['lms:daily-plan:operate'], ifShow: activeWorkType === '2' && 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') },

View File

@@ -15,11 +15,10 @@ import {
restoreDailyPlanOrder,
restoreDailyPlanSleeving,
restoreDailyPlanStocking,
updateDailyPlanWeight,
} from '#/api/lms/dailyplan';
type OperationType = 'append' | 'cancelOrder' | 'cancelSleeving' | 'cancelStocking'
| 'restoreOrder' | 'restoreSleeving' | 'restoreStocking' | 'weight';
| 'restoreOrder' | 'restoreSleeving' | 'restoreStocking';
interface ModalData {
row: LmsDailyPlanApi.DailyPlan;
@@ -31,11 +30,9 @@ 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,
@@ -50,9 +47,9 @@ const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!data.value) return;
const { row, type } = data.value;
if (isNumberOperation.value) {
if (isAppend.value) {
if (!Number.isInteger(appendQty.value) || appendQty.value <= 0) {
message.warning(`${type === 'append' ? '追加数量' : '权重'}必须为正整数`);
message.warning('追加数量必须为正整数');
return;
}
} else if (!reason.value.trim()) {
@@ -63,8 +60,6 @@ const [Modal, modalApi] = useVbenModal({
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() });
}
@@ -83,7 +78,7 @@ const [Modal, modalApi] = useVbenModal({
return;
}
data.value = modalApi.getData<ModalData>();
appendQty.value = data.value.type === 'weight' ? data.value.row.weight : 1;
appendQty.value = 1;
},
});
</script>
@@ -94,10 +89,10 @@ const [Modal, modalApi] = useVbenModal({
<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>
<div v-if="isAppend">
<div class="mb-2">本次追加数量</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 class="mt-2 text-sm text-gray-500">当前需求总量 {{ data.row.totalQty }} </div>
</div>
<div v-else>
<div class="mb-2">操作原因</div>

View File

@@ -44,8 +44,8 @@ const [Modal, modalApi] = useVbenModal({
</script>
<template>
<Modal class="w-[720px]" title="调整当前页顺序">
<div class="mb-3 text-sm text-gray-500">列表始终按顺序号从小到大处理权重模式下也由顺序号决定订单轮转先后</div>
<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>

View File

@@ -65,20 +65,49 @@ WHERE menu.id = COALESCE(
AND menu.deleted=b'0'
AND NOT EXISTS (SELECT 1 FROM system_menu existing WHERE existing.permission=permission.code AND existing.deleted=b'0');
-- 作业权限归属于日计划作业
-- 作业页面独立挂载自身实际使用的按钮权限,避免仅授权作业菜单时按钮全部不可见
INSERT INTO system_menu
(id, name, permission, type, sort, parent_id, path, icon, component, component_name,
status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT 2093000000000000120, '日计划作业操作', 'lms:daily-plan:operate', 3, 1, menu.id,
SELECT permission.id, permission.name, permission.code, 3, permission.sort, menu.id,
'', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM system_menu menu
CROSS JOIN (
SELECT 2093000000000000120 id, 1 sort, '日计划作业操作' name, 'lms:daily-plan:operate' code
UNION ALL SELECT 2093000000000000121, 2, '日计划作业查询', 'lms:daily-plan:query'
UNION ALL SELECT 2093000000000000122, 3, '日计划作业启动', 'lms:daily-plan:start'
UNION ALL SELECT 2093000000000000123, 4, '日计划作业排程', 'lms:daily-plan:schedule'
UNION ALL SELECT 2093000000000000124, 5, '日计划作业策略', 'lms:daily-plan:strategy'
) permission
WHERE menu.id = COALESCE(
(SELECT fixed.id FROM system_menu fixed WHERE fixed.id=2093000000000000101 AND fixed.deleted=b'0'),
(SELECT MIN(fallback.id) FROM system_menu fallback
WHERE fallback.component='lms/daily-plan/work/index' AND fallback.deleted=b'0')
)
AND menu.deleted=b'0'
AND NOT EXISTS (SELECT 1 FROM system_menu WHERE permission='lms:daily-plan:operate' AND deleted=b'0');
AND NOT EXISTS (
SELECT 1 FROM system_menu existing
WHERE existing.parent_id=menu.id AND existing.permission=permission.code AND existing.deleted=b'0'
);
-- 已授权日计划作业菜单的角色同步获得该页面全部按钮权限。
INSERT INTO system_role_menu
(role_id, menu_id, creator, create_time, updater, update_time, deleted, tenant_id)
SELECT DISTINCT work_role.role_id, button.id, '1', NOW(), '1', NOW(), b'0', work_role.tenant_id
FROM system_role_menu work_role
JOIN system_menu work_menu ON work_menu.id=work_role.menu_id
AND work_menu.component='lms/daily-plan/work/index' AND work_menu.deleted=b'0'
JOIN system_menu button ON button.parent_id=work_menu.id AND button.type=3 AND button.deleted=b'0'
AND button.permission IN (
'lms:daily-plan:query', 'lms:daily-plan:start', 'lms:daily-plan:operate',
'lms:daily-plan:schedule', 'lms:daily-plan:strategy'
)
WHERE work_role.deleted=b'0'
AND NOT EXISTS (
SELECT 1 FROM system_role_menu existing
WHERE existing.role_id=work_role.role_id AND existing.menu_id=button.id
AND existing.tenant_id=work_role.tenant_id AND existing.deleted=b'0'
);
-- 已授权管芯库现有子菜单的角色同步获得管芯库父目录权限。
INSERT INTO system_role_menu