feat:排产-甘特图添加锁定、撤回操作按钮

main
黄伟杰 6 days ago
parent fc6f348ad6
commit 8c3f7a5aec

@ -4623,7 +4623,18 @@ export default {
statusStarted: 'Started', statusStarted: 'Started',
statusPaused: 'Paused', statusPaused: 'Paused',
statusPendingStorage: 'Pending Storage', statusPendingStorage: 'Pending Storage',
statusStored: 'Stored' statusStored: 'Stored',
dialogTitle: 'Schedule Gantt Preview',
workerLabel: 'Worker',
workerPlaceholder: 'Please select worker',
calcLossLabel: 'Calculate Loss',
lockBtn: 'Lock',
unlockBtn: 'Unlock',
undoBtn: 'Undo',
buttonSave: 'Save',
buttonClose: 'Close',
warningNoPlanData: 'No plan data to save',
saveSuccess: 'Schedule saved successfully'
} }
} }
} }

@ -4834,7 +4834,18 @@ export default {
statusStarted: '已开工', statusStarted: '已开工',
statusPaused: '暂停', statusPaused: '暂停',
statusPendingStorage: '待入库', statusPendingStorage: '待入库',
statusStored: '已入库' statusStored: '已入库',
dialogTitle: '排产甘特图预览',
workerLabel: '领料人',
workerPlaceholder: '请选择领料人',
calcLossLabel: '是否计算损耗',
lockBtn: '锁定',
unlockBtn: '解锁',
undoBtn: '撤回',
buttonSave: '保存',
buttonClose: '关闭',
warningNoPlanData: '暂无可保存的计划数据',
saveSuccess: '排产计划保存成功'
} }
} }
} }

