feat(mes): 迁移 work task 功能
parent
7bf65041f9
commit
be213b6b31
|
|
@ -40,7 +40,7 @@ const routes: RouteRecordRaw[] = [
|
|||
component: () => import('#/views/mes/wm/barcode/config/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'pro/task/edit',
|
||||
path: 'pro/task/gantt-edit',
|
||||
name: 'MesProTaskGanttEdit',
|
||||
meta: {
|
||||
title: '甘特图编辑',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,239 @@
|
|||
<script lang="ts" setup>
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { gantt } from 'dhtmlx-gantt';
|
||||
|
||||
import { BarcodeBizTypeEnum } from '#/views/mes/utils/constants';
|
||||
|
||||
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css';
|
||||
|
||||
/**
|
||||
* 甘特图组件(基于 dhtmlx-gantt)
|
||||
*
|
||||
* 1. 按工单分组展示生产任务,工单为 project 行,任务为子行
|
||||
* 2. 支持只读预览和拖拽编辑两种模式
|
||||
* 3. 拖拽后触发 taskUpdate 事件,通知父组件批量保存
|
||||
* 4. 时间刻度:周 → 日 → 8 小时(1 工作日 = 8 小时)
|
||||
*/
|
||||
defineOptions({ name: 'GanttChart', inheritAttrs: false });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
height?: number; // 甘特图高度
|
||||
readonly?: boolean; // 是否只读
|
||||
tasks?: any[]; // 甘特图任务数据
|
||||
}>(),
|
||||
{
|
||||
height: 350,
|
||||
readonly: false,
|
||||
tasks: () => [],
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
taskClick: [id: number | string];
|
||||
taskUpdate: [task: any];
|
||||
}>();
|
||||
|
||||
const ganttContainer = ref<HTMLElement>(); // 甘特图挂载容器
|
||||
const ganttInited = ref(false); // 甘特图是否已初始化
|
||||
|
||||
/** 初始化甘特图配置 */
|
||||
function initGantt() {
|
||||
if (!ganttContainer.value) {
|
||||
return;
|
||||
}
|
||||
gantt.i18n.setLocale('cn');
|
||||
|
||||
gantt.config.readonly = props.readonly;
|
||||
gantt.config.date_format = '%Y-%m-%d %H:%i:%s';
|
||||
gantt.config.duration_unit = 'hour'; // 持续时间单位为小时,配合 duration_step 实现工作日单位
|
||||
gantt.config.duration_step = 8; // 1 工作日 = 8 小时
|
||||
gantt.config.row_height = 36;
|
||||
gantt.config.bar_height = 24;
|
||||
gantt.config.fit_tasks = true;
|
||||
gantt.config.auto_scheduling = false;
|
||||
gantt.config.drag_links = false;
|
||||
gantt.config.details_on_create = true;
|
||||
gantt.config.details_on_dblclick = true;
|
||||
gantt.config.show_progress = true;
|
||||
gantt.config.open_tree_initially = true;
|
||||
gantt.config.auto_types = false;
|
||||
gantt.config.drag_move = !props.readonly;
|
||||
gantt.config.drag_resize = !props.readonly;
|
||||
gantt.config.drag_progress = false;
|
||||
|
||||
// lightbox 弹窗只保留时间编辑,去掉描述编辑和删除按钮
|
||||
gantt.config.lightbox.sections = [
|
||||
{ name: 'time', type: 'duration', map_to: 'auto' },
|
||||
];
|
||||
gantt.config.buttons_left = ['gantt_save_btn'];
|
||||
gantt.config.buttons_right = ['gantt_cancel_btn'];
|
||||
|
||||
// 时间刻度:周 > 日 > 8 小时
|
||||
const weekScaleTemplate = (date: Date) => {
|
||||
const dateToStr = gantt.date.date_to_str('%M %d');
|
||||
const endDate = gantt.date.add(gantt.date.add(date, 1, 'week'), -1, 'day');
|
||||
return `${dateToStr(date)} - ${dateToStr(endDate)}`;
|
||||
};
|
||||
const dayTemplate = (date: Date) => gantt.date.date_to_str('%M %d')(date);
|
||||
const daysStyle = (date: Date) =>
|
||||
date.getDay() === 0 || date.getDay() === 6 ? 'weekend' : '';
|
||||
gantt.config.scales = [
|
||||
{ unit: 'week', step: 1, format: weekScaleTemplate },
|
||||
{ unit: 'day', step: 1, format: dayTemplate, css: daysStyle },
|
||||
{ unit: 'hour', step: 8, format: '%H:%i' },
|
||||
];
|
||||
gantt.config.scale_height = 50;
|
||||
gantt.config.show_task_cells = true;
|
||||
|
||||
gantt.config.columns = [
|
||||
{ name: 'text', label: '任务名称', tree: true, width: 180, resize: true },
|
||||
{ name: 'workstation', label: '工作站', align: 'center', width: 100, resize: true },
|
||||
{ name: 'process', label: '工序', align: 'center', width: 100, resize: true },
|
||||
{ name: 'start_date', label: '开始时间', align: 'center', width: 130 },
|
||||
{ name: 'end_date', label: '结束时间', align: 'center', width: 130 },
|
||||
];
|
||||
|
||||
gantt.plugins({ marker: true, tooltip: true });
|
||||
gantt.addMarker({ start_date: new Date(), css: 'today', text: '今天' });
|
||||
|
||||
gantt.templates.task_text = (_start: any, _end: any, task: any) => {
|
||||
const percent = Math.round((task.progress || 0) * 100);
|
||||
if (task.type === 'project') {
|
||||
return `<b>生产工单:</b> ${task.text} <span>完成比例:${percent}%</span>`;
|
||||
}
|
||||
return `<b>生产任务:</b> ${task.process || ''} ${task.text} <span>完成比例:${percent}%</span>`;
|
||||
};
|
||||
|
||||
gantt.templates.tooltip_text = (_start: any, _end: any, task: any) => {
|
||||
const percent = Math.round((task.progress || 0) * 100);
|
||||
if (task.type === 'project') {
|
||||
return `<b>生产工单:</b> ${task.text} <span>完成比例:${percent}%</span>`;
|
||||
}
|
||||
return `<b>生产任务:</b> ${task.process || ''} ${task.text} <span>完成比例:${percent}%</span>`;
|
||||
};
|
||||
|
||||
gantt.templates.task_class = (_start: any, _end: any, task: any) =>
|
||||
task.type === gantt.config.types.project ? 'gantt-project-bar' : '';
|
||||
gantt.templates.timeline_cell_class = () => '';
|
||||
gantt.templates.task_row_class = () => '';
|
||||
|
||||
// 编辑事件监听(通过 lightbox 弹窗编辑后触发)
|
||||
if (!props.readonly) {
|
||||
gantt.attachEvent('onAfterTaskUpdate', (id: number | string) => {
|
||||
const task = gantt.getTask(id);
|
||||
// 只回写 task 节点,避免把工单(project)节点当成任务保存
|
||||
if (task.type !== gantt.config.types.task || !task.originalId) {
|
||||
return;
|
||||
}
|
||||
emit('taskUpdate', {
|
||||
duration: task.duration,
|
||||
endTime: task.end_date,
|
||||
id: task.originalId,
|
||||
startTime: task.start_date,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
gantt.attachEvent('onTaskClick', (id: number | string) => {
|
||||
emit('taskClick', id);
|
||||
return true;
|
||||
});
|
||||
|
||||
gantt.init(ganttContainer.value);
|
||||
ganttInited.value = true;
|
||||
}
|
||||
|
||||
/** 加载数据到甘特图 */
|
||||
function loadData(tasks: any[]) {
|
||||
if (!ganttInited.value) {
|
||||
return;
|
||||
}
|
||||
gantt.clearAll();
|
||||
// 后端 type 使用整数业务类型,需映射为 gantt 类型字符串
|
||||
const typeMap: Record<number, string> = {
|
||||
[BarcodeBizTypeEnum.WORKORDER]: 'project',
|
||||
[BarcodeBizTypeEnum.TASK]: 'task',
|
||||
};
|
||||
gantt.parse({
|
||||
data: tasks.map((item: any) => ({
|
||||
...item,
|
||||
type: typeMap[item.type] || item.type,
|
||||
start_date: item.startDate ? new Date(item.startDate) : undefined,
|
||||
end_date: item.endDate ? new Date(item.endDate) : undefined,
|
||||
})),
|
||||
links: [],
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.tasks,
|
||||
(val) => {
|
||||
if (val?.length && ganttInited.value) {
|
||||
loadData(val);
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
initGantt();
|
||||
if (props.tasks?.length) {
|
||||
loadData(props.tasks);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (ganttInited.value) {
|
||||
gantt.clearAll();
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({ loadData });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="ganttContainer" :style="{ width: '100%', height: `${height}px` }"></div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 今天标记线 */
|
||||
.gantt_marker.today {
|
||||
background-color: #f44;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.gantt_marker.today .gantt_marker_content {
|
||||
font-size: 12px;
|
||||
color: #f44;
|
||||
}
|
||||
|
||||
/* 工单(project)行样式 */
|
||||
.gantt-project-bar .gantt_task_progress {
|
||||
background: #7b68ee;
|
||||
}
|
||||
|
||||
/* 甘特条圆角 */
|
||||
.gantt_task_line {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 周末背景色 */
|
||||
.weekend {
|
||||
background: #f0f0f0 !important;
|
||||
}
|
||||
|
||||
/* 行悬浮高亮 */
|
||||
.gantt_grid_data .gantt_row:hover,
|
||||
.gantt_grid_data .gantt_row.odd:hover {
|
||||
background-color: #f3f1fe !important;
|
||||
}
|
||||
|
||||
/* 选中行高亮 */
|
||||
.gantt_grid_data .gantt_row.gantt_selected,
|
||||
.gantt_grid_data .gantt_row.odd.gantt_selected,
|
||||
.gantt_task_row.gantt_selected {
|
||||
background-color: #f3f1fe !important;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -47,23 +47,19 @@ const showClear = computed(() => // 是否显示清空图标
|
|||
props.allowClear &&
|
||||
!props.disabled &&
|
||||
hovering.value &&
|
||||
props.modelValue !== undefined,
|
||||
props.modelValue != null,
|
||||
);
|
||||
|
||||
/** 根据任务编号回显选择器 */
|
||||
async function resolveItemById(id: number | undefined) {
|
||||
if (id === undefined) {
|
||||
if (id == null) {
|
||||
selectedItem.value = undefined;
|
||||
return;
|
||||
}
|
||||
if (selectedItem.value?.id === id) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
selectedItem.value = await getTask(id);
|
||||
} catch (error) {
|
||||
console.error('[ProTaskSelect] resolveItemById failed:', error);
|
||||
}
|
||||
selectedItem.value = await getTask(id);
|
||||
}
|
||||
|
||||
watch(
|
||||
|
|
@ -92,8 +88,7 @@ function handleClick(event: MouseEvent) {
|
|||
clearSelected();
|
||||
return;
|
||||
}
|
||||
const selectedIds =
|
||||
props.modelValue === undefined ? [] : [props.modelValue];
|
||||
const selectedIds = props.modelValue == null ? [] : [props.modelValue];
|
||||
dialogRef.value?.open(selectedIds, {
|
||||
multiple: false,
|
||||
workOrderId: props.workOrderId,
|
||||
|
|
|
|||
|
|
@ -279,6 +279,15 @@ export function useScheduleFormSchema(): VbenFormSchema[] {
|
|||
valueFormat: 'x',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '工单状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
disabled: true,
|
||||
options: getDictOptions(DICT_TYPE.MES_PRO_WORK_ORDER_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { DocAlert, Page } from '@vben/common-ui';
|
||||
|
||||
import { Badge, Button, message } from 'ant-design-vue';
|
||||
|
||||
import { getGanttTaskList, updateTask } from '#/api/mes/pro/task';
|
||||
|
||||
import GanttChart from '../components/gantt-chart.vue';
|
||||
|
||||
const submitting = ref(false); // 批量保存按钮加载态
|
||||
const taskList = ref<any[]>([]); // 甘特图任务数据
|
||||
const pendingChanges = ref(new Map<number, any>()); // 待保存的修改 Map<taskId, changeData>
|
||||
|
||||
const pendingCount = computed(() => pendingChanges.value.size); // 待保存数量
|
||||
const ganttHeight = computed(() => window.innerHeight - 220); // 甘特图高度
|
||||
|
||||
/** 加载甘特图数据 */
|
||||
async function loadGanttData() {
|
||||
taskList.value = await getGanttTaskList({});
|
||||
}
|
||||
|
||||
/** 任务编辑回调:缓存待保存的修改 */
|
||||
function handleTaskUpdate(change: any) {
|
||||
pendingChanges.value.set(change.id, change);
|
||||
}
|
||||
|
||||
/** 批量保存 */
|
||||
async function handleSave() {
|
||||
if (pendingChanges.value.size === 0) {
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
const changes = [...pendingChanges.value.values()];
|
||||
await Promise.all(
|
||||
changes.map((change) =>
|
||||
updateTask({
|
||||
duration: change.duration,
|
||||
endTime: change.endTime,
|
||||
id: change.id,
|
||||
startTime: change.startTime,
|
||||
}),
|
||||
),
|
||||
);
|
||||
message.success(`已保存 ${changes.length} 条修改`);
|
||||
pendingChanges.value = new Map();
|
||||
await loadGanttData();
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新 */
|
||||
async function handleRefresh() {
|
||||
pendingChanges.value = new Map();
|
||||
await loadGanttData();
|
||||
}
|
||||
|
||||
onMounted(loadGanttData);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【生产】生产排产、工序流转卡"
|
||||
url="https://doc.iocoder.cn/mes/pro/schedule-card/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-sm text-gray-500">
|
||||
可直接拖拽/拉伸任务条,或双击编辑开始时间和时长,修改后点击「批量保存」
|
||||
</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<Badge :count="pendingCount">
|
||||
<Button
|
||||
type="primary"
|
||||
:disabled="pendingCount === 0"
|
||||
:loading="submitting"
|
||||
@click="handleSave"
|
||||
>
|
||||
批量保存
|
||||
</Button>
|
||||
</Badge>
|
||||
<Button @click="handleRefresh">刷新</Button>
|
||||
</div>
|
||||
</div>
|
||||
<GanttChart
|
||||
:height="ganttHeight"
|
||||
:readonly="false"
|
||||
:tasks="taskList"
|
||||
@task-update="handleTaskUpdate"
|
||||
/>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card } from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getGanttTaskList } from '#/api/mes/pro/task';
|
||||
import { getWorkOrderPage } from '#/api/mes/pro/workorder';
|
||||
import {
|
||||
MesProWorkOrderStatusEnum,
|
||||
MesProWorkOrderTypeEnum,
|
||||
} from '#/views/mes/utils/constants';
|
||||
|
||||
import GanttChart from './components/gantt-chart.vue';
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import ScheduleForm from './modules/schedule-form.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const ganttTasks = ref<any[]>([]); // 甘特图预览数据
|
||||
|
||||
const [ScheduleModal, scheduleModalApi] = useVbenModal({
|
||||
connectedComponent: ScheduleForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
loadGanttPreview();
|
||||
}
|
||||
|
||||
/** 加载甘特图预览数据 */
|
||||
async function loadGanttPreview() {
|
||||
ganttTasks.value = await getGanttTaskList({
|
||||
status: MesProWorkOrderStatusEnum.CONFIRMED,
|
||||
type: MesProWorkOrderTypeEnum.SELF,
|
||||
});
|
||||
}
|
||||
|
||||
/** 查看工单详情 */
|
||||
function handleDetail(row: MesProWorkOrderApi.WorkOrder) {
|
||||
scheduleModalApi.setData({ formType: 'detail', id: row.id }).open();
|
||||
}
|
||||
|
||||
/** 排产 */
|
||||
function handleSchedule(row: MesProWorkOrderApi.WorkOrder) {
|
||||
scheduleModalApi.setData({ formType: 'schedule', id: row.id }).open();
|
||||
}
|
||||
|
||||
/** 打开甘特图编辑页面 */
|
||||
function handleGanttEdit() {
|
||||
router.push({ name: 'MesProTaskGanttEdit' });
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getWorkOrderPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
status: MesProWorkOrderStatusEnum.CONFIRMED,
|
||||
type: MesProWorkOrderTypeEnum.SELF,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
treeConfig: {
|
||||
parentField: 'parentId',
|
||||
rowField: 'id',
|
||||
transform: true,
|
||||
expandAll: true,
|
||||
reserve: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MesProWorkOrderApi.WorkOrder>,
|
||||
});
|
||||
|
||||
onMounted(loadGanttPreview);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【生产】生产排产、工序流转卡"
|
||||
url="https://doc.iocoder.cn/mes/pro/schedule-card/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<ScheduleModal @success="handleRefresh" />
|
||||
|
||||
<!-- 排产甘特图预览 -->
|
||||
<Card title="排产甘特图" class="mb-4">
|
||||
<GanttChart :height="350" :readonly="true" :tasks="ganttTasks" />
|
||||
</Card>
|
||||
|
||||
<Grid table-title="待排产工单">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '甘特图编辑',
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
onClick: handleGanttEdit,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #code="{ row }">
|
||||
<Button type="link" @click="handleDetail(row)">
|
||||
{{ row.code }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '排产',
|
||||
type: 'link',
|
||||
auth: ['mes:pro-task:create'],
|
||||
ifShow: row.status === MesProWorkOrderStatusEnum.CONFIRMED,
|
||||
onClick: handleSchedule.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
<script lang="ts" setup>
|
||||
import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
|
||||
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, message, Popconfirm, Steps } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getRouteProcessListByProduct } from '#/api/mes/pro/route/process';
|
||||
import { finishWorkOrder, getWorkOrder } from '#/api/mes/pro/workorder';
|
||||
|
||||
import { useScheduleFormSchema } from '../data';
|
||||
import TaskList from './task-list.vue';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formType = ref<'detail' | 'schedule'>('schedule');
|
||||
const workOrder = ref<MesProWorkOrderApi.WorkOrder>(); // 当前工单详情
|
||||
const routeProcessList = ref<MesProRouteProcessApi.RouteProcess[]>([]); // 工艺路线工序列表
|
||||
const activeProcessStep = ref(0); // 当前工序步骤索引
|
||||
const currentRouteId = ref(0); // 当前工艺路线编号
|
||||
|
||||
const isReadonly = computed(() => formType.value === 'detail'); // 详情态只读
|
||||
const getTitle = computed(() =>
|
||||
formType.value === 'detail' ? '工单详情' : '生产排产',
|
||||
);
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-1',
|
||||
labelWidth: 100,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useScheduleFormSchema(),
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-3',
|
||||
});
|
||||
|
||||
/** 加载工艺路线工序列表 */
|
||||
async function loadRouteProcesses(productId: number) {
|
||||
const processes = await getRouteProcessListByProduct(productId);
|
||||
if (!processes || processes.length === 0) {
|
||||
message.warning('当前产品未配置工艺路线,请先在工艺路线中维护');
|
||||
return;
|
||||
}
|
||||
currentRouteId.value = processes[0]!.routeId ?? 0;
|
||||
routeProcessList.value = [...processes].toSorted(
|
||||
(a, b) => (a.sort ?? 0) - (b.sort ?? 0),
|
||||
);
|
||||
}
|
||||
|
||||
/** 完成工单 */
|
||||
async function handleFinish() {
|
||||
if (!workOrder.value?.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
await finishWorkOrder(workOrder.value.id);
|
||||
message.success('工单已完成');
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
showConfirmButton: false,
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
workOrder.value = undefined;
|
||||
routeProcessList.value = [];
|
||||
activeProcessStep.value = 0;
|
||||
currentRouteId.value = 0;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<{ formType: 'detail' | 'schedule'; id: number }>();
|
||||
formType.value = data.formType;
|
||||
activeProcessStep.value = 0;
|
||||
routeProcessList.value = [];
|
||||
modalApi.lock();
|
||||
try {
|
||||
workOrder.value = await getWorkOrder(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(workOrder.value);
|
||||
if (workOrder.value.productId) {
|
||||
await loadRouteProcesses(workOrder.value.productId);
|
||||
}
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-4/5">
|
||||
<Form class="mx-4" />
|
||||
<!-- 工序步骤导航 + 当前工序任务列表 -->
|
||||
<template v-if="routeProcessList.length > 0 && workOrder?.id">
|
||||
<Steps
|
||||
v-model:current="activeProcessStep"
|
||||
class="my-4 px-4"
|
||||
size="small"
|
||||
type="navigation"
|
||||
>
|
||||
<Steps.Step
|
||||
v-for="rp in routeProcessList"
|
||||
:key="rp.processId"
|
||||
:title="rp.processName"
|
||||
/>
|
||||
</Steps>
|
||||
<Card
|
||||
v-for="(rp, index) in routeProcessList"
|
||||
v-show="activeProcessStep === index"
|
||||
:key="rp.processId"
|
||||
class="mx-4"
|
||||
>
|
||||
<TaskList
|
||||
:color-code="rp.colorCode"
|
||||
:disabled="isReadonly"
|
||||
:item-id="workOrder.productId"
|
||||
:process-id="rp.processId!"
|
||||
:route-id="currentRouteId"
|
||||
:work-order-id="workOrder.id!"
|
||||
/>
|
||||
</Card>
|
||||
</template>
|
||||
<template #prepend-footer>
|
||||
<div class="flex flex-auto items-center gap-2">
|
||||
<Popconfirm
|
||||
v-if="formType === 'schedule'"
|
||||
title="确认要完成该工单吗?完成后工单下所有任务将标记为已完成。"
|
||||
@confirm="handleFinish"
|
||||
>
|
||||
<Button type="primary">完成</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
<script lang="ts" setup>
|
||||
import type { MesProTaskApi } from '#/api/mes/pro/task';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createTask, getTask, updateTask } from '#/api/mes/pro/task';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useTaskFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<MesProTaskApi.Task>();
|
||||
const context = ref<{
|
||||
colorCode?: string;
|
||||
itemId?: number;
|
||||
processId?: number;
|
||||
routeId?: number;
|
||||
workOrderId?: number;
|
||||
}>({}); // 新增任务时的工单 / 工序上下文
|
||||
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['生产任务'])
|
||||
: $t('ui.actionTitle.create', ['生产任务']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-1',
|
||||
labelWidth: 100,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [],
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-3',
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as MesProTaskApi.Task;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateTask({ ...formData.value, ...data })
|
||||
: createTask({ ...context.value, ...data }));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
context.value = {};
|
||||
return;
|
||||
}
|
||||
formApi.setState({ schema: useTaskFormSchema(formApi) });
|
||||
// 加载数据
|
||||
const data = modalApi.getData<{
|
||||
colorCode?: string;
|
||||
id?: number;
|
||||
itemId?: number;
|
||||
processId?: number;
|
||||
routeId?: number;
|
||||
workOrderId?: number;
|
||||
}>();
|
||||
if (data?.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getTask(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
return;
|
||||
}
|
||||
context.value = {
|
||||
colorCode: data.colorCode,
|
||||
itemId: data.itemId,
|
||||
processId: data.processId,
|
||||
routeId: data.routeId,
|
||||
workOrderId: data.workOrderId,
|
||||
};
|
||||
await formApi.setValues({
|
||||
colorCode: data.colorCode || '#00AEF3',
|
||||
duration: 1,
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-3/5">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MesProTaskApi } from '#/api/mes/pro/task';
|
||||
|
||||
import { computed, watch } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteTask, getTaskPage } from '#/api/mes/pro/task';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useTaskGridColumns } from '../data';
|
||||
import TaskForm from './task-form.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
colorCode?: string;
|
||||
disabled?: boolean;
|
||||
itemId?: number;
|
||||
processId: number;
|
||||
routeId: number;
|
||||
workOrderId: number;
|
||||
}>();
|
||||
|
||||
const editable = computed(() => !props.disabled); // 是否可维护任务
|
||||
|
||||
const [TaskFormModal, taskFormModalApi] = useVbenModal({
|
||||
connectedComponent: TaskForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 新增任务 */
|
||||
function handleCreate() {
|
||||
taskFormModalApi
|
||||
.setData({
|
||||
colorCode: props.colorCode,
|
||||
itemId: props.itemId,
|
||||
processId: props.processId,
|
||||
routeId: props.routeId,
|
||||
workOrderId: props.workOrderId,
|
||||
})
|
||||
.open();
|
||||
}
|
||||
|
||||
/** 编辑任务 */
|
||||
function handleEdit(row: MesProTaskApi.Task) {
|
||||
taskFormModalApi.setData({ id: row.id }).open();
|
||||
}
|
||||
|
||||
/** 删除任务 */
|
||||
async function handleDelete(row: MesProTaskApi.Task) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.code]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteTask(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.code]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useTaskGridColumns(editable.value),
|
||||
height: 360,
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => {
|
||||
if (!props.workOrderId) {
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
return await getTaskPage({
|
||||
pageNo: 1,
|
||||
pageSize: 100,
|
||||
processId: props.processId,
|
||||
routeId: props.routeId,
|
||||
workOrderId: props.workOrderId,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MesProTaskApi.Task>,
|
||||
});
|
||||
|
||||
// 工序步骤切换时重新加载当前工序的任务
|
||||
watch(
|
||||
() => props.processId,
|
||||
() => handleRefresh(),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<TaskFormModal @success="handleRefresh" />
|
||||
<Grid table-title="生产任务">
|
||||
<template v-if="editable" #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '新增任务',
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['mes:pro-task:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #colorCode="{ row }">
|
||||
<div
|
||||
class="mx-auto size-5 rounded"
|
||||
:style="{ background: row.colorCode || '#00AEF3' }"
|
||||
></div>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['mes:pro-task:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['mes:pro-task:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.code]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -53,6 +53,12 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||
warehouseId: values.warehouseId,
|
||||
placeholder: '请选择库区',
|
||||
}),
|
||||
// 仓库切换时清空库区,避免旧库区条件残留
|
||||
trigger: (values, formApi) => {
|
||||
if (values.locationId !== undefined) {
|
||||
void formApi.setFieldValue('locationId', undefined);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -191,6 +197,12 @@ export function useSelectGridFormSchema(): VbenFormSchema[] {
|
|||
warehouseId: values.warehouseId,
|
||||
placeholder: '请选择库区',
|
||||
}),
|
||||
// 仓库切换时清空库区
|
||||
trigger: (values, formApi) => {
|
||||
if (values.locationId !== undefined) {
|
||||
void formApi.setFieldValue('locationId', undefined);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -198,11 +210,17 @@ export function useSelectGridFormSchema(): VbenFormSchema[] {
|
|||
label: '库位',
|
||||
component: markRaw(WmWarehouseAreaSelect),
|
||||
dependencies: {
|
||||
triggerFields: ['locationId'],
|
||||
triggerFields: ['warehouseId', 'locationId'],
|
||||
componentProps: (values) => ({
|
||||
locationId: values.locationId,
|
||||
placeholder: '请选择库位',
|
||||
}),
|
||||
// 仓库或库区切换时清空库位
|
||||
trigger: (values, formApi) => {
|
||||
if (values.areaId !== undefined) {
|
||||
void formApi.setFieldValue('areaId', undefined);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ const routes: RouteRecordRaw[] = [
|
|||
component: () => import('#/views/mes/wm/barcode/config/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'pro/task/edit',
|
||||
path: 'pro/task/gantt-edit',
|
||||
name: 'MesProTaskGanttEdit',
|
||||
meta: {
|
||||
title: '甘特图编辑',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,239 @@
|
|||
<script lang="ts" setup>
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { gantt } from 'dhtmlx-gantt';
|
||||
|
||||
import { BarcodeBizTypeEnum } from '#/views/mes/utils/constants';
|
||||
|
||||
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css';
|
||||
|
||||
/**
|
||||
* 甘特图组件(基于 dhtmlx-gantt)
|
||||
*
|
||||
* 1. 按工单分组展示生产任务,工单为 project 行,任务为子行
|
||||
* 2. 支持只读预览和拖拽编辑两种模式
|
||||
* 3. 拖拽后触发 taskUpdate 事件,通知父组件批量保存
|
||||
* 4. 时间刻度:周 → 日 → 8 小时(1 工作日 = 8 小时)
|
||||
*/
|
||||
defineOptions({ name: 'GanttChart', inheritAttrs: false });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
height?: number; // 甘特图高度
|
||||
readonly?: boolean; // 是否只读
|
||||
tasks?: any[]; // 甘特图任务数据
|
||||
}>(),
|
||||
{
|
||||
height: 350,
|
||||
readonly: false,
|
||||
tasks: () => [],
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
taskClick: [id: number | string];
|
||||
taskUpdate: [task: any];
|
||||
}>();
|
||||
|
||||
const ganttContainer = ref<HTMLElement>(); // 甘特图挂载容器
|
||||
const ganttInited = ref(false); // 甘特图是否已初始化
|
||||
|
||||
/** 初始化甘特图配置 */
|
||||
function initGantt() {
|
||||
if (!ganttContainer.value) {
|
||||
return;
|
||||
}
|
||||
gantt.i18n.setLocale('cn');
|
||||
|
||||
gantt.config.readonly = props.readonly;
|
||||
gantt.config.date_format = '%Y-%m-%d %H:%i:%s';
|
||||
gantt.config.duration_unit = 'hour'; // 持续时间单位为小时,配合 duration_step 实现工作日单位
|
||||
gantt.config.duration_step = 8; // 1 工作日 = 8 小时
|
||||
gantt.config.row_height = 36;
|
||||
gantt.config.bar_height = 24;
|
||||
gantt.config.fit_tasks = true;
|
||||
gantt.config.auto_scheduling = false;
|
||||
gantt.config.drag_links = false;
|
||||
gantt.config.details_on_create = true;
|
||||
gantt.config.details_on_dblclick = true;
|
||||
gantt.config.show_progress = true;
|
||||
gantt.config.open_tree_initially = true;
|
||||
gantt.config.auto_types = false;
|
||||
gantt.config.drag_move = !props.readonly;
|
||||
gantt.config.drag_resize = !props.readonly;
|
||||
gantt.config.drag_progress = false;
|
||||
|
||||
// lightbox 弹窗只保留时间编辑,去掉描述编辑和删除按钮
|
||||
gantt.config.lightbox.sections = [
|
||||
{ name: 'time', type: 'duration', map_to: 'auto' },
|
||||
];
|
||||
gantt.config.buttons_left = ['gantt_save_btn'];
|
||||
gantt.config.buttons_right = ['gantt_cancel_btn'];
|
||||
|
||||
// 时间刻度:周 > 日 > 8 小时
|
||||
const weekScaleTemplate = (date: Date) => {
|
||||
const dateToStr = gantt.date.date_to_str('%M %d');
|
||||
const endDate = gantt.date.add(gantt.date.add(date, 1, 'week'), -1, 'day');
|
||||
return `${dateToStr(date)} - ${dateToStr(endDate)}`;
|
||||
};
|
||||
const dayTemplate = (date: Date) => gantt.date.date_to_str('%M %d')(date);
|
||||
const daysStyle = (date: Date) =>
|
||||
date.getDay() === 0 || date.getDay() === 6 ? 'weekend' : '';
|
||||
gantt.config.scales = [
|
||||
{ unit: 'week', step: 1, format: weekScaleTemplate },
|
||||
{ unit: 'day', step: 1, format: dayTemplate, css: daysStyle },
|
||||
{ unit: 'hour', step: 8, format: '%H:%i' },
|
||||
];
|
||||
gantt.config.scale_height = 50;
|
||||
gantt.config.show_task_cells = true;
|
||||
|
||||
gantt.config.columns = [
|
||||
{ name: 'text', label: '任务名称', tree: true, width: 180, resize: true },
|
||||
{ name: 'workstation', label: '工作站', align: 'center', width: 100, resize: true },
|
||||
{ name: 'process', label: '工序', align: 'center', width: 100, resize: true },
|
||||
{ name: 'start_date', label: '开始时间', align: 'center', width: 130 },
|
||||
{ name: 'end_date', label: '结束时间', align: 'center', width: 130 },
|
||||
];
|
||||
|
||||
gantt.plugins({ marker: true, tooltip: true });
|
||||
gantt.addMarker({ start_date: new Date(), css: 'today', text: '今天' });
|
||||
|
||||
gantt.templates.task_text = (_start: any, _end: any, task: any) => {
|
||||
const percent = Math.round((task.progress || 0) * 100);
|
||||
if (task.type === 'project') {
|
||||
return `<b>生产工单:</b> ${task.text} <span>完成比例:${percent}%</span>`;
|
||||
}
|
||||
return `<b>生产任务:</b> ${task.process || ''} ${task.text} <span>完成比例:${percent}%</span>`;
|
||||
};
|
||||
|
||||
gantt.templates.tooltip_text = (_start: any, _end: any, task: any) => {
|
||||
const percent = Math.round((task.progress || 0) * 100);
|
||||
if (task.type === 'project') {
|
||||
return `<b>生产工单:</b> ${task.text} <span>完成比例:${percent}%</span>`;
|
||||
}
|
||||
return `<b>生产任务:</b> ${task.process || ''} ${task.text} <span>完成比例:${percent}%</span>`;
|
||||
};
|
||||
|
||||
gantt.templates.task_class = (_start: any, _end: any, task: any) =>
|
||||
task.type === gantt.config.types.project ? 'gantt-project-bar' : '';
|
||||
gantt.templates.timeline_cell_class = () => '';
|
||||
gantt.templates.task_row_class = () => '';
|
||||
|
||||
// 编辑事件监听(通过 lightbox 弹窗编辑后触发)
|
||||
if (!props.readonly) {
|
||||
gantt.attachEvent('onAfterTaskUpdate', (id: number | string) => {
|
||||
const task = gantt.getTask(id);
|
||||
// 只回写 task 节点,避免把工单(project)节点当成任务保存
|
||||
if (task.type !== gantt.config.types.task || !task.originalId) {
|
||||
return;
|
||||
}
|
||||
emit('taskUpdate', {
|
||||
duration: task.duration,
|
||||
endTime: task.end_date,
|
||||
id: task.originalId,
|
||||
startTime: task.start_date,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
gantt.attachEvent('onTaskClick', (id: number | string) => {
|
||||
emit('taskClick', id);
|
||||
return true;
|
||||
});
|
||||
|
||||
gantt.init(ganttContainer.value);
|
||||
ganttInited.value = true;
|
||||
}
|
||||
|
||||
/** 加载数据到甘特图 */
|
||||
function loadData(tasks: any[]) {
|
||||
if (!ganttInited.value) {
|
||||
return;
|
||||
}
|
||||
gantt.clearAll();
|
||||
// 后端 type 使用整数业务类型,需映射为 gantt 类型字符串
|
||||
const typeMap: Record<number, string> = {
|
||||
[BarcodeBizTypeEnum.WORKORDER]: 'project',
|
||||
[BarcodeBizTypeEnum.TASK]: 'task',
|
||||
};
|
||||
gantt.parse({
|
||||
data: tasks.map((item: any) => ({
|
||||
...item,
|
||||
type: typeMap[item.type] || item.type,
|
||||
start_date: item.startDate ? new Date(item.startDate) : undefined,
|
||||
end_date: item.endDate ? new Date(item.endDate) : undefined,
|
||||
})),
|
||||
links: [],
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.tasks,
|
||||
(val) => {
|
||||
if (val?.length && ganttInited.value) {
|
||||
loadData(val);
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
initGantt();
|
||||
if (props.tasks?.length) {
|
||||
loadData(props.tasks);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (ganttInited.value) {
|
||||
gantt.clearAll();
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({ loadData });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="ganttContainer" :style="{ width: '100%', height: `${height}px` }"></div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 今天标记线 */
|
||||
.gantt_marker.today {
|
||||
background-color: #f44;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.gantt_marker.today .gantt_marker_content {
|
||||
font-size: 12px;
|
||||
color: #f44;
|
||||
}
|
||||
|
||||
/* 工单(project)行样式 */
|
||||
.gantt-project-bar .gantt_task_progress {
|
||||
background: #7b68ee;
|
||||
}
|
||||
|
||||
/* 甘特条圆角 */
|
||||
.gantt_task_line {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 周末背景色 */
|
||||
.weekend {
|
||||
background: #f0f0f0 !important;
|
||||
}
|
||||
|
||||
/* 行悬浮高亮 */
|
||||
.gantt_grid_data .gantt_row:hover,
|
||||
.gantt_grid_data .gantt_row.odd:hover {
|
||||
background-color: #f3f1fe !important;
|
||||
}
|
||||
|
||||
/* 选中行高亮 */
|
||||
.gantt_grid_data .gantt_row.gantt_selected,
|
||||
.gantt_grid_data .gantt_row.odd.gantt_selected,
|
||||
.gantt_task_row.gantt_selected {
|
||||
background-color: #f3f1fe !important;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -47,23 +47,19 @@ const showClear = computed(() => // 是否显示清空图标
|
|||
props.clearable &&
|
||||
!props.disabled &&
|
||||
hovering.value &&
|
||||
props.modelValue !== undefined,
|
||||
props.modelValue != null,
|
||||
);
|
||||
|
||||
/** 根据任务编号回显选择器 */
|
||||
async function resolveItemById(id: number | undefined) {
|
||||
if (id === undefined) {
|
||||
if (id == null) {
|
||||
selectedItem.value = undefined;
|
||||
return;
|
||||
}
|
||||
if (selectedItem.value?.id === id) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
selectedItem.value = await getTask(id);
|
||||
} catch (error) {
|
||||
console.error('[ProTaskSelect] resolveItemById failed:', error);
|
||||
}
|
||||
selectedItem.value = await getTask(id);
|
||||
}
|
||||
|
||||
watch(
|
||||
|
|
@ -92,8 +88,7 @@ function handleClick(event: MouseEvent) {
|
|||
clearSelected();
|
||||
return;
|
||||
}
|
||||
const selectedIds =
|
||||
props.modelValue === undefined ? [] : [props.modelValue];
|
||||
const selectedIds = props.modelValue == null ? [] : [props.modelValue];
|
||||
dialogRef.value?.open(selectedIds, {
|
||||
multiple: false,
|
||||
workOrderId: props.workOrderId,
|
||||
|
|
|
|||
|
|
@ -280,6 +280,15 @@ export function useScheduleFormSchema(): VbenFormSchema[] {
|
|||
valueFormat: 'x',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '工单状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
disabled: true,
|
||||
options: getDictOptions(DICT_TYPE.MES_PRO_WORK_ORDER_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { DocAlert, Page } from '@vben/common-ui';
|
||||
|
||||
import { ElBadge, ElButton, ElMessage } from 'element-plus';
|
||||
|
||||
import { getGanttTaskList, updateTask } from '#/api/mes/pro/task';
|
||||
|
||||
import GanttChart from '../components/gantt-chart.vue';
|
||||
|
||||
const submitting = ref(false); // 批量保存按钮加载态
|
||||
const taskList = ref<any[]>([]); // 甘特图任务数据
|
||||
const pendingChanges = ref(new Map<number, any>()); // 待保存的修改 Map<taskId, changeData>
|
||||
|
||||
const pendingCount = computed(() => pendingChanges.value.size); // 待保存数量
|
||||
const ganttHeight = computed(() => window.innerHeight - 220); // 甘特图高度
|
||||
|
||||
/** 加载甘特图数据 */
|
||||
async function loadGanttData() {
|
||||
taskList.value = await getGanttTaskList({});
|
||||
}
|
||||
|
||||
/** 任务编辑回调:缓存待保存的修改 */
|
||||
function handleTaskUpdate(change: any) {
|
||||
pendingChanges.value.set(change.id, change);
|
||||
}
|
||||
|
||||
/** 批量保存 */
|
||||
async function handleSave() {
|
||||
if (pendingChanges.value.size === 0) {
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
const changes = [...pendingChanges.value.values()];
|
||||
await Promise.all(
|
||||
changes.map((change) =>
|
||||
updateTask({
|
||||
duration: change.duration,
|
||||
endTime: change.endTime,
|
||||
id: change.id,
|
||||
startTime: change.startTime,
|
||||
}),
|
||||
),
|
||||
);
|
||||
ElMessage.success(`已保存 ${changes.length} 条修改`);
|
||||
pendingChanges.value = new Map();
|
||||
await loadGanttData();
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新 */
|
||||
async function handleRefresh() {
|
||||
pendingChanges.value = new Map();
|
||||
await loadGanttData();
|
||||
}
|
||||
|
||||
onMounted(loadGanttData);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【生产】生产排产、工序流转卡"
|
||||
url="https://doc.iocoder.cn/mes/pro/schedule-card/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-sm text-gray-500">
|
||||
可直接拖拽/拉伸任务条,或双击编辑开始时间和时长,修改后点击「批量保存」
|
||||
</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<ElBadge :value="pendingCount" :hidden="pendingCount === 0">
|
||||
<ElButton
|
||||
type="primary"
|
||||
:disabled="pendingCount === 0"
|
||||
:loading="submitting"
|
||||
@click="handleSave"
|
||||
>
|
||||
批量保存
|
||||
</ElButton>
|
||||
</ElBadge>
|
||||
<ElButton @click="handleRefresh">刷新</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
<GanttChart
|
||||
:height="ganttHeight"
|
||||
:readonly="false"
|
||||
:tasks="taskList"
|
||||
@task-update="handleTaskUpdate"
|
||||
/>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElButton, ElCard } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getGanttTaskList } from '#/api/mes/pro/task';
|
||||
import { getWorkOrderPage } from '#/api/mes/pro/workorder';
|
||||
import {
|
||||
MesProWorkOrderStatusEnum,
|
||||
MesProWorkOrderTypeEnum,
|
||||
} from '#/views/mes/utils/constants';
|
||||
|
||||
import GanttChart from './components/gantt-chart.vue';
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import ScheduleForm from './modules/schedule-form.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const ganttTasks = ref<any[]>([]); // 甘特图预览数据
|
||||
|
||||
const [ScheduleModal, scheduleModalApi] = useVbenModal({
|
||||
connectedComponent: ScheduleForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
loadGanttPreview();
|
||||
}
|
||||
|
||||
/** 加载甘特图预览数据 */
|
||||
async function loadGanttPreview() {
|
||||
ganttTasks.value = await getGanttTaskList({
|
||||
status: MesProWorkOrderStatusEnum.CONFIRMED,
|
||||
type: MesProWorkOrderTypeEnum.SELF,
|
||||
});
|
||||
}
|
||||
|
||||
/** 查看工单详情 */
|
||||
function handleDetail(row: MesProWorkOrderApi.WorkOrder) {
|
||||
scheduleModalApi.setData({ formType: 'detail', id: row.id }).open();
|
||||
}
|
||||
|
||||
/** 排产 */
|
||||
function handleSchedule(row: MesProWorkOrderApi.WorkOrder) {
|
||||
scheduleModalApi.setData({ formType: 'schedule', id: row.id }).open();
|
||||
}
|
||||
|
||||
/** 打开甘特图编辑页面 */
|
||||
function handleGanttEdit() {
|
||||
router.push({ name: 'MesProTaskGanttEdit' });
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getWorkOrderPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
status: MesProWorkOrderStatusEnum.CONFIRMED,
|
||||
type: MesProWorkOrderTypeEnum.SELF,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
treeConfig: {
|
||||
parentField: 'parentId',
|
||||
rowField: 'id',
|
||||
transform: true,
|
||||
expandAll: true,
|
||||
reserve: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MesProWorkOrderApi.WorkOrder>,
|
||||
});
|
||||
|
||||
onMounted(loadGanttPreview);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【生产】生产排产、工序流转卡"
|
||||
url="https://doc.iocoder.cn/mes/pro/schedule-card/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<ScheduleModal @success="handleRefresh" />
|
||||
|
||||
<!-- 排产甘特图预览 -->
|
||||
<ElCard class="mb-4" header="排产甘特图">
|
||||
<GanttChart :height="350" :readonly="true" :tasks="ganttTasks" />
|
||||
</ElCard>
|
||||
|
||||
<Grid table-title="待排产工单">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '甘特图编辑',
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
onClick: handleGanttEdit,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #code="{ row }">
|
||||
<ElButton link type="primary" @click="handleDetail(row)">
|
||||
{{ row.code }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '排产',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
auth: ['mes:pro-task:create'],
|
||||
ifShow: row.status === MesProWorkOrderStatusEnum.CONFIRMED,
|
||||
onClick: handleSchedule.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
<script lang="ts" setup>
|
||||
import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
|
||||
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElButton, ElCard, ElMessage, ElPopconfirm, ElStep, ElSteps } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getRouteProcessListByProduct } from '#/api/mes/pro/route/process';
|
||||
import { finishWorkOrder, getWorkOrder } from '#/api/mes/pro/workorder';
|
||||
|
||||
import { useScheduleFormSchema } from '../data';
|
||||
import TaskList from './task-list.vue';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formType = ref<'detail' | 'schedule'>('schedule');
|
||||
const workOrder = ref<MesProWorkOrderApi.WorkOrder>(); // 当前工单详情
|
||||
const routeProcessList = ref<MesProRouteProcessApi.RouteProcess[]>([]); // 工艺路线工序列表
|
||||
const activeProcessStep = ref(0); // 当前工序步骤索引
|
||||
const currentRouteId = ref(0); // 当前工艺路线编号
|
||||
|
||||
const isReadonly = computed(() => formType.value === 'detail'); // 详情态只读
|
||||
const getTitle = computed(() =>
|
||||
formType.value === 'detail' ? '工单详情' : '生产排产',
|
||||
);
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-1',
|
||||
labelWidth: 100,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useScheduleFormSchema(),
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-3',
|
||||
});
|
||||
|
||||
/** 加载工艺路线工序列表 */
|
||||
async function loadRouteProcesses(productId: number) {
|
||||
const processes = await getRouteProcessListByProduct(productId);
|
||||
if (!processes || processes.length === 0) {
|
||||
ElMessage.warning('当前产品未配置工艺路线,请先在工艺路线中维护');
|
||||
return;
|
||||
}
|
||||
currentRouteId.value = processes[0]!.routeId ?? 0;
|
||||
routeProcessList.value = [...processes].toSorted(
|
||||
(a, b) => (a.sort ?? 0) - (b.sort ?? 0),
|
||||
);
|
||||
}
|
||||
|
||||
/** 完成工单 */
|
||||
async function handleFinish() {
|
||||
if (!workOrder.value?.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
await finishWorkOrder(workOrder.value.id);
|
||||
ElMessage.success('工单已完成');
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
showConfirmButton: false,
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
workOrder.value = undefined;
|
||||
routeProcessList.value = [];
|
||||
activeProcessStep.value = 0;
|
||||
currentRouteId.value = 0;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<{ formType: 'detail' | 'schedule'; id: number }>();
|
||||
formType.value = data.formType;
|
||||
activeProcessStep.value = 0;
|
||||
routeProcessList.value = [];
|
||||
modalApi.lock();
|
||||
try {
|
||||
workOrder.value = await getWorkOrder(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(workOrder.value);
|
||||
if (workOrder.value.productId) {
|
||||
await loadRouteProcesses(workOrder.value.productId);
|
||||
}
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-4/5">
|
||||
<Form class="mx-4" />
|
||||
<!-- 工序步骤导航 + 当前工序任务列表 -->
|
||||
<template v-if="routeProcessList.length > 0 && workOrder?.id">
|
||||
<ElSteps
|
||||
:active="activeProcessStep"
|
||||
align-center
|
||||
class="my-4 px-4"
|
||||
simple
|
||||
>
|
||||
<ElStep
|
||||
v-for="(rp, index) in routeProcessList"
|
||||
:key="rp.processId"
|
||||
class="cursor-pointer"
|
||||
:title="rp.processName"
|
||||
@click="activeProcessStep = index"
|
||||
/>
|
||||
</ElSteps>
|
||||
<ElCard
|
||||
v-for="(rp, index) in routeProcessList"
|
||||
v-show="activeProcessStep === index"
|
||||
:key="rp.processId"
|
||||
class="mx-4"
|
||||
shadow="never"
|
||||
>
|
||||
<TaskList
|
||||
:color-code="rp.colorCode"
|
||||
:disabled="isReadonly"
|
||||
:item-id="workOrder.productId"
|
||||
:process-id="rp.processId!"
|
||||
:route-id="currentRouteId"
|
||||
:work-order-id="workOrder.id!"
|
||||
/>
|
||||
</ElCard>
|
||||
</template>
|
||||
<template #prepend-footer>
|
||||
<div class="flex flex-auto items-center gap-2">
|
||||
<ElPopconfirm
|
||||
v-if="formType === 'schedule'"
|
||||
title="确认要完成该工单吗?完成后工单下所有任务将标记为已完成。"
|
||||
width="320"
|
||||
@confirm="handleFinish"
|
||||
>
|
||||
<template #reference>
|
||||
<ElButton type="primary">完成</ElButton>
|
||||
</template>
|
||||
</ElPopconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
<script lang="ts" setup>
|
||||
import type { MesProTaskApi } from '#/api/mes/pro/task';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createTask, getTask, updateTask } from '#/api/mes/pro/task';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useTaskFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<MesProTaskApi.Task>();
|
||||
const context = ref<{
|
||||
colorCode?: string;
|
||||
itemId?: number;
|
||||
processId?: number;
|
||||
routeId?: number;
|
||||
workOrderId?: number;
|
||||
}>({}); // 新增任务时的工单 / 工序上下文
|
||||
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['生产任务'])
|
||||
: $t('ui.actionTitle.create', ['生产任务']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-1',
|
||||
labelWidth: 100,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [],
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-3',
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as MesProTaskApi.Task;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateTask({ ...formData.value, ...data })
|
||||
: createTask({ ...context.value, ...data }));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
context.value = {};
|
||||
return;
|
||||
}
|
||||
formApi.setState({ schema: useTaskFormSchema(formApi) });
|
||||
// 加载数据
|
||||
const data = modalApi.getData<{
|
||||
colorCode?: string;
|
||||
id?: number;
|
||||
itemId?: number;
|
||||
processId?: number;
|
||||
routeId?: number;
|
||||
workOrderId?: number;
|
||||
}>();
|
||||
if (data?.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getTask(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
return;
|
||||
}
|
||||
context.value = {
|
||||
colorCode: data.colorCode,
|
||||
itemId: data.itemId,
|
||||
processId: data.processId,
|
||||
routeId: data.routeId,
|
||||
workOrderId: data.workOrderId,
|
||||
};
|
||||
await formApi.setValues({
|
||||
colorCode: data.colorCode || '#00AEF3',
|
||||
duration: 1,
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-3/5">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MesProTaskApi } from '#/api/mes/pro/task';
|
||||
|
||||
import { computed, watch } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteTask, getTaskPage } from '#/api/mes/pro/task';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useTaskGridColumns } from '../data';
|
||||
import TaskForm from './task-form.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
colorCode?: string;
|
||||
disabled?: boolean;
|
||||
itemId?: number;
|
||||
processId: number;
|
||||
routeId: number;
|
||||
workOrderId: number;
|
||||
}>();
|
||||
|
||||
const editable = computed(() => !props.disabled); // 是否可维护任务
|
||||
|
||||
const [TaskFormModal, taskFormModalApi] = useVbenModal({
|
||||
connectedComponent: TaskForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 新增任务 */
|
||||
function handleCreate() {
|
||||
taskFormModalApi
|
||||
.setData({
|
||||
colorCode: props.colorCode,
|
||||
itemId: props.itemId,
|
||||
processId: props.processId,
|
||||
routeId: props.routeId,
|
||||
workOrderId: props.workOrderId,
|
||||
})
|
||||
.open();
|
||||
}
|
||||
|
||||
/** 编辑任务 */
|
||||
function handleEdit(row: MesProTaskApi.Task) {
|
||||
taskFormModalApi.setData({ id: row.id }).open();
|
||||
}
|
||||
|
||||
/** 删除任务 */
|
||||
async function handleDelete(row: MesProTaskApi.Task) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.code]),
|
||||
});
|
||||
try {
|
||||
await deleteTask(row.id!);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.code]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useTaskGridColumns(editable.value),
|
||||
height: 360,
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => {
|
||||
if (!props.workOrderId) {
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
return await getTaskPage({
|
||||
pageNo: 1,
|
||||
pageSize: 100,
|
||||
processId: props.processId,
|
||||
routeId: props.routeId,
|
||||
workOrderId: props.workOrderId,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MesProTaskApi.Task>,
|
||||
});
|
||||
|
||||
// 工序步骤切换时重新加载当前工序的任务
|
||||
watch(
|
||||
() => props.processId,
|
||||
() => handleRefresh(),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<TaskFormModal @success="handleRefresh" />
|
||||
<Grid table-title="生产任务">
|
||||
<template v-if="editable" #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '新增任务',
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['mes:pro-task:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #colorCode="{ row }">
|
||||
<div
|
||||
class="mx-auto size-5 rounded"
|
||||
:style="{ background: row.colorCode || '#00AEF3' }"
|
||||
></div>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['mes:pro-task:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['mes:pro-task:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.code]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -53,6 +53,12 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||
placeholder: '请选择库区',
|
||||
warehouseId: values.warehouseId,
|
||||
}),
|
||||
// 仓库切换时清空库区,避免旧库区条件残留
|
||||
trigger: (values, formApi) => {
|
||||
if (values.locationId !== undefined) {
|
||||
void formApi.setFieldValue('locationId', undefined);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -191,6 +197,12 @@ export function useSelectGridFormSchema(): VbenFormSchema[] {
|
|||
placeholder: '请选择库区',
|
||||
warehouseId: values.warehouseId,
|
||||
}),
|
||||
// 仓库切换时清空库区
|
||||
trigger: (values, formApi) => {
|
||||
if (values.locationId !== undefined) {
|
||||
void formApi.setFieldValue('locationId', undefined);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -198,11 +210,17 @@ export function useSelectGridFormSchema(): VbenFormSchema[] {
|
|||
label: '库位',
|
||||
component: markRaw(WmWarehouseAreaSelect),
|
||||
dependencies: {
|
||||
triggerFields: ['locationId'],
|
||||
triggerFields: ['warehouseId', 'locationId'],
|
||||
componentProps: (values) => ({
|
||||
locationId: values.locationId,
|
||||
placeholder: '请选择库位',
|
||||
}),
|
||||
// 仓库或库区切换时清空库位
|
||||
trigger: (values, formApi) => {
|
||||
if (values.areaId !== undefined) {
|
||||
void formApi.setFieldValue('areaId', undefined);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
Loading…
Reference in New Issue