项目管理部分甘特图

This commit is contained in:
tzy 2026-06-01 13:59:37 +08:00
parent 5fae83d994
commit 8794a8cfc6
4 changed files with 194 additions and 87 deletions

28
package-lock.json generated
View File

@ -12,6 +12,7 @@
"@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^6.0.0",
"axios": "^1.6.5",
"dhtmlx-gantt": "^9.1.4",
"disable-devtool": "^0.3.9",
"echarts": "^6.0.0",
"jsencrypt": "^3.3.2",
@ -19,6 +20,7 @@
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"vue": "^3.4.15",
"vue-draggable-plus": "^0.6.1",
"vue-router": "^4.2.5"
},
"devDependencies": {
@ -1201,6 +1203,11 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@types/sortablejs": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.9.tgz",
"integrity": "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ=="
},
"node_modules/@vicons/ionicons5": {
"version": "0.12.0",
"resolved": "https://registry.npmmirror.com/@vicons/ionicons5/-/ionicons5-0.12.0.tgz",
@ -1555,6 +1562,11 @@
"node": ">=8"
}
},
"node_modules/dhtmlx-gantt": {
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/dhtmlx-gantt/-/dhtmlx-gantt-9.1.4.tgz",
"integrity": "sha512-XCNA5QUiuV79Xq1ykNpH9LFNR2IVpDZMqnmBV6dsBeOkHyPMOpkyQ/gqAPCcK2GAvYHoN2nGAMYb2LldCWhMuQ=="
},
"node_modules/disable-devtool": {
"version": "0.3.9",
"resolved": "https://registry.npmmirror.com/disable-devtool/-/disable-devtool-0.3.9.tgz",
@ -2392,6 +2404,22 @@
}
}
},
"node_modules/vue-draggable-plus": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/vue-draggable-plus/-/vue-draggable-plus-0.6.1.tgz",
"integrity": "sha512-FbtQ/fuoixiOfTZzG3yoPl4JAo9HJXRHmBQZFB9x2NYCh6pq0TomHf7g5MUmpaDYv+LU2n6BPq2YN9sBO+FbIg==",
"dependencies": {
"@types/sortablejs": "^1.15.8"
},
"peerDependencies": {
"@types/sortablejs": "^1.15.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/vue-router": {
"version": "4.6.4",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz",

View File

@ -116,6 +116,19 @@ const routes: RouteRecordRaw[] = [
component: () => import('@/views/org/post/index.vue'),
meta: { title: '岗位管理', icon: 'IdCardOutline' }
},
// 项目管理
{
path: 'biz/orderProject',
name: 'OrderProject',
component: () => import('@/views/biz/orderProject/index.vue'),
meta: { title: '项目管理', icon: 'PersonOutline' }
},
{
path: 'biz/orderProject/gantt',
name: 'OrderProjectGantt',
component: () => import('@/views/biz/orderProject/components/GanttSchedule.vue'),
meta: { title: '甘特图排产', icon: 'CalendarOutline', activeMenu: '/biz/orderProject' }
},
//运营管理
{
path: 'biz/product',

View File

@ -1,57 +1,66 @@
<template>
<n-modal
v-model:show="visible"
preset="card"
title="甘特图排产"
class="gantt-modal"
:style="{ width: '96vw', maxWidth: '1920px' }"
:content-style="{ padding: '16px 24px', minHeight: '72vh', display: 'flex', flexDirection: 'column' }"
:mask-closable="false"
>
<div class="gantt-schedule-page">
<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>
</div>
<div class="gantt-container" ref="ganttContainer"></div>
</n-modal>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onUnmounted, nextTick } from 'vue'
import { ref, onMounted, onUnmounted, toRaw } from 'vue'
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'
const props = defineProps<{
show: boolean
data: any[] //
}>()
const emit = defineEmits<{
(e: 'update:show', val: boolean): void
(e: 'update-time', payload: { id: string; type: string; item: any; start: string; end: string }): void
}>()
const visible = ref(props.show)
watch(() => props.show, (val) => {
visible.value = val
if (val) {
nextTick(() => {
initGantt()
})
}
})
watch(visible, (val) => {
emit('update:show', val)
})
const router = useRouter()
const ganttContainer = ref<HTMLElement | null>(null)
const message = useMessage()
let ganttEventIds: string[] = []
const tableData = ref<any[]>([])
function handleBack() {
router.back()
}
onMounted(async () => {
try {
initGantt()
const stateStr = sessionStorage.getItem('gantt_schedule_data')
if (stateStr) {
const parsedData = JSON.parse(stateStr)
tableData.value = parsedData
renderGanttData()
} else {
message.warning('未获取到排产数据')
}
} catch (error) {
message.error('加载排产数据失败')
}
})
function formatGanttDate(date: Date) {
const pad = (n: number) => String(n).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
}
function initGantt() {
if (!ganttContainer.value) return
//
ganttEventIds.forEach(id => gantt.detachEvent(id))
ganttEventIds = []
gantt.clearAll()
//
gantt.config.date_format = "%Y-%m-%d %H:%i:%s"
@ -61,36 +70,71 @@ function initGantt() {
{ unit: "day", step: 1, format: "%d日" }
]
gantt.config.columns = [
{ name: "text", label: "工单 / 工序", tree: true, width: 220, resize: true },
{ name: "start_date", label: "计划开始", align: "center", width: 120 },
{ name: "end_date", label: "计划结束", align: "center", width: 120 },
{ name: "text", label: "单号 / 序号", tree: true, width: 200, resize: true },
{ name: "materialCode", label: "物料编码", align: "center", width: 140, resize: true },
{ name: "name", label: "名称", align: "center", width: 160, resize: true },
{ name: "start_date", label: "计划开始", align: "center", width: 130 },
{ name: "end_date", label: "计划结束", align: "center", width: 130 },
]
//
gantt.config.drag_progress = false //
gantt.config.drag_links = false // 线
gantt.config.work_time = true //
gantt.config.work_time = true //
// gantt.config.auto_scheduling
gantt.i18n.setLocale("cn")
// marker
gantt.plugins({
marker: true
})
//
gantt.templates.task_text = function(start, end, task) {
if (task._type === 'route') {
return task.name; //
}
return task.text;
};
gantt.init(ganttContainer.value)
//
const today = new Date()
gantt.addMarker({
start_date: today,
css: "today-marker",
text: "今日",
title: "今日: " + formatGanttDate(today)
})
}
function renderGanttData() {
gantt.clearAll()
//
const tasks: any[] = []
props.data.forEach((mo) => {
const moId = `mo_${mo.productionOrderNo}_${mo.billNo}`
const rawData = toRaw(tableData.value) || []
const processColors = ['#3498db', '#1abc9c', '#9b59b6', '#e67e22', '#e74c3c', '#2ecc71', '#f1c40f', '#34495e', '#7f8c8d']
rawData.forEach((mo: any, moIndex: number) => {
const moId = `mo_${moIndex}_${mo.productionOrderNo}_${mo.billNo}`
// 使
let minDate: Date | null = mo.planStartTime ? new Date(mo.planStartTime.replace(' ', 'T')) : null
let maxDate: Date | null = mo.planFinishTime ? new Date(mo.planFinishTime.replace(' ', 'T')) : null
if (!minDate || isNaN(minDate.getTime())) minDate = new Date()
if (!maxDate || isNaN(maxDate.getTime())) maxDate = new Date(minDate.getTime() + 86400000)
if (maxDate <= minDate) maxDate = new Date(minDate.getTime() + 86400000)
tasks.push({
id: moId,
text: `${mo.materialName || '产品'} (${mo.productionOrderNo})`,
start_date: minDate || new Date(),
end_date: maxDate || new Date(Date.now() + 86400000),
open: true,
text: mo.productionOrderNo || mo.billNo || '未知单号',
materialCode: mo.materialCode || '',
name: mo.materialName || '产品',
start_date: formatGanttDate(minDate),
end_date: formatGanttDate(maxDate),
open: false, //
type: gantt.config.types.project,
_type: 'mo',
_raw: mo
@ -98,15 +142,22 @@ function initGantt() {
if (mo.processRoutes && mo.processRoutes.length > 0) {
mo.processRoutes.forEach((route: any, index: number) => {
const routeId = `route_${mo.productionOrderNo}_${route.operNumber}_${index}`
const rStart = route.planStartTime ? new Date(route.planStartTime.replace(' ', 'T')) : (minDate || new Date())
const rEnd = route.planFinishTime ? new Date(route.planFinishTime.replace(' ', 'T')) : (maxDate || new Date(Date.now() + 86400000))
const routeId = `route_${moId}_${index}`
let rStart = route.planStartTime ? new Date(route.planStartTime.replace(' ', 'T')) : null
let rEnd = route.planFinishTime ? new Date(route.planFinishTime.replace(' ', 'T')) : null
if (!rStart || isNaN(rStart.getTime())) rStart = minDate
if (!rEnd || isNaN(rEnd.getTime())) rEnd = maxDate
if (rEnd <= rStart) rEnd = new Date(rStart!.getTime() + 3600000) // 1
tasks.push({
id: routeId,
text: `${route.operDescription || route.processProperty} (${route.operNumber})`,
start_date: rStart,
end_date: rEnd,
text: route.operNumber != null ? String(route.operNumber) : String(index + 1),
materialCode: '',
name: route.processProperty || '工序',
color: processColors[index % processColors.length],
start_date: formatGanttDate(rStart!),
end_date: formatGanttDate(rEnd),
parent: moId,
_type: 'route',
_raw: route
@ -115,9 +166,18 @@ function initGantt() {
}
})
gantt.clearAll()
gantt.parse({ data: tasks, links: [] })
//
const evDragId = gantt.attachEvent("onBeforeTaskDrag", (id, mode, e) => {
const task = gantt.getTask(id)
if (task.type === gantt.config.types.project) {
return false
}
return true
})
ganttEventIds.push(evDragId)
//
const evId = gantt.attachEvent("onAfterTaskDrag", (id, mode, e) => {
const task = gantt.getTask(id)
@ -127,14 +187,14 @@ function initGantt() {
message.success(`任务 [${task.text}] 计划时间已更新: ${start} ~ ${end}`)
//
emit('update-time', {
id: id as string,
type: task._type,
item: task._raw,
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
}
}
})
ganttEventIds.push(evId)
@ -147,16 +207,37 @@ onUnmounted(() => {
</script>
<style scoped>
.gantt-modal :deep(.n-card__content) {
.gantt-schedule-page {
display: flex;
flex-direction: column;
height: calc(100vh - 120px);
background-color: var(--n-color);
padding: 16px;
border-radius: 8px;
box-sizing: border-box;
}
.gantt-header {
margin-bottom: 16px;
flex-shrink: 0;
}
.gantt-container {
flex: 1;
width: 100%;
height: 100%;
min-height: 500px;
min-height: 0; /* 必须设置min-height以允许flex子元素内部滚动 */
border: 1px solid var(--n-border-color);
border-radius: 4px;
overflow: hidden;
}
:deep(.today-marker) {
background-color: #ff5252;
}
:deep(.today-marker .gantt_marker_content) {
background-color: #ff5252;
color: white;
border-radius: 2px;
padding: 0 4px;
}
</style>

View File

@ -214,13 +214,6 @@
<n-button @click="importModalVisible = false">关闭</n-button>
</template>
</n-modal>
<!-- 甘特图排产组件 -->
<GanttSchedule
v-model:show="ganttModalVisible"
:data="kingdeeTableData"
@update-time="handleGanttTimeUpdate"
/>
</div>
</template>
@ -231,11 +224,13 @@ import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline,
import { orderProjectApi, type OrderProject, type KingdeePrdMo, type KingdeeProcessRoute } from '@/api/orderProject'
import { dictDataApi } from '@/api/org'
import { useUserStore } from '@/stores/user'
import GanttSchedule from './components/GanttSchedule.vue'
import { useRouter } from 'vue-router'
const message = useMessage()
const dialog = useDialog()
const userStore = useUserStore()
const router = useRouter()
const hasPermission = (permission: string) => userStore.hasPermission(permission)
//
@ -264,7 +259,6 @@ const importModalVisible = ref(false)
const kingdeeModalVisible = ref(false)
const kingdeeModalTitle = ref('金蝶生产订单')
const kingdeeLoading = ref(false)
const ganttModalVisible = ref(false)
/** 主表行:工序仅存 processRoutes避免 children 触发树形重复行 */
type KingdeeMoTableRow = Omit<KingdeePrdMo, 'children'> & { processRoutes: KingdeeProcessRoute[] }
@ -647,19 +641,10 @@ async function handleSubmit() {
//
function handleGanttSchedule() {
ganttModalVisible.value = true
}
//
function handleGanttTimeUpdate(payload: { id: string; type: string; item: any; start: string; end: string }) {
const { type, item, start, end } = payload
if (type === 'mo') {
item.planStartTime = start
item.planFinishTime = end
} else if (type === 'route') {
item.planStartTime = start
item.planFinishTime = end
}
// sessionStorage
sessionStorage.setItem('gantt_schedule_data', JSON.stringify(kingdeeTableData.value))
//
router.push('/biz/orderProject/gantt')
}
//