@ -90,9 +90,11 @@ const props = withDefaults(
defineProps<{ defineProps<{
scheduleList: any[] scheduleList: any[]
height?: string height?: string
editable?: boolean
}>(), }>(),
{ {
height: '800px' height: '800px',
editable: false
} }
) )
@ -109,6 +111,18 @@ const taskAdjustForm = reactive({
endDate: '' endDate: ''
}) })
const editingPlanIdentity = ref<string | null>(null) const editingPlanIdentity = ref<string | null>(null)
const undoStack = ref<string[]>([])
const saveUndoSnapshot = () => {
const data = previewScheduleList.value.map((device: any) => ({
deviceId: device.deviceId,
deviceName: device.deviceName,
capacityType: device.capacityType,
ratedCapacity: device.ratedCapacity,
plans: (device.plans ?? []).map((plan: any) => ({ ...plan }))
}))
undoStack.value.push(JSON.stringify(data))
}
const getCapacityTypeLabel = (value: any) => { const getCapacityTypeLabel = (value: any) => {
if (value === undefined || value === null) return '-' if (value === undefined || value === null) return '-'
@ -288,7 +302,7 @@ const renderCustomPlanBars = () => {
height: ${barHeight}px; height: ${barHeight}px;
border-radius: 6px; border-radius: 6px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); box-shadow: 0 2px 4px rgba(0,0,0,0.1);
cursor: ${isCurrent ? 'grab' : 'pointer'}; cursor: ${isCurrent && props.editable ? 'grab' : 'pointer'};
z-index: 2; z-index: 2;
display: flex; display: flex;
align-items: center; align-items: center;
@ -352,12 +366,12 @@ const renderCustomPlanBars = () => {
}) })
bar.addEventListener('contextmenu', (e) => { bar.addEventListener('contextmenu', (e) => {
if (!isCurrent) { e.preventDefault(); return } if (!isCurrent || !props.editable) { e.preventDefault(); return }
e.preventDefault() e.preventDefault()
openTaskAdjustDialogForPlan(plan, task._deviceData) openTaskAdjustDialogForPlan(plan, task._deviceData)
}) })
if (isCurrent) { if (isCurrent && props.editable) {
attachPlanBarDrag(bar, plan, task) attachPlanBarDrag(bar, plan, task)
attachPlanBarResize(bar, plan, task) attachPlanBarResize(bar, plan, task)
} }
@ -420,6 +434,8 @@ const attachPlanBarDrag = (bar: HTMLElement, plan: any, task: any) => {
if (!moved) return if (!moved) return
moved = false moved = false
saveUndoSnapshot()
const dx = e.clientX - startX const dx = e.clientX - startX
const newLeft = Math.max(0, startLeft + dx) const newLeft = Math.max(0, startLeft + dx)
const newStartDate = gantt.dateFromPos(newLeft) const newStartDate = gantt.dateFromPos(newLeft)
@ -524,6 +540,8 @@ const attachPlanBarResize = (bar: HTMLElement, plan: any, task: any) => {
resizing = false resizing = false
bar.style.zIndex = '2' bar.style.zIndex = '2'
saveUndoSnapshot()
const newLeft = parseFloat(bar.style.left) || startLeft const newLeft = parseFloat(bar.style.left) || startLeft
const newWidth = parseFloat(bar.style.width) || startWidth const newWidth = parseFloat(bar.style.width) || startWidth
const newStartDate = gantt.dateFromPos(newLeft) const newStartDate = gantt.dateFromPos(newLeft)
@ -562,6 +580,8 @@ const handleTaskAdjustSubmit = () => {
} }
if (!editingPlanIdentity.value) return if (!editingPlanIdentity.value) return
saveUndoSnapshot()
const targetDevice = previewScheduleList.value.find( const targetDevice = previewScheduleList.value.find(
(d: any) => `device-${d.deviceId}` === taskAdjustForm.deviceTaskId (d: any) => `device-${d.deviceId}` === taskAdjustForm.deviceTaskId
) )
@ -719,6 +739,7 @@ const initGanttPreview = () => {
} }
onMounted(async () => { onMounted(async () => {
undoStack.value = []
await nextTick() await nextTick()
initGanttPreview() initGanttPreview()
}) })
@ -737,6 +758,30 @@ watch(
onBeforeUnmount(() => { onBeforeUnmount(() => {
destroyGantt() destroyGantt()
}) })
const undo = () => {
if (!undoStack.value.length) return
const saved = JSON.parse(undoStack.value.pop()!)
for (const savedDevice of saved) {
const originalDevice = previewScheduleList.value.find(
(d: any) => d.deviceId === savedDevice.deviceId
)
if (originalDevice) {
originalDevice.plans.splice(0, originalDevice.plans.length, ...savedDevice.plans)
}
}
initGanttPreview()
}
watch(
() => props.editable,
async () => {
await nextTick()
renderCustomPlanBars()
}
)
defineExpose({ undo })
</script> </script>
<style scoped> <style scoped>

@ -1,21 +1,33 @@
<template> <template>
<Dialog v-model="previewVisible" title="排产甘特图预览" width="100%" align-center> <Dialog v-model="previewVisible" :title="t('GanttChart.GanttPanel.dialogTitle')" width="100%" align-center>
<div class="preview-options"> <div class="preview-options">
<el-form :inline="true"> <el-form :inline="true">
<el-form-item label="领料人"> <el-form-item :label="t('GanttChart.GanttPanel.workerLabel')">
<el-select v-model="scheduleOptions.workerId" clearable filterable placeholder="请选择领料人" style="width: 200px"> <el-select v-model="scheduleOptions.workerId" clearable filterable :placeholder="t('GanttChart.GanttPanel.workerPlaceholder')" style="width: 200px">
<el-option v-for="item in workerList" :key="item.id" :label="item.nickname" :value="item.id" /> <el-option v-for="item in workerList" :key="item.id" :label="item.nickname" :value="item.id" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="是否计算损耗"> <el-form-item :label="t('GanttChart.GanttPanel.calcLossLabel')">
<el-switch v-model="scheduleOptions.isCalculateLoss" /> <el-switch v-model="scheduleOptions.isCalculateLoss" />
</el-form-item> </el-form-item>
<el-form-item>
<el-button :type="ganttEditable ? 'primary' : 'default'" @click="toggleGanttEditable">
<Icon :icon="ganttEditable ? 'ep:unlock' : 'ep:lock'" class="mr-4px" />
{{ ganttEditable ? t('GanttChart.GanttPanel.unlockBtn') : t('GanttChart.GanttPanel.lockBtn') }}
</el-button>
</el-form-item>
<el-form-item>
<el-button :disabled="!ganttEditable" @click="handleUndo">
<Icon icon="ep:refresh-left" class="mr-4px" />
{{ t('GanttChart.GanttPanel.undoBtn') }}
</el-button>
</el-form-item>
</el-form> </el-form>
</div> </div>
<ScheduleGanttPanelEditable :schedule-list="previewScheduleList" height="800px" /> <ScheduleGanttPanelEditable ref="ganttPanelRef" :schedule-list="previewScheduleList" :editable="ganttEditable" height="800px" />
<template #footer> <template #footer>
<el-button type="primary" :loading="previewSaveLoading" @click="handlePreviewSave"></el-button> <el-button type="primary" :loading="previewSaveLoading" @click="handlePreviewSave">{{ t('GanttChart.GanttPanel.buttonSave') }}</el-button>
<el-button @click="previewVisible = false">关闭</el-button> <el-button @click="previewVisible = false">{{ t('GanttChart.GanttPanel.buttonClose') }}</el-button>
</template> </template>
</Dialog> </Dialog>
</template> </template>
@ -25,9 +37,12 @@ import { PlanApi } from '@/api/mes/plan'
import * as UserApi from '@/api/system/user' import * as UserApi from '@/api/system/user'
import ScheduleGanttPanelEditable from './ScheduleGanttPanelEditable.vue' import ScheduleGanttPanelEditable from './ScheduleGanttPanelEditable.vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useI18n } from '@/hooks/web/useI18n'
defineOptions({ name: 'TaskSchedulePreviewDialog' }) defineOptions({ name: 'TaskSchedulePreviewDialog' })
const { t } = useI18n()
const props = defineProps<{ const props = defineProps<{
modelValue: boolean modelValue: boolean
scheduleList: any[] scheduleList: any[]
@ -40,6 +55,16 @@ const emit = defineEmits<{
const message = useMessage() const message = useMessage()
const previewSaveLoading = ref(false) const previewSaveLoading = ref(false)
const ganttEditable = ref(false)
const ganttPanelRef = ref<InstanceType<typeof ScheduleGanttPanelEditable>>()
const toggleGanttEditable = () => {
ganttEditable.value = !ganttEditable.value
}
const handleUndo = () => {
ganttPanelRef.value?.undo()
}
const workerList = ref<UserApi.UserVO[]>([]) const workerList = ref<UserApi.UserVO[]>([])
const scheduleOptions = reactive({ const scheduleOptions = reactive({
@ -61,8 +86,11 @@ const previewVisible = computed({
}) })
watch(previewVisible, (visible) => { watch(previewVisible, (visible) => {
if (visible && workerList.value.length === 0) { if (visible) {
loadWorkerList() if (workerList.value.length === 0) {
loadWorkerList()
}
ganttEditable.value = false
} }
}) })
@ -100,14 +128,14 @@ const handlePreviewSave = async () => {
}) })
if (!createReqVOList.length) { if (!createReqVOList.length) {
message.warning('暂无可保存的计划数据') message.warning(t('GanttChart.GanttPanel.warningNoPlanData'))
return return
} }
previewSaveLoading.value = true previewSaveLoading.value = true
try { try {
await PlanApi.createBatch({ createReqVOList }) await PlanApi.createBatch({ createReqVOList })
message.success('排产计划保存成功') message.success(t('GanttChart.GanttPanel.saveSuccess'))
previewVisible.value = false previewVisible.value = false
emit('saved') emit('saved')
} finally { } finally {

Loading…
Cancel
Save