463 lines
14 KiB
Vue
463 lines
14 KiB
Vue
<template>
|
||
<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"
|
||
style="margin-bottom:15px"
|
||
/>
|
||
<div style="margin-bottom: 1px;">
|
||
<!-- <template #footer> -->
|
||
<n-space justify="end">
|
||
<n-button type="error" :disabled="kingdeeLoading" @click="reCrawlReorderForm">
|
||
重新读取数据
|
||
</n-button>
|
||
<n-button type="primary" :disabled="kingdeeLoading" @click="handleSaveDraft">
|
||
保存草稿
|
||
</n-button>
|
||
<n-button type="info" :disabled="kingdeeLoading" @click="handleSyncOrderAndPlan">
|
||
同步订单计划
|
||
</n-button>
|
||
<n-button @click="$emit('close-modal')">关闭</n-button>
|
||
</n-space>
|
||
<!-- </template> -->
|
||
</div>
|
||
|
||
</template>
|
||
|
||
|
||
<script setup lang="ts">
|
||
import { ref, h, computed, defineProps, onMounted, watch, defineEmits} from 'vue'
|
||
import { NButton, NSpace, NIcon, NTag, NDatePicker, NDataTable, useMessage, useDialog, type DataTableColumns } from 'naive-ui'
|
||
import { type KingdeePrdMo, type KingdeeProcessRoute } from '@/api/orderProject'
|
||
import { useRouter } from 'vue-router'
|
||
import { CalendarOutline } from '@vicons/ionicons5'
|
||
|
||
|
||
const emit = defineEmits(['close-modal','holdTemporarilyCallback', "synchronizationCallback","reCrawlCallback"])
|
||
const dialog = useDialog()
|
||
const message = useMessage()
|
||
const router = useRouter()
|
||
|
||
const props = defineProps({
|
||
kingdeeTableData: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
selectedId:{
|
||
type: Number,
|
||
default: null
|
||
}
|
||
})
|
||
|
||
onMounted(() => {
|
||
kingdeeTableData.value = props.kingdeeTableData as KingdeeMoTableRow[]
|
||
})
|
||
|
||
watch(()=>props.kingdeeTableData,()=>{
|
||
kingdeeTableData.value = props.kingdeeTableData as KingdeeMoTableRow[]
|
||
},{ deep: true } )
|
||
|
||
|
||
const kingdeeModalVisible = ref(false)
|
||
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
|
||
|
||
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: '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 } },
|
||
]
|
||
|
||
|
||
|
||
|
||
const kingdeeAllExpanded = computed(() => {
|
||
const total = kingdeeTableData.value.length
|
||
if (!total) return false
|
||
return kingdeeExpandedKeys.value.length >= total
|
||
})
|
||
|
||
const pickMtrlStatusMap: Record<string, string> = {
|
||
'1': '未领料',
|
||
'2': '待定',
|
||
'3': '已领料',
|
||
}
|
||
|
||
function kingdeeMoRowClassName() {
|
||
return 'kingdee-mo-row'
|
||
}
|
||
|
||
|
||
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())}`
|
||
}
|
||
|
||
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 formatKingdeeQty(val: number | null | undefined) {
|
||
return val != null ? Number(val).toFixed(3) : '-'
|
||
}
|
||
|
||
|
||
function kingdeeMoRowKey(row: KingdeeMoTableRow) {
|
||
return `mo-${row.productionOrderNo}-${row.billNo}`
|
||
}
|
||
|
||
function kingdeeToggleExpandAll() {
|
||
if (kingdeeAllExpanded.value) {
|
||
kingdeeExpandedKeys.value = []
|
||
} else {
|
||
kingdeeExpandedKeys.value = kingdeeTableData.value.map(kingdeeMoRowKey)
|
||
}
|
||
}
|
||
|
||
|
||
|
||
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,
|
||
}),
|
||
])
|
||
}
|
||
|
||
function getKingdeeRoutes(mo: KingdeeMoTableRow) {
|
||
return mo.processRoutes
|
||
}
|
||
function kingdeeProcessRowKey(row: KingdeeProcessRoute) {
|
||
return `route-${row.operNumber}-${row.workCenterName}-${row.operDescription}`
|
||
}
|
||
|
||
// 甘特图排产
|
||
function handleGanttSchedule() {
|
||
// 将待排产数据和当前项目ID存入 sessionStorage,供甘特图页面使用
|
||
sessionStorage.setItem('gantt_schedule_data', JSON.stringify(kingdeeTableData.value))
|
||
if (props.selectedId) {
|
||
sessionStorage.setItem('gantt_project_id', String(props.selectedId))
|
||
}
|
||
// 跳转到甘特图排产独立页面
|
||
router.push('/biz/orderProject/gantt')
|
||
}
|
||
|
||
// 同步订单和计划表
|
||
async function handleSyncOrderAndPlan() {
|
||
if (!props.selectedId) return
|
||
|
||
kingdeeLoading.value = true
|
||
try {
|
||
// 恢复数据结构,将 processRoutes 转换为 children
|
||
const draftData = kingdeeTableData.value.map(mo => {
|
||
const { processRoutes, ...rest } = mo
|
||
return { ...rest, children: processRoutes } as KingdeePrdMo
|
||
})
|
||
|
||
emit('synchronizationCallback', props.selectedId, draftData, (res) => {
|
||
if("success" === res){
|
||
message.success('同步订单计划成功')
|
||
emit('close-modal')
|
||
}
|
||
})
|
||
kingdeeModalVisible.value = false
|
||
} catch (error) {
|
||
// 错误拦截器已处理
|
||
} finally {
|
||
kingdeeLoading.value = false
|
||
}
|
||
}
|
||
|
||
|
||
|
||
const reCrawlReorderForm = () => {
|
||
// // 暂存金蝶生产订单数据(草稿)
|
||
if (!props.selectedId) return
|
||
|
||
dialog.warning({
|
||
title: '提示',
|
||
content: '重启读取会导致当前页面数据全部重置, 确定需要重新读取吗?',
|
||
positiveText: '确定',
|
||
negativeText: '取消',
|
||
onPositiveClick: () => {
|
||
kingdeeLoading.value = true
|
||
try {
|
||
emit('reCrawlCallback', props.selectedId, true)
|
||
message.success('重启读取成功')
|
||
} catch (error) {
|
||
// 错误拦截器已处理
|
||
} finally {
|
||
kingdeeLoading.value = false
|
||
}
|
||
}
|
||
})
|
||
|
||
|
||
|
||
}
|
||
|
||
const handleSaveDraft = () => {
|
||
// // 暂存金蝶生产订单数据(草稿)
|
||
if (!props.selectedId) return
|
||
|
||
kingdeeLoading.value = true
|
||
try {
|
||
const draftData = kingdeeTableData.value.map(mo => {
|
||
const { processRoutes, ...rest } = mo
|
||
return { ...rest, children: processRoutes } as KingdeePrdMo
|
||
})
|
||
emit('holdTemporarilyCallback', props.selectedId, draftData)
|
||
message.success('草稿保存成功')
|
||
} catch (error) {
|
||
// 错误拦截器已处理
|
||
} finally {
|
||
kingdeeLoading.value = false
|
||
}
|
||
|
||
}
|
||
// // 暂存金蝶生产订单数据(草稿)
|
||
// async function handleSaveDraft() {
|
||
// if (!kingdeeCurrentProjectId) return
|
||
|
||
// kingdeeLoading.value = true
|
||
// try {
|
||
// // 恢复数据结构,将 processRoutes 转换为 children
|
||
// const draftData = kingdeeTableData.value.map(mo => {
|
||
// const { processRoutes, ...rest } = mo
|
||
// return { ...rest, children: processRoutes } as KingdeePrdMo
|
||
// })
|
||
|
||
// emit('holdTemporarilyCallback', { id:kingdeeCurrentProjectId, data:draftData })
|
||
// // await orderProjectApi.saveKingdeeOrderDraft(kingdeeCurrentProjectId.value, draftData)
|
||
// // message.success('草稿保存成功')
|
||
// } catch (error) {
|
||
// // 错误拦截器已处理
|
||
// } finally {
|
||
// kingdeeLoading.value = false
|
||
// }
|
||
// }
|
||
</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>
|