948 lines
32 KiB
Vue
948 lines
32 KiB
Vue
<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>
|