项目管理,甘特图增加保存草稿,同步工序计划和生产订单

This commit is contained in:
tzy 2026-06-01 17:00:06 +08:00
parent 8794a8cfc6
commit 0e65eb52e1
3 changed files with 187 additions and 27 deletions

View File

@ -135,8 +135,18 @@ export const orderProjectApi = {
return request({ url: `/biz/orderProject/template`, method: 'get', responseType: 'blob' })
},
// 查询金蝶生产订单
// 查询金蝶生产订单 (支持优先读取本地暂存)
getKingdeeOrder(id: number) {
return request<KingdeePrdMo[]>({ url: `/biz/orderProject/kingdee/order/${id}`, method: 'get' })
},
// 暂存金蝶生产订单数据(草稿)
saveKingdeeOrderDraft(id: number, data: KingdeePrdMo[]) {
return request({ url: `/biz/orderProject/kingdee/order/${id}/draft`, method: 'post', data })
},
// 同步订单和计划表
saveKingdeeOrderAndPlan(id: number, data: KingdeePrdMo[]) {
return request({ url: `/biz/orderProject/kingdee/order/${id}/saveOrderAndPlan`, method: 'post', data })
},
}

View File

@ -3,7 +3,10 @@
<div class="gantt-header">
<n-space justify="space-between" align="center">
<n-h2 style="margin: 0">甘特图排产</n-h2>
<n-button @click="handleBack">返回</n-button>
<n-space>
<n-button type="primary" :loading="saving" @click="handleSaveDraft">保存排产草稿</n-button>
<n-button @click="handleBack">返回</n-button>
</n-space>
</n-space>
</div>
<div class="gantt-container" ref="ganttContainer"></div>
@ -16,7 +19,7 @@ import { useMessage } from 'naive-ui'
import { useRouter } from 'vue-router'
import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
import { orderProjectApi } from '@/api/orderProject'
import { orderProjectApi, type KingdeePrdMo } from '@/api/orderProject'
const router = useRouter()
const ganttContainer = ref<HTMLElement | null>(null)
@ -24,11 +27,39 @@ const message = useMessage()
let ganttEventIds: string[] = []
const tableData = ref<any[]>([])
const projectId = ref<number | null>(null)
const saving = ref(false)
function handleBack() {
router.back()
}
async function handleSaveDraft() {
if (!projectId.value) {
message.warning('缺少项目ID无法保存')
return
}
saving.value = true
try {
// processRoutes children
const draftData = tableData.value.map(mo => {
const { processRoutes, ...rest } = mo
return { ...rest, children: processRoutes } as KingdeePrdMo
})
await orderProjectApi.saveKingdeeOrderDraft(projectId.value, draftData)
message.success('排产草稿保存成功')
// session
sessionStorage.setItem('gantt_schedule_data', JSON.stringify(tableData.value))
} catch (error) {
//
} finally {
saving.value = false
}
}
onMounted(async () => {
try {
@ -36,6 +67,12 @@ onMounted(async () => {
const stateStr = sessionStorage.getItem('gantt_schedule_data')
const idStr = sessionStorage.getItem('gantt_project_id')
if (idStr) {
projectId.value = Number(idStr)
}
if (stateStr) {
const parsedData = JSON.parse(stateStr)
tableData.value = parsedData
@ -179,24 +216,49 @@ function renderGanttData() {
ganttEventIds.push(evDragId)
//
const evId = gantt.attachEvent("onAfterTaskDrag", (id, mode, e) => {
const task = gantt.getTask(id)
if (mode === 'move' || mode === 'resize') {
const start = gantt.date.date_to_str(gantt.config.date_format)(task.start_date)
const end = gantt.date.date_to_str(gantt.config.date_format)(task.end_date)
message.success(`任务 [${task.text}] 计划时间已更新: ${start} ~ ${end}`)
//
if (task._type === 'mo') {
task._raw.planStartTime = start
task._raw.planFinishTime = end
} else if (task._type === 'route') {
task._raw.planStartTime = start
task._raw.planFinishTime = end
const evId = gantt.attachEvent("onAfterTaskDrag", (id, mode, e) => {
const task = gantt.getTask(id)
if (mode === 'move' || mode === 'resize') {
const start = gantt.date.date_to_str(gantt.config.date_format)(task.start_date)
const end = gantt.date.date_to_str(gantt.config.date_format)(task.end_date)
//
if (task._type === 'mo') {
task._raw.planStartTime = start
task._raw.planFinishTime = end
} else if (task._type === 'route') {
task._raw.planStartTime = start
task._raw.planFinishTime = end
//
const parentId = task.parent
if (parentId) {
const parentTask = gantt.getTask(parentId)
if (parentTask) {
const children = gantt.getChildren(parentId)
let minDate: Date | null = null
let maxDate: Date | null = null
children.forEach((childId: string | number) => {
const childTask = gantt.getTask(childId)
if (!minDate || childTask.start_date < minDate) minDate = childTask.start_date
if (!maxDate || childTask.end_date > maxDate) maxDate = childTask.end_date
})
if (minDate && maxDate) {
parentTask.start_date = new Date(minDate)
parentTask.end_date = new Date(maxDate)
parentTask._raw.planStartTime = gantt.date.date_to_str(gantt.config.date_format)(minDate)
parentTask._raw.planFinishTime = gantt.date.date_to_str(gantt.config.date_format)(maxDate)
gantt.updateTask(parentId)
}
}
}
}
message.success(`任务 [${task.text}] 计划时间已更新`)
}
}
})
})
ganttEventIds.push(evId)
}

View File

@ -180,7 +180,15 @@
size="small"
/>
<template #footer>
<n-button @click="kingdeeModalVisible = false">关闭</n-button>
<n-space justify="end">
<n-button type="info" :disabled="kingdeeLoading" @click="handleSyncOrderAndPlan">
同步订单计划
</n-button>
<n-button type="primary" :disabled="kingdeeLoading" @click="handleSaveDraft">
保存草稿
</n-button>
<n-button @click="kingdeeModalVisible = false">关闭</n-button>
</n-space>
</template>
</n-modal>
@ -218,21 +226,30 @@
</template>
<script setup lang="ts">
import { ref, reactive, h, computed, onMounted } from 'vue'
import { ref, reactive, h, computed, onMounted, watch } from 'vue'
import { NButton, NSpace, NIcon, NTag, NDatePicker, NDataTable, NUpload, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions } from 'naive-ui'
import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline, EyeOutline, CalendarOutline } from '@vicons/ionicons5'
import { orderProjectApi, type OrderProject, type KingdeePrdMo, type KingdeeProcessRoute } from '@/api/orderProject'
import { dictDataApi } from '@/api/org'
import { useUserStore } from '@/stores/user'
import { useRouter } from 'vue-router'
import { useRouter, useRoute } from 'vue-router'
const message = useMessage()
const dialog = useDialog()
const userStore = useUserStore()
const router = useRouter()
const route = useRoute()
const hasPermission = (permission: string) => userStore.hasPermission(permission)
watch(() => route.path, (newPath, oldPath) => {
if (newPath === '/biz/orderProject' && oldPath === '/biz/orderProject/gantt') {
if (kingdeeModalVisible.value && kingdeeCurrentProjectId.value) {
handleKingdeeQuery({ id: kingdeeCurrentProjectId.value } as OrderProject)
}
}
})
//
const searchForm = reactive({
id: null as number | null,
@ -264,6 +281,7 @@ type KingdeeMoTableRow = Omit<KingdeePrdMo, 'children'> & { processRoutes: Kingd
const kingdeeTableData = ref<KingdeeMoTableRow[]>([])
const kingdeeExpandedKeys = ref<Array<string | number>>([])
const kingdeeCurrentProjectId = ref<number | null>(null) // ID
const kingdeeTableMaxHeight = 'min(78vh, 720px)'
const kingdeeMoScrollX = 1820
const kingdeeProcessScrollX = 1360
@ -316,7 +334,20 @@ function renderKingdeePlanPicker(row: KingdeeProcessRoute, field: 'planStartTime
clearable: true,
size: 'small',
style: { width: '100%' },
'onUpdate:value': (val: number | null) => {
onUpdateValue: (val: number | null) => {
row[field] = val != null ? formatDateTime(val) : null
},
})
}
function renderKingdeePlanPickerMo(row: KingdeeMoTableRow, field: 'planStartTime' | 'planFinishTime') {
return h(NDatePicker, {
value: parseKingdeePlanTime(row[field]),
type: 'datetime',
clearable: true,
size: 'small',
style: { width: '100%' },
onUpdateValue: (val: number | null) => {
row[field] = val != null ? formatDateTime(val) : null
},
})
@ -469,8 +500,18 @@ const kingdeeMoColumns: DataTableColumns<KingdeeMoTableRow> = [
{ title: '生产车间', key: 'workShopName', width: 100, align: 'center', ellipsis: { tooltip: true } },
{ title: '规格型号', key: 'specModel', width: 110, align: 'center', ellipsis: { tooltip: true } },
{ title: '单位', key: 'unit', width: 60, align: 'center' },
{ title: '计划开工', key: 'planStartTime', width: 165, align: 'center' },
{ title: '计划完工', key: 'planFinishTime', width: 165, align: 'center' },
{
title: '计划开工', key: 'planStartTime', width: 180, align: 'center',
render(row) {
return renderKingdeePlanPickerMo(row, 'planStartTime')
}
},
{
title: '计划完工', key: 'planFinishTime', width: 180, align: 'center',
render(row) {
return renderKingdeePlanPickerMo(row, 'planFinishTime')
}
},
{
title: '领料状态', key: 'pickMtrlStatus', width: 90, align: 'center',
render(row) {
@ -641,8 +682,11 @@ async function handleSubmit() {
//
function handleGanttSchedule() {
// sessionStorage
// ID sessionStorage使
sessionStorage.setItem('gantt_schedule_data', JSON.stringify(kingdeeTableData.value))
if (kingdeeCurrentProjectId.value) {
sessionStorage.setItem('gantt_project_id', String(kingdeeCurrentProjectId.value))
}
//
router.push('/biz/orderProject/gantt')
}
@ -653,6 +697,7 @@ async function handleKingdeeQuery(row: OrderProject) {
message.warning('缺少项目ID无法查询')
return
}
kingdeeCurrentProjectId.value = row.id
kingdeeModalTitle.value = `金蝶生产订单${row.code ? ` - ${row.code}` : ''}`
kingdeeModalVisible.value = true
kingdeeLoading.value = true
@ -677,6 +722,49 @@ async function handleKingdeeQuery(row: OrderProject) {
}
}
// 稿
async function handleSaveDraft() {
if (!kingdeeCurrentProjectId.value) return
kingdeeLoading.value = true
try {
// processRoutes children
const draftData = kingdeeTableData.value.map(mo => {
const { processRoutes, ...rest } = mo
return { ...rest, children: processRoutes } as KingdeePrdMo
})
await orderProjectApi.saveKingdeeOrderDraft(kingdeeCurrentProjectId.value, draftData)
message.success('草稿保存成功')
} catch (error) {
//
} finally {
kingdeeLoading.value = false
}
}
//
async function handleSyncOrderAndPlan() {
if (!kingdeeCurrentProjectId.value) return
kingdeeLoading.value = true
try {
// processRoutes children
const draftData = kingdeeTableData.value.map(mo => {
const { processRoutes, ...rest } = mo
return { ...rest, children: processRoutes } as KingdeePrdMo
})
await orderProjectApi.saveKingdeeOrderAndPlan(kingdeeCurrentProjectId.value, draftData)
message.success('同步订单计划成功')
kingdeeModalVisible.value = false
} catch (error) {
//
} finally {
kingdeeLoading.value = false
}
}
//
function handleDelete(row: OrderProject) {
dialog.warning({