提交
This commit is contained in:
commit
f29e30aadb
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",
|
||||
|
||||
@ -13,6 +13,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",
|
||||
|
||||
142
src/api/orderProject.ts
Normal file
142
src/api/orderProject.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
// 项目表 类型定义
|
||||
export interface OrderProject {
|
||||
id?: number
|
||||
|
||||
name?: string
|
||||
|
||||
code?: string
|
||||
|
||||
mainCode?: string
|
||||
|
||||
mainName?: string
|
||||
|
||||
quantity?: string
|
||||
|
||||
source?: number
|
||||
|
||||
prodType?: number
|
||||
|
||||
sourceId?: string
|
||||
|
||||
bomVersion?: string
|
||||
|
||||
specModel?: string
|
||||
|
||||
unit?: string
|
||||
|
||||
customerName?: string
|
||||
|
||||
version?: string
|
||||
|
||||
salesDept?: string
|
||||
|
||||
parentId?: number
|
||||
|
||||
deliveryTime?: string
|
||||
|
||||
status?: number
|
||||
|
||||
planFinishTime?: string
|
||||
|
||||
createTime?: string
|
||||
|
||||
createBy?: string
|
||||
|
||||
changeTime?: string
|
||||
|
||||
changeBy?: string
|
||||
|
||||
}
|
||||
|
||||
/** 金蝶生产订单(树父节点) */
|
||||
export interface KingdeePrdMo {
|
||||
productionOrderNo: string
|
||||
billNo: string
|
||||
workShopName: string
|
||||
materialCode: string
|
||||
quantity: number | null
|
||||
specModel: string | null
|
||||
unit: string | null
|
||||
materialName: string | null
|
||||
routingNo: string | null
|
||||
planStartTime: string | null
|
||||
planFinishTime: string | null
|
||||
pickMtrlStatus: string | null
|
||||
children: KingdeeProcessRoute[]
|
||||
}
|
||||
|
||||
/** 金蝶工艺路线工序(树子节点) */
|
||||
export interface KingdeeProcessRoute {
|
||||
operNumber: number | null
|
||||
workCenterName: string | null
|
||||
departmentName: string | null
|
||||
processProperty: string | null
|
||||
operDescription: string | null
|
||||
activityUnit: string | null
|
||||
optCtrlCodeName: string | null
|
||||
activityQty: number | null
|
||||
planStartTime: string | null
|
||||
planFinishTime: string | null
|
||||
}
|
||||
|
||||
// 项目表 API
|
||||
export const orderProjectApi = {
|
||||
// 分页查询
|
||||
page(params: { page: number; pageSize: number; id?: number; name?: string; status?: number }) {
|
||||
return request({ url: '/biz/orderProject/page', method: 'get', params })
|
||||
},
|
||||
|
||||
// 获取详情
|
||||
detail(id: string) {
|
||||
return request({ url: `/biz/orderProject/${id}`, method: 'get' })
|
||||
},
|
||||
|
||||
// 新增
|
||||
create(data: OrderProject) {
|
||||
return request({ url: '/biz/orderProject', method: 'post', data })
|
||||
},
|
||||
|
||||
// 修改
|
||||
update(data: OrderProject) {
|
||||
return request({ url: '/biz/orderProject', method: 'put', data })
|
||||
},
|
||||
|
||||
// 删除
|
||||
delete(ids: string[]) {
|
||||
return request({ url: `/biz/orderProject/${ids.join(',')}`, method: 'delete' })
|
||||
},
|
||||
|
||||
// 导出
|
||||
export(params?: { ids?: string[]; id?: number; name?: string; status?: number }) {
|
||||
const p: Record<string, any> = {}
|
||||
if (params?.ids?.length) p.ids = params.ids.join(',')
|
||||
if (params?.id !== undefined && params?.id !== null) p.id = params.id
|
||||
if (params?.name !== undefined && params?.name !== null) p.name = params.name
|
||||
if (params?.status !== undefined && params?.status !== null) p.status = params.status
|
||||
return request({ url: `/biz/orderProject/export`, method: 'get', params: p, responseType: 'blob' })
|
||||
},
|
||||
|
||||
// 导入
|
||||
importData(file: File) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return request<{ success: number; fail: number; errors: string[] }>({
|
||||
url: `/biz/orderProject/import`,
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
},
|
||||
|
||||
// 下载导入模板
|
||||
downloadTemplate() {
|
||||
return request({ url: `/biz/orderProject/template`, method: 'get', responseType: 'blob' })
|
||||
},
|
||||
|
||||
// 查询金蝶生产订单
|
||||
getKingdeeOrder(id: number) {
|
||||
return request<KingdeePrdMo[]>({ url: `/biz/orderProject/kingdee/order/${id}`, method: 'get' })
|
||||
},
|
||||
}
|
||||
243
src/views/biz/orderProject/components/GanttSchedule.vue
Normal file
243
src/views/biz/orderProject/components/GanttSchedule.vue
Normal file
@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
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 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"
|
||||
gantt.config.scale_height = 50
|
||||
gantt.config.scales = [
|
||||
{ unit: "month", step: 1, format: "%Y年 %m月" },
|
||||
{ unit: "day", step: 1, format: "%d日" }
|
||||
]
|
||||
gantt.config.columns = [
|
||||
{ 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.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[] = []
|
||||
|
||||
|
||||
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.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
|
||||
})
|
||||
|
||||
if (mo.processRoutes && mo.processRoutes.length > 0) {
|
||||
mo.processRoutes.forEach((route: any, index: number) => {
|
||||
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.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
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
ganttEventIds.push(evId)
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
ganttEventIds.forEach(id => gantt.detachEvent(id))
|
||||
gantt.clearAll()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.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%;
|
||||
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>
|
||||
857
src/views/biz/orderProject/index.vue
Normal file
857
src/views/biz/orderProject/index.vue
Normal file
@ -0,0 +1,857 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<n-card>
|
||||
<!-- 搜索表单 -->
|
||||
<div class="search-form">
|
||||
<n-form inline :model="searchForm" label-placement="left">
|
||||
|
||||
<n-form-item label="项目名称">
|
||||
<n-input v-model:value="searchForm.name" placeholder="请输入项目名称" clearable />
|
||||
</n-form-item>
|
||||
<n-form-item label="项目状态">
|
||||
<n-select v-model:value="searchForm.status" placeholder="请选择项目状态" clearable style="width: 150px" :options="statusOptions" />
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-space>
|
||||
<n-button type="primary" @click="handleSearch">
|
||||
<template #icon><n-icon><SearchOutline /></n-icon></template>
|
||||
搜索
|
||||
</n-button>
|
||||
<n-button @click="handleReset">
|
||||
<template #icon><n-icon><RefreshOutline /></n-icon></template>
|
||||
重置
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div class="table-toolbar">
|
||||
<n-space>
|
||||
<n-button type="primary" @click="handleAdd">
|
||||
<template #icon><n-icon><AddOutline /></n-icon></template>
|
||||
新增
|
||||
</n-button>
|
||||
<n-button @click="importModalVisible = true">
|
||||
<template #icon><n-icon><CloudUploadOutline /></n-icon></template>
|
||||
导入
|
||||
</n-button>
|
||||
<n-button @click="handleExport">
|
||||
<template #icon><n-icon><DownloadOutline /></n-icon></template>
|
||||
导出{{ selectedIds.length > 0 ? `(${selectedIds.length})` : '' }}
|
||||
</n-button>
|
||||
<n-button type="error" :disabled="selectedIds.length === 0" @click="handleBatchDelete">
|
||||
<template #icon><n-icon><TrashOutline /></n-icon></template>
|
||||
删除
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-key="(row) => row.id"
|
||||
:scroll-x="2720"
|
||||
@update:page="handlePageChange"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
@update:checked-row-keys="handleCheck"
|
||||
/>
|
||||
</n-card>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 800px">
|
||||
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="100px">
|
||||
<n-grid :cols="2" :x-gap="24">
|
||||
<n-form-item-gi label="项目名称" path="name">
|
||||
<n-input v-model:value="formData.name" placeholder="请输入项目名称" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="生产令号" path="code">
|
||||
<n-input v-model:value="formData.code" placeholder="请输入生产令号" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="产品编码" path="mainCode">
|
||||
<n-input v-model:value="formData.mainCode" placeholder="请输入产品编码" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="产品名称" path="mainName">
|
||||
<n-input v-model:value="formData.mainName" placeholder="请输入产品名称" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="生产数量" path="quantity">
|
||||
<n-input v-model:value="formData.quantity" placeholder="请输入生产数量" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="项目来源" path="source">
|
||||
<n-select v-model:value="formData.source" placeholder="请选择项目来源" :options="sourceOptions" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="生产类型" path="prodType">
|
||||
<n-select v-model:value="formData.prodType" placeholder="请选择生产类型" :options="[]" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="来源单据编码" path="sourceId">
|
||||
<n-input v-model:value="formData.sourceId" placeholder="请输入项目来源单据编码" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="BOM版本" path="bomVersion">
|
||||
<n-input v-model:value="formData.bomVersion" placeholder="请输入BOM版本" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="规格型号" path="specModel">
|
||||
<n-input v-model:value="formData.specModel" placeholder="请输入规格型号" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="单位" path="unit">
|
||||
<n-input v-model:value="formData.unit" placeholder="请输入单位" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="客户" path="customerName">
|
||||
<n-input v-model:value="formData.customerName" placeholder="请输入客户" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="当前版本" path="version">
|
||||
<n-input v-model:value="formData.version" placeholder="请输入当前版本" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="销售部门" path="salesDept">
|
||||
<n-input v-model:value="formData.salesDept" placeholder="请输入销售部门" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="上级id" path="parentId">
|
||||
<n-input v-model:value="formData.parentId" placeholder="请输入上级id" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="要货日期" path="deliveryTime">
|
||||
<n-date-picker v-model:value="formData.deliveryTime" type="datetime" clearable style="width: 100%" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="项目状态" path="status">
|
||||
<n-select v-model:value="formData.status" placeholder="请选择项目状态" :options="statusOptions" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="计划完成日期" path="planFinishTime">
|
||||
<n-date-picker v-model:value="formData.planFinishTime" type="datetime" clearable style="width: 100%" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="变更时间" path="changeTime">
|
||||
<n-date-picker v-model:value="formData.changeTime" type="datetime" clearable style="width: 100%" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="变更人" path="changeBy">
|
||||
<n-input v-model:value="formData.changeBy" placeholder="请输入变更人" />
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="modalVisible = false">取消</n-button>
|
||||
<n-button type="primary" @click="handleSubmit">确定</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<!-- 金蝶预生产查询弹窗 -->
|
||||
<n-modal
|
||||
v-model:show="kingdeeModalVisible"
|
||||
preset="card"
|
||||
:title="kingdeeModalTitle"
|
||||
class="kingdee-modal"
|
||||
:style="{ width: '96vw', maxWidth: '1920px' }"
|
||||
:content-style="{ padding: '16px 24px', minHeight: '72vh' }"
|
||||
:mask-closable="false"
|
||||
:closable="false"
|
||||
:close-on-esc="false"
|
||||
>
|
||||
<div class="kingdee-toolbar">
|
||||
<n-space>
|
||||
<n-button
|
||||
size="small"
|
||||
:disabled="kingdeeLoading || !kingdeeTableData.length"
|
||||
@click="kingdeeToggleExpandAll"
|
||||
>
|
||||
{{ kingdeeAllExpanded ? '全部收起' : '全部展开' }}
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
type="primary"
|
||||
:disabled="kingdeeLoading || !kingdeeTableData.length"
|
||||
@click="handleGanttSchedule"
|
||||
>
|
||||
<template #icon><n-icon><CalendarOutline /></n-icon></template>
|
||||
甘特图排产
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
<n-data-table
|
||||
v-model:expanded-row-keys="kingdeeExpandedKeys"
|
||||
:columns="kingdeeMoColumns"
|
||||
:data="kingdeeTableData"
|
||||
:loading="kingdeeLoading"
|
||||
:row-key="kingdeeMoRowKey"
|
||||
:scroll-x="kingdeeMoScrollX"
|
||||
:max-height="kingdeeTableMaxHeight"
|
||||
:row-class-name="kingdeeMoRowClassName"
|
||||
size="small"
|
||||
/>
|
||||
<template #footer>
|
||||
<n-button @click="kingdeeModalVisible = false">关闭</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<!-- 导入弹窗 -->
|
||||
<n-modal v-model:show="importModalVisible" preset="card" title="导入项目表" style="width: 500px">
|
||||
<n-space vertical>
|
||||
<n-alert type="info">
|
||||
<template #header>导入说明</template>
|
||||
<ul style="margin: 0; padding-left: 16px; line-height: 1.8">
|
||||
<li>请先下载导入模板,按模板格式填写数据</li>
|
||||
<li>支持 .xlsx 或 .xls 格式</li>
|
||||
</ul>
|
||||
</n-alert>
|
||||
<n-space>
|
||||
<n-button type="primary" @click="handleDownloadTemplate">
|
||||
<template #icon><n-icon><DownloadOutline /></n-icon></template>
|
||||
下载模板
|
||||
</n-button>
|
||||
</n-space>
|
||||
<n-upload :max="1" accept=".xlsx,.xls" :show-file-list="true" :custom-request="handleImportUpload">
|
||||
<n-upload-dragger>
|
||||
<div style="margin-bottom: 12px">
|
||||
<n-icon size="48" :depth="3"><CloudUploadOutline /></n-icon>
|
||||
</div>
|
||||
<n-text style="font-size: 16px">点击或拖拽文件到此处上传</n-text>
|
||||
<n-p depth="3" style="margin: 8px 0 0 0">支持 .xlsx 或 .xls 格式</n-p>
|
||||
</n-upload-dragger>
|
||||
</n-upload>
|
||||
</n-space>
|
||||
<template #footer>
|
||||
<n-button @click="importModalVisible = false">关闭</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, h, computed, onMounted } 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'
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const hasPermission = (permission: string) => userStore.hasPermission(permission)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
id: null as number | null,
|
||||
name: '',
|
||||
status: null as number | null,
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<OrderProject[]>([])
|
||||
const loading = ref(false)
|
||||
const selectedIds = ref<number[]>([])
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 50]
|
||||
})
|
||||
|
||||
// 弹窗
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const importModalVisible = ref(false)
|
||||
const kingdeeModalVisible = ref(false)
|
||||
const kingdeeModalTitle = ref('金蝶生产订单')
|
||||
const kingdeeLoading = ref(false)
|
||||
/** 主表行:工序仅存 processRoutes,避免 children 触发树形重复行 */
|
||||
type KingdeeMoTableRow = Omit<KingdeePrdMo, 'children'> & { processRoutes: KingdeeProcessRoute[] }
|
||||
|
||||
const kingdeeTableData = ref<KingdeeMoTableRow[]>([])
|
||||
const kingdeeExpandedKeys = ref<Array<string | number>>([])
|
||||
const kingdeeTableMaxHeight = 'min(78vh, 720px)'
|
||||
const kingdeeMoScrollX = 1820
|
||||
const kingdeeProcessScrollX = 1360
|
||||
|
||||
function kingdeeMoRowKey(row: KingdeeMoTableRow) {
|
||||
return `mo-${row.productionOrderNo}-${row.billNo}`
|
||||
}
|
||||
|
||||
function kingdeeProcessRowKey(row: KingdeeProcessRoute) {
|
||||
return `route-${row.operNumber}-${row.workCenterName}-${row.operDescription}`
|
||||
}
|
||||
|
||||
function getKingdeeRoutes(mo: KingdeeMoTableRow) {
|
||||
return mo.processRoutes
|
||||
}
|
||||
|
||||
function formatKingdeeQty(val: number | null | undefined) {
|
||||
return val != null ? Number(val).toFixed(3) : '-'
|
||||
}
|
||||
|
||||
function kingdeeMoRowClassName() {
|
||||
return 'kingdee-mo-row'
|
||||
}
|
||||
|
||||
const kingdeeAllExpanded = computed(() => {
|
||||
const total = kingdeeTableData.value.length
|
||||
if (!total) return false
|
||||
return kingdeeExpandedKeys.value.length >= total
|
||||
})
|
||||
|
||||
function kingdeeToggleExpandAll() {
|
||||
if (kingdeeAllExpanded.value) {
|
||||
kingdeeExpandedKeys.value = []
|
||||
} else {
|
||||
kingdeeExpandedKeys.value = kingdeeTableData.value.map(kingdeeMoRowKey)
|
||||
}
|
||||
}
|
||||
|
||||
function parseKingdeePlanTime(val: string | number | null | undefined): number | null {
|
||||
if (val == null || val === '') return null
|
||||
if (typeof val === 'number') return val
|
||||
const ts = new Date(val.replace(' ', 'T')).getTime()
|
||||
return Number.isNaN(ts) ? null : ts
|
||||
}
|
||||
|
||||
function renderKingdeePlanPicker(row: KingdeeProcessRoute, field: 'planStartTime' | 'planFinishTime') {
|
||||
return h(NDatePicker, {
|
||||
value: parseKingdeePlanTime(row[field]),
|
||||
type: 'datetime',
|
||||
clearable: true,
|
||||
size: 'small',
|
||||
style: { width: '100%' },
|
||||
'onUpdate:value': (val: number | null) => {
|
||||
row[field] = val != null ? formatDateTime(val) : null
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function renderKingdeeExpand(row: KingdeeMoTableRow) {
|
||||
const routes = getKingdeeRoutes(row)
|
||||
if (!routes.length) {
|
||||
return h('div', { class: 'kingdee-expand-empty' }, '暂无工艺路线工序')
|
||||
}
|
||||
return h('div', { class: 'kingdee-expand-wrap' }, [
|
||||
h(NDataTable, {
|
||||
class: 'kingdee-process-table',
|
||||
columns: kingdeeProcessColumns,
|
||||
data: routes,
|
||||
rowKey: kingdeeProcessRowKey,
|
||||
size: 'small',
|
||||
bordered: true,
|
||||
striped: true,
|
||||
scrollX: kingdeeProcessScrollX,
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
const formRef = ref()
|
||||
const defaultFormData: OrderProject = {
|
||||
name: '',
|
||||
code: '',
|
||||
mainCode: '',
|
||||
mainName: '',
|
||||
quantity: undefined,
|
||||
source: undefined,
|
||||
prodType: undefined,
|
||||
sourceId: '',
|
||||
bomVersion: '',
|
||||
specModel: '',
|
||||
unit: '',
|
||||
customerName: '',
|
||||
version: '',
|
||||
salesDept: '',
|
||||
parentId: undefined,
|
||||
deliveryTime: undefined,
|
||||
status: undefined,
|
||||
planFinishTime: undefined,
|
||||
changeTime: undefined,
|
||||
changeBy: '',
|
||||
}
|
||||
const formData = reactive<OrderProject>({ ...defaultFormData })
|
||||
|
||||
// 字典选项(下拉框/单选框/复选框关联字典时使用)
|
||||
const sourceOptions = ref<{ label: string; value: any }[]>([])
|
||||
const statusOptions = ref<{ label: string; value: any }[]>([])
|
||||
|
||||
// 表单校验规则
|
||||
const formRules = {
|
||||
mainCode: { required: true, message: '请输入产品编码', trigger: 'blur' },
|
||||
mainName: { required: true, message: '请输入产品名称', trigger: 'blur' },
|
||||
changeTime: { required: true, type: 'number', message: '请选择变更时间', trigger: ['change', 'blur'] },
|
||||
changeBy: { required: true, message: '请输入变更人', trigger: 'blur' },
|
||||
}
|
||||
|
||||
// 表格列
|
||||
const columns: DataTableColumns<OrderProject> = [
|
||||
{ type: 'selection', width: 48, fixed: 'left' },
|
||||
{ title: '生产令号', key: 'code', width: 140, align: 'center', fixed: 'left' },
|
||||
{ title: '产品编码', key: 'mainCode', width: 120, align: 'center' },
|
||||
{ title: '产品名称', key: 'mainName', width: 160, ellipsis: { tooltip: true } },
|
||||
{ title: '生产数量', key: 'quantity', width: 90 ,align: 'center'},
|
||||
{ title: '项目来源', key: 'source', width: 110,align: 'center',
|
||||
render(row) {
|
||||
const val = row.source
|
||||
const opt = sourceOptions.value.find(o => o.value === val || String(o.value) === String(val))
|
||||
return opt ? opt.label : (val ?? '-')
|
||||
}
|
||||
},
|
||||
{ title: '生产类型', key: 'prodType', width: 100 ,align: 'center'},
|
||||
{ title: 'BOM版本', key: 'bomVersion', width: 100 ,align: 'center'},
|
||||
{ title: '规格型号', key: 'specModel', width: 140, ellipsis: { tooltip: true } },
|
||||
{ title: '单位', key: 'unit', width: 70 ,align: 'center'},
|
||||
{ title: '客户', key: 'customerName', width: 140, ellipsis: { tooltip: true } ,align: 'center'},
|
||||
{ title: '销售部门', key: 'salesDept', width: 120 ,align: 'center'},
|
||||
{ title: '要货日期', key: 'deliveryTime', width: 170 ,align: 'center'},
|
||||
{ title: '项目状态', key: 'status', width: 100,
|
||||
render(row) {
|
||||
const val = row.status
|
||||
const opt = statusOptions.value.find(o => o.value === val || String(o.value) === String(val))
|
||||
return opt ? opt.label : (val ?? '-')
|
||||
}
|
||||
},
|
||||
{ title: '计划完成日期', key: 'planFinishTime', width: 170 ,align: 'center'},
|
||||
{ title: '创建时间', key: 'createTime', width: 180,align: 'center' },
|
||||
{ title: '创建人', key: 'createBy', width: 100 ,align: 'center'},
|
||||
{ title: '变更时间', key: 'changeTime', width: 180 ,align: 'center'},
|
||||
{ title: '变更人', key: 'changeBy', width: 100,align: 'center' },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 260,
|
||||
fixed: 'right',
|
||||
render(row) {
|
||||
const buttons = []
|
||||
if (hasPermission('biz:orderProject:getKingdeeOrder')) {
|
||||
buttons.push(
|
||||
h(NButton, { size: 'small', quaternary: true, type: 'info', onClick: () => handleKingdeeQuery(row) }, {
|
||||
default: () => [h(NIcon, null, { default: () => h(EyeOutline) }), ' 预生产查询']
|
||||
})
|
||||
)
|
||||
}
|
||||
buttons.push(
|
||||
h(NButton, { size: 'small', quaternary: true, onClick: () => handleEdit(row) }, {
|
||||
default: () => [h(NIcon, null, { default: () => h(CreateOutline) }), ' 编辑']
|
||||
}),
|
||||
h(NButton, { size: 'small', quaternary: true, type: 'error', onClick: () => handleDelete(row) }, {
|
||||
default: () => [h(NIcon, null, { default: () => h(TrashOutline) }), ' 删除']
|
||||
})
|
||||
)
|
||||
return h('div', { style: { display: 'flex', alignItems: 'center', gap: '4px', flexWrap: 'nowrap' } }, buttons)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const pickMtrlStatusMap: Record<string, string> = {
|
||||
'0': '未领料',
|
||||
'1': '已领料',
|
||||
}
|
||||
|
||||
const kingdeeMoColumns: DataTableColumns<KingdeeMoTableRow> = [
|
||||
{
|
||||
type: 'expand',
|
||||
expandable: () => true,
|
||||
renderExpand: (row) => renderKingdeeExpand(row),
|
||||
},
|
||||
{
|
||||
title: '类型', key: 'rowType', width: 96, align: 'center',
|
||||
render() {
|
||||
return h(NTag, { size: 'small', type: 'info', bordered: false }, { default: () => '生产订单' })
|
||||
}
|
||||
},
|
||||
{ title: '单据编号', key: 'billNo', width: 120, align: 'center', ellipsis: { tooltip: true } },
|
||||
{ title: '生产令号', key: 'productionOrderNo', width: 130, align: 'center', ellipsis: { tooltip: true } },
|
||||
{ title: '工艺路线', key: 'routingNo', width: 110, align: 'center', ellipsis: { tooltip: true } },
|
||||
{ title: '物料编码', key: 'materialCode', width: 180, ellipsis: { tooltip: true } },
|
||||
{ title: '物料名称', key: 'materialName', width: 160, ellipsis: { tooltip: true } },
|
||||
{
|
||||
title: '数量', key: 'quantity', width: 90, align: 'center',
|
||||
render(row) {
|
||||
return formatKingdeeQty(row.quantity)
|
||||
}
|
||||
},
|
||||
{ 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: 'pickMtrlStatus', width: 90, align: 'center',
|
||||
render(row) {
|
||||
const val = row.pickMtrlStatus
|
||||
return val != null ? (pickMtrlStatusMap[String(val)] ?? val) : '-'
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
const kingdeeProcessColumns: DataTableColumns<KingdeeProcessRoute> = [
|
||||
{
|
||||
title: '类型', key: 'rowType', width: 72, align: 'center',
|
||||
render() {
|
||||
return h(NTag, { size: 'small', type: 'warning', bordered: false }, { default: () => '工序' })
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '工序号', key: 'operNumber', width: 80, align: 'center',
|
||||
render(row) {
|
||||
return row.operNumber != null ? String(row.operNumber) : '-'
|
||||
}
|
||||
},
|
||||
{ title: '工序名称', key: 'processProperty', width: 100, align: 'center' },
|
||||
{ title: '工序说明', key: 'operDescription', width: 140, ellipsis: { tooltip: true } },
|
||||
{ title: '工作中心', key: 'workCenterName', width: 110, align: 'center', ellipsis: { tooltip: true } },
|
||||
{ title: '生产车间', key: 'departmentName', width: 110, align: 'center', ellipsis: { tooltip: true } },
|
||||
{
|
||||
title: '计划开始时间', key: 'planStartTime', width: 180, align: 'center',
|
||||
render(row) {
|
||||
return renderKingdeePlanPicker(row, 'planStartTime')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '计划结束时间', key: 'planFinishTime', width: 180, align: 'center',
|
||||
render(row) {
|
||||
return renderKingdeePlanPicker(row, 'planFinishTime')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '活动量', key: 'activityQty', width: 90, align: 'center',
|
||||
render(row) {
|
||||
return formatKingdeeQty(row.activityQty)
|
||||
}
|
||||
},
|
||||
{ title: '活动单位', key: 'activityUnit', width: 80, align: 'center' },
|
||||
{ title: '控制码', key: 'optCtrlCodeName', width: 120, align: 'center', ellipsis: { tooltip: true } },
|
||||
]
|
||||
|
||||
// 加载数据
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await orderProjectApi.page({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
id: searchForm.id || undefined,
|
||||
name: searchForm.name || undefined,
|
||||
status: searchForm.status || undefined,
|
||||
})
|
||||
tableData.value = res.list
|
||||
pagination.itemCount = res.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchForm.id = null
|
||||
|
||||
searchForm.name = ''
|
||||
|
||||
searchForm.status = null
|
||||
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
// 分页
|
||||
function handlePageChange(page: number) {
|
||||
pagination.page = page
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
pagination.pageSize = pageSize
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 选择
|
||||
function handleCheck(keys: Array<string | number>) {
|
||||
selectedIds.value = keys as number[]
|
||||
}
|
||||
|
||||
// 新增
|
||||
function handleAdd() {
|
||||
modalTitle.value = '新增项目表'
|
||||
Object.assign(formData, defaultFormData)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
function handleEdit(row: OrderProject) {
|
||||
modalTitle.value = '编辑项目表'
|
||||
Object.assign(formData, row)
|
||||
if (formData.deliveryTime && typeof formData.deliveryTime === 'string') {
|
||||
formData.deliveryTime = new Date(formData.deliveryTime.replace(' ', 'T')).getTime()
|
||||
}
|
||||
if (formData.planFinishTime && typeof formData.planFinishTime === 'string') {
|
||||
formData.planFinishTime = new Date(formData.planFinishTime.replace(' ', 'T')).getTime()
|
||||
}
|
||||
if (formData.createTime && typeof formData.createTime === 'string') {
|
||||
formData.createTime = new Date(formData.createTime.replace(' ', 'T')).getTime()
|
||||
}
|
||||
if (formData.changeTime && typeof formData.changeTime === 'string') {
|
||||
const ts = new Date(formData.changeTime.replace(' ', 'T')).getTime()
|
||||
formData.changeTime = Number.isNaN(ts) ? undefined : ts
|
||||
}
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
function formatDateTime(ts: number) {
|
||||
const d = new Date(ts)
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
// 提交
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const submitData = { ...formData } as OrderProject
|
||||
if (typeof submitData.deliveryTime === 'number') {
|
||||
submitData.deliveryTime = formatDateTime(submitData.deliveryTime)
|
||||
}
|
||||
if (typeof submitData.planFinishTime === 'number') {
|
||||
submitData.planFinishTime = formatDateTime(submitData.planFinishTime)
|
||||
}
|
||||
if (typeof submitData.createTime === 'number') {
|
||||
submitData.createTime = formatDateTime(submitData.createTime)
|
||||
}
|
||||
if (typeof submitData.changeTime === 'number') {
|
||||
submitData.changeTime = formatDateTime(submitData.changeTime)
|
||||
}
|
||||
if (submitData.id) {
|
||||
await orderProjectApi.update(submitData)
|
||||
message.success('修改成功')
|
||||
} else {
|
||||
await orderProjectApi.create(submitData)
|
||||
message.success('新增成功')
|
||||
}
|
||||
modalVisible.value = false
|
||||
loadData()
|
||||
} catch (error) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
// 甘特图排产
|
||||
function handleGanttSchedule() {
|
||||
// 将待排产数据存入 sessionStorage
|
||||
sessionStorage.setItem('gantt_schedule_data', JSON.stringify(kingdeeTableData.value))
|
||||
// 跳转到甘特图排产独立页面
|
||||
router.push('/biz/orderProject/gantt')
|
||||
}
|
||||
|
||||
// 金蝶预生产查询
|
||||
async function handleKingdeeQuery(row: OrderProject) {
|
||||
if (!row.id) {
|
||||
message.warning('缺少项目ID,无法查询')
|
||||
return
|
||||
}
|
||||
kingdeeModalTitle.value = `金蝶生产订单${row.code ? ` - ${row.code}` : ''}`
|
||||
kingdeeModalVisible.value = true
|
||||
kingdeeLoading.value = true
|
||||
kingdeeTableData.value = []
|
||||
kingdeeExpandedKeys.value = []
|
||||
try {
|
||||
const data = await orderProjectApi.getKingdeeOrder(row.id)
|
||||
kingdeeTableData.value = (Array.isArray(data) ? data : []).map((mo) => {
|
||||
const { children, ...rest } = mo
|
||||
const routes = [...(children ?? [])].sort((a, b) => (a.operNumber ?? 0) - (b.operNumber ?? 0)).map((r) => ({
|
||||
...r,
|
||||
planStartTime: r.planStartTime ?? null,
|
||||
planFinishTime: r.planFinishTime ?? null,
|
||||
}))
|
||||
return { ...rest, processRoutes: routes }
|
||||
})
|
||||
if (kingdeeTableData.value.length === 0) {
|
||||
message.info('未查询到金蝶生产订单数据')
|
||||
}
|
||||
} finally {
|
||||
kingdeeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
function handleDelete(row: OrderProject) {
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: '确定要删除该记录吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await orderProjectApi.delete([row.id!])
|
||||
message.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
function handleBatchDelete() {
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: `确定要删除选中的 ${selectedIds.value.length} 条记录吗?`,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await orderProjectApi.delete(selectedIds.value)
|
||||
message.success('删除成功')
|
||||
selectedIds.value = []
|
||||
loadData()
|
||||
} catch (error) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 导出
|
||||
async function handleExport() {
|
||||
try {
|
||||
const params: Record<string, any> = {}
|
||||
if (selectedIds.value.length > 0) params.ids = selectedIds.value
|
||||
if (searchForm.id != null) params.id = searchForm.id
|
||||
if (searchForm.name) params.name = searchForm.name
|
||||
if (searchForm.status != null) params.status = searchForm.status
|
||||
const blob = await orderProjectApi.export(params)
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = '项目表数据.xlsx'
|
||||
link.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
} catch (error) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
// 下载导入模板
|
||||
async function handleDownloadTemplate() {
|
||||
try {
|
||||
const blob = await orderProjectApi.downloadTemplate()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = '项目表导入模板.xlsx'
|
||||
link.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
} catch (error) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
// 导入上传
|
||||
async function handleImportUpload({ file }: UploadCustomRequestOptions) {
|
||||
if (!file.file) return
|
||||
try {
|
||||
const result = await orderProjectApi.importData(file.file)
|
||||
if (result.fail > 0) {
|
||||
dialog.warning({
|
||||
title: '导入结果',
|
||||
content: `成功: ${result.success} 条,失败: ${result.fail} 条\n错误信息: ${(result.errors || []).join('\n') || '无'}`,
|
||||
positiveText: '确定'
|
||||
})
|
||||
} else {
|
||||
message.success(`导入成功,共 ${result.success} 条数据`)
|
||||
importModalVisible.value = false
|
||||
}
|
||||
loadData()
|
||||
} catch (error) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
// 加载字典选项
|
||||
async function loadDictOptions() {
|
||||
try {
|
||||
const data = await dictDataApi.listByType('pro_source')
|
||||
sourceOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue) }))
|
||||
} catch {}
|
||||
try {
|
||||
const data = await dictDataApi.listByType('pro_status')
|
||||
statusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue) }))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadDictOptions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.kingdee-toolbar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
:deep(.kingdee-modal .n-card) {
|
||||
max-height: 94vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:deep(.kingdee-modal .n-card__content) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.kingdee-mo-row td) {
|
||||
background-color: rgba(32, 128, 240, 0.06) !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.kingdee-mo-row:hover td) {
|
||||
background-color: rgba(32, 128, 240, 0.1) !important;
|
||||
}
|
||||
|
||||
.kingdee-expand-wrap {
|
||||
margin: 4px 8px 8px 36px;
|
||||
padding: 12px 14px 14px;
|
||||
border: 1px solid var(--n-border-color);
|
||||
border-left: 4px solid #18a058;
|
||||
border-radius: 6px;
|
||||
background: var(--n-action-color);
|
||||
}
|
||||
|
||||
:deep(.kingdee-process-table .n-data-table-th) {
|
||||
background: rgba(24, 160, 88, 0.12) !important;
|
||||
}
|
||||
|
||||
:deep(.kingdee-process-table .n-data-table-td) {
|
||||
background-color: var(--n-color) !important;
|
||||
}
|
||||
|
||||
:deep(.kingdee-process-table .n-data-table-tr--striped .n-data-table-td) {
|
||||
background-color: var(--n-action-color) !important;
|
||||
}
|
||||
|
||||
.kingdee-expand-empty {
|
||||
margin: 4px 8px 8px 36px;
|
||||
padding: 12px 14px;
|
||||
font-size: 13px;
|
||||
color: var(--n-text-color-3);
|
||||
border: 1px dashed var(--n-border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--n-action-color);
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user