项目管理部分甘特图
This commit is contained in:
parent
2caa3e0fa0
commit
5fae83d994
@ -13,6 +13,7 @@
|
|||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
"@xterm/xterm": "^6.0.0",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"axios": "^1.6.5",
|
"axios": "^1.6.5",
|
||||||
|
"dhtmlx-gantt": "^9.1.4",
|
||||||
"disable-devtool": "^0.3.9",
|
"disable-devtool": "^0.3.9",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"jsencrypt": "^3.3.2",
|
"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' })
|
||||||
|
},
|
||||||
|
}
|
||||||
162
src/views/biz/orderProject/components/GanttSchedule.vue
Normal file
162
src/views/biz/orderProject/components/GanttSchedule.vue
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
<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-container" ref="ganttContainer"></div>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, onUnmounted, nextTick } from 'vue'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import { gantt } from 'dhtmlx-gantt'
|
||||||
|
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
|
||||||
|
|
||||||
|
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 ganttContainer = ref<HTMLElement | null>(null)
|
||||||
|
const message = useMessage()
|
||||||
|
let ganttEventIds: string[] = []
|
||||||
|
|
||||||
|
function initGantt() {
|
||||||
|
if (!ganttContainer.value) return
|
||||||
|
|
||||||
|
// 清理之前的事件绑定
|
||||||
|
ganttEventIds.forEach(id => gantt.detachEvent(id))
|
||||||
|
ganttEventIds = []
|
||||||
|
|
||||||
|
// 配置甘特图
|
||||||
|
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: 220, resize: true },
|
||||||
|
{ name: "start_date", label: "计划开始", align: "center", width: 120 },
|
||||||
|
{ name: "end_date", label: "计划结束", align: "center", width: 120 },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 行为配置
|
||||||
|
gantt.config.drag_progress = false // 禁用进度拖拽
|
||||||
|
gantt.config.drag_links = false // 禁用连线拖拽(视需求而定)
|
||||||
|
gantt.config.work_time = true // 启用工作时间计算(如果需要可配置)
|
||||||
|
|
||||||
|
gantt.i18n.setLocale("cn")
|
||||||
|
|
||||||
|
gantt.init(ganttContainer.value)
|
||||||
|
|
||||||
|
// 转换数据格式
|
||||||
|
const tasks: any[] = []
|
||||||
|
|
||||||
|
props.data.forEach((mo) => {
|
||||||
|
const moId = `mo_${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
|
||||||
|
|
||||||
|
tasks.push({
|
||||||
|
id: moId,
|
||||||
|
text: `${mo.materialName || '产品'} (${mo.productionOrderNo})`,
|
||||||
|
start_date: minDate || new Date(),
|
||||||
|
end_date: maxDate || new Date(Date.now() + 86400000),
|
||||||
|
open: true,
|
||||||
|
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_${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))
|
||||||
|
|
||||||
|
tasks.push({
|
||||||
|
id: routeId,
|
||||||
|
text: `${route.operDescription || route.processProperty} (${route.operNumber})`,
|
||||||
|
start_date: rStart,
|
||||||
|
end_date: rEnd,
|
||||||
|
parent: moId,
|
||||||
|
_type: 'route',
|
||||||
|
_raw: route
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
gantt.clearAll()
|
||||||
|
gantt.parse({ data: tasks, links: [] })
|
||||||
|
|
||||||
|
// 绑定拖拽结束事件
|
||||||
|
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}`)
|
||||||
|
|
||||||
|
// 触发更新事件给父组件,同步表格数据
|
||||||
|
emit('update-time', {
|
||||||
|
id: id as string,
|
||||||
|
type: task._type,
|
||||||
|
item: task._raw,
|
||||||
|
start,
|
||||||
|
end
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
ganttEventIds.push(evId)
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
ganttEventIds.forEach(id => gantt.detachEvent(id))
|
||||||
|
gantt.clearAll()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.gantt-modal :deep(.n-card__content) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.gantt-container {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 500px;
|
||||||
|
border: 1px solid var(--n-border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
872
src/views/biz/orderProject/index.vue
Normal file
872
src/views/biz/orderProject/index.vue
Normal file
@ -0,0 +1,872 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 甘特图排产组件 -->
|
||||||
|
<GanttSchedule
|
||||||
|
v-model:show="ganttModalVisible"
|
||||||
|
:data="kingdeeTableData"
|
||||||
|
@update-time="handleGanttTimeUpdate"
|
||||||
|
/>
|
||||||
|
</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 GanttSchedule from './components/GanttSchedule.vue'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
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)
|
||||||
|
const ganttModalVisible = 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() {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 金蝶预生产查询
|
||||||
|
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>
|
||||||
@ -16,11 +16,12 @@ export default defineConfig({
|
|||||||
emptyOutDir: true
|
emptyOutDir: true
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
host: "192.168.5.231",
|
// host: "192.168.5.231",
|
||||||
|
host: true,
|
||||||
port: 3000,
|
port: 3000,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target:'http://192.168.6.107:8080', //'http://192.168.5.230:8888',
|
target:'http://localhost:8888', //'http://192.168.5.230:8888',
|
||||||
//target:'http://192.168.5.230:8888',
|
//target:'http://192.168.5.230:8888',
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user