项目管理部分甘特图
This commit is contained in:
parent
5fae83d994
commit
8794a8cfc6
28
package-lock.json
generated
28
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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>
|
||||
@ -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')
|
||||
}
|
||||
|
||||
// 金蝶预生产查询
|
||||
|
||||
Loading…
Reference in New Issue
Block a user