mes-vue/src/views/biz/orderProject/index.vue

948 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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-space justify="end">
<n-button type="info" :disabled="kingdeeLoading" @click="handleSyncOrderAndPlan">
同步订单计划
</n-button>
<n-button type="primary" :disabled="kingdeeLoading" @click="handleSaveDraft">
保存草稿
</n-button>
<n-button @click="kingdeeModalVisible = false">关闭</n-button>
</n-space>
</template>
</n-modal>
<!-- 导入弹窗 -->
<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, watch } from 'vue'
import { NButton, NSpace, NIcon, NTag, NDatePicker, NDataTable, NUpload, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions } from 'naive-ui'
import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline, EyeOutline, CalendarOutline } from '@vicons/ionicons5'
import { orderProjectApi, type OrderProject, type KingdeePrdMo, type KingdeeProcessRoute, normalizeKingdeeProcessRoute } from '@/api/orderProject'
import { dictDataApi } from '@/api/org'
import { useUserStore } from '@/stores/user'
import { useRouter, useRoute } from 'vue-router'
const message = useMessage()
const dialog = useDialog()
const userStore = useUserStore()
const router = useRouter()
const route = useRoute()
const hasPermission = (permission: string) => userStore.hasPermission(permission)
watch(() => route.path, (newPath, oldPath) => {
if (newPath === '/biz/orderProject' && oldPath === '/biz/orderProject/gantt') {
if (kingdeeModalVisible.value && kingdeeCurrentProjectId.value) {
handleKingdeeQuery({ id: kingdeeCurrentProjectId.value } as OrderProject)
}
}
})
// 搜索表单
const searchForm = reactive({
id: null as number | null,
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 kingdeeCurrentProjectId = ref<number | null>(null) // 记录当前正在查询的订单ID
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%' },
onUpdateValue: (val: number | null) => {
row[field] = val != null ? formatDateTime(val) : null
},
})
}
function renderKingdeePlanPickerMo(row: KingdeeMoTableRow, field: 'planStartTime' | 'planFinishTime') {
return h(NDatePicker, {
value: parseKingdeePlanTime(row[field]),
type: 'datetime',
clearable: true,
size: 'small',
style: { width: '100%' },
onUpdateValue: (val: number | null) => {
row[field] = val != null ? formatDateTime(val) : null
},
})
}
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: 180, align: 'center',
render(row) {
return renderKingdeePlanPickerMo(row, 'planStartTime')
}
},
{
title: '计划完工', key: 'planFinishTime', width: 180, align: 'center',
render(row) {
return renderKingdeePlanPickerMo(row, 'planFinishTime')
}
},
{
title: '领料状态', key: 'pickMtrlStatus', width: 90, align: 'center',
render(row) {
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: 'processName', 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() {
// 将待排产数据和当前项目ID存入 sessionStorage供甘特图页面使用
sessionStorage.setItem('gantt_schedule_data', JSON.stringify(kingdeeTableData.value))
if (kingdeeCurrentProjectId.value) {
sessionStorage.setItem('gantt_project_id', String(kingdeeCurrentProjectId.value))
}
// 跳转到甘特图排产独立页面
router.push('/biz/orderProject/gantt')
}
// 金蝶预生产查询
async function handleKingdeeQuery(row: OrderProject) {
if (!row.id) {
message.warning('缺少项目ID无法查询')
return
}
kingdeeCurrentProjectId.value = row.id
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) => normalizeKingdeeProcessRoute({
...r,
planStartTime: r.planStartTime ?? null,
planFinishTime: r.planFinishTime ?? null,
}))
return { ...rest, processRoutes: routes }
})
if (kingdeeTableData.value.length === 0) {
message.info('未查询到金蝶生产订单数据')
}
} finally {
kingdeeLoading.value = false
}
}
// 暂存金蝶生产订单数据(草稿)
async function handleSaveDraft() {
if (!kingdeeCurrentProjectId.value) return
kingdeeLoading.value = true
try {
// 恢复数据结构,将 processRoutes 转换为 children
const draftData = kingdeeTableData.value.map(mo => {
const { processRoutes, ...rest } = mo
return { ...rest, children: processRoutes } as KingdeePrdMo
})
await orderProjectApi.saveKingdeeOrderDraft(kingdeeCurrentProjectId.value, draftData)
message.success('草稿保存成功')
} catch (error) {
// 错误拦截器已处理
} finally {
kingdeeLoading.value = false
}
}
// 同步订单和计划表
async function handleSyncOrderAndPlan() {
if (!kingdeeCurrentProjectId.value) return
kingdeeLoading.value = true
try {
// 恢复数据结构,将 processRoutes 转换为 children
const draftData = kingdeeTableData.value.map(mo => {
const { processRoutes, ...rest } = mo
return { ...rest, children: processRoutes } as KingdeePrdMo
})
await orderProjectApi.saveKingdeeOrderAndPlan(kingdeeCurrentProjectId.value, draftData)
message.success('同步订单计划成功')
kingdeeModalVisible.value = false
} catch (error) {
// 错误拦截器已处理
} finally {
kingdeeLoading.value = false
}
}
// 删除
function handleDelete(row: OrderProject) {
dialog.warning({
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>