工序计划拆分预览/提交,完善工艺路线选择
This commit is contained in:
parent
8f9bf2bdf2
commit
f22754edbf
@ -22,6 +22,14 @@ export interface BasicProcessPlan {
|
|||||||
|
|
||||||
departmentName?: string
|
departmentName?: string
|
||||||
|
|
||||||
|
sectionId?: number | null
|
||||||
|
|
||||||
|
workShopId?: number | null
|
||||||
|
|
||||||
|
sectionName?: string
|
||||||
|
|
||||||
|
workShopName?: string
|
||||||
|
|
||||||
operDescription?: string
|
operDescription?: string
|
||||||
|
|
||||||
createBy?: number
|
createBy?: number
|
||||||
|
|||||||
@ -31,15 +31,51 @@ export interface DeviceRealtimeVO {
|
|||||||
displayName: string
|
displayName: string
|
||||||
synEquipId: number
|
synEquipId: number
|
||||||
bound: boolean
|
bound: boolean
|
||||||
|
/** connected:机台是否在线 */
|
||||||
connected?: number | null
|
connected?: number | null
|
||||||
|
/** cnc_status:控制器状态 */
|
||||||
cncStatus?: number | null
|
cncStatus?: number | null
|
||||||
cncStatusText?: string
|
cncStatusText?: string
|
||||||
|
/** part_count:目前工件数 */
|
||||||
partCount?: number | null
|
partCount?: number | null
|
||||||
|
/** part_count_required:需求工件数 */
|
||||||
|
partCountRequired?: number | null
|
||||||
|
/** main_program:各轴群的当前加工号码 */
|
||||||
mainProgram?: string | null
|
mainProgram?: string | null
|
||||||
|
/** feedrate_act:轴群的实际进给速率 */
|
||||||
|
feedrateAct?: number | null
|
||||||
|
/** feedrate_override:轴群的实际进给倍率 */
|
||||||
|
feedrateOverride?: number | null
|
||||||
|
/** speed_act:轴群的加工主轴转速 */
|
||||||
|
speedAct?: number | null
|
||||||
|
/** speed_cmd:轴群的加工主轴转速命令 */
|
||||||
|
speedCmd?: number | null
|
||||||
|
/** load:轴群的加工主轴负载率 */
|
||||||
|
load?: number | null
|
||||||
|
/** speed_override:轴群的加工主轴速度百分比 */
|
||||||
|
speedOverride?: number | null
|
||||||
|
/** tool_id:各轴群的加工主轴刀号 */
|
||||||
|
toolId?: string | null
|
||||||
|
/** torque_load:轴向负载率 */
|
||||||
|
torqueLoad?: number | null
|
||||||
|
/** span_cutting_cycle:单件加工时间 */
|
||||||
|
spanCuttingCycle?: number | null
|
||||||
|
/** span_cutting_acc:累计加工时间 */
|
||||||
|
spanCuttingAcc?: number | null
|
||||||
|
/** span_power_on_acc:累计开机时间 */
|
||||||
|
spanPowerOnAcc?: number | null
|
||||||
|
/** span_cutting_install:安装累计加工时间 */
|
||||||
|
spanCuttingInstall?: number | null
|
||||||
|
/** span_power_on:开机时间 */
|
||||||
|
spanPowerOn?: number | null
|
||||||
|
/** alarm_current:警报相关 */
|
||||||
alarmCount?: number
|
alarmCount?: number
|
||||||
alarmMessages?: string[]
|
alarmMessages?: string[]
|
||||||
dataTimestamp?: number
|
dataTimestamp?: number
|
||||||
|
sectionId?: number | null
|
||||||
sectionName?: string
|
sectionName?: string
|
||||||
|
workshopId?: number | null
|
||||||
|
workshopName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -70,6 +70,9 @@ export interface ProcessPlanItemVO {
|
|||||||
|
|
||||||
workOrderCode?: string
|
workOrderCode?: string
|
||||||
|
|
||||||
|
/** 拆单来源工序计划ID */
|
||||||
|
splitFromId?: number | null
|
||||||
|
|
||||||
processCode?: string
|
processCode?: string
|
||||||
|
|
||||||
operNumber?: number
|
operNumber?: number
|
||||||
@ -150,6 +153,9 @@ export interface OrderProcessPlanVO {
|
|||||||
|
|
||||||
orderItemId?: number
|
orderItemId?: number
|
||||||
|
|
||||||
|
/** 工单号(列表按工单分组) */
|
||||||
|
workOrderCode?: string
|
||||||
|
|
||||||
mainCode?: string
|
mainCode?: string
|
||||||
|
|
||||||
materialCode?: string
|
materialCode?: string
|
||||||
@ -290,7 +296,7 @@ export function toOrderProcessPlanPayload(data: Record<string, unknown>): OrderP
|
|||||||
|
|
||||||
const displayKeys = [
|
const displayKeys = [
|
||||||
|
|
||||||
'mainCode', 'materialCode', 'materialName', 'orderCode', 'routeCode', 'workshopName',
|
'mainCode', 'materialCode', 'materialName', 'orderCode', 'routeCode', 'workshopName', 'workOrderCode',
|
||||||
|
|
||||||
'actualStartTime', 'actualEndTime', 'completedQty', 'orderStatus', 'currentProcessName',
|
'actualStartTime', 'actualEndTime', 'completedQty', 'orderStatus', 'currentProcessName',
|
||||||
|
|
||||||
@ -302,7 +308,7 @@ export function toOrderProcessPlanPayload(data: Record<string, unknown>): OrderP
|
|||||||
|
|
||||||
'transferOutQty', 'scrapQty', 'reworkQty', 'assign', 'assignByName', 'assignTime',
|
'transferOutQty', 'scrapQty', 'reworkQty', 'assign', 'assignByName', 'assignTime',
|
||||||
|
|
||||||
'processStatus', 'prevPlanId', 'nextPlanId', 'assignWorkList',
|
'processStatus', 'prevPlanId', 'nextPlanId', 'assignWorkList', 'splitFromId',
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -354,9 +360,9 @@ export const orderProcessPlanApi = {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 获取详情(路径参数为生产订单 id)
|
// 获取详情(路径参数为生产订单 id;可选 workOrderCode 隔离拆单工单)
|
||||||
|
|
||||||
detail(orderItemId: number | string) {
|
detail(orderItemId: number | string, workOrderCode?: string) {
|
||||||
|
|
||||||
return request<OrderProcessPlanVO>({
|
return request<OrderProcessPlanVO>({
|
||||||
|
|
||||||
@ -364,6 +370,8 @@ export const orderProcessPlanApi = {
|
|||||||
|
|
||||||
method: 'get',
|
method: 'get',
|
||||||
|
|
||||||
|
params: workOrderCode ? { workOrderCode } : undefined,
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
},
|
},
|
||||||
@ -493,6 +501,81 @@ export const orderProcessPlanApi = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** 工序工单拆单预览 */
|
||||||
|
splitPreview(planId: number | string) {
|
||||||
|
return request<ProcessPlanSplitPreview>({
|
||||||
|
url: `/biz/orderProcessPlan/split/preview/${planId}`,
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 工序工单拆单 */
|
||||||
|
split(data: ProcessPlanSplitPayload) {
|
||||||
|
return request<ProcessPlanSplitResult>({
|
||||||
|
url: '/biz/orderProcessPlan/split',
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拆单预览 */
|
||||||
|
export interface ProcessPlanSplitPreview {
|
||||||
|
planId?: number
|
||||||
|
workOrderCode?: string
|
||||||
|
orderItemId?: number
|
||||||
|
orderItemName?: string
|
||||||
|
/** 物料编码,用于查金蝶工艺路线 */
|
||||||
|
materialCode?: string
|
||||||
|
processCount?: number
|
||||||
|
processNames?: string[]
|
||||||
|
/** 工单计划数量 */
|
||||||
|
quantity?: number
|
||||||
|
/** 已进入生产数量(不可拆)= MAX(各工序投入) */
|
||||||
|
investedQty?: number
|
||||||
|
/** 可拆 = 计划 − 已进入生产数量 */
|
||||||
|
splittableQty?: number
|
||||||
|
canSplit?: boolean
|
||||||
|
reason?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拆单请求:quantities[0]=原单保留(≥不可拆),其余=新工单;新工艺二选一 */
|
||||||
|
export interface ProcessPlanSplitPayload {
|
||||||
|
planId: number
|
||||||
|
quantities: number[]
|
||||||
|
/** kingdee=金蝶工艺路线;assemble=工序表组装 */
|
||||||
|
routeMode: 'kingdee' | 'assemble'
|
||||||
|
/** routeMode=kingdee:金蝶工艺路线编码 FNumber */
|
||||||
|
routingNo?: string | null
|
||||||
|
/** routeMode=assemble:完整工序明细(推荐) */
|
||||||
|
assembleSteps?: ProcessPlanSplitStep[]
|
||||||
|
/** 兼容旧版:仅工序 id */
|
||||||
|
processIds?: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拆单组装工序行(对齐金蝶工艺路线主要字段) */
|
||||||
|
export interface ProcessPlanSplitStep {
|
||||||
|
processId?: number | null
|
||||||
|
operNumber?: number | null
|
||||||
|
processCode?: string | null
|
||||||
|
processName?: string | null
|
||||||
|
operDescription?: string | null
|
||||||
|
sectionId?: number | null
|
||||||
|
workshopId?: number | null
|
||||||
|
activityUnit?: string | null
|
||||||
|
optCtrlCodeName?: string | null
|
||||||
|
activityQty?: number | null
|
||||||
|
qualityInspection?: number | null
|
||||||
|
storageEntry?: number | null
|
||||||
|
procurement?: number | null
|
||||||
|
isOutsource?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拆单结果 */
|
||||||
|
export interface ProcessPlanSplitResult {
|
||||||
|
source?: ProcessPlanItemVO
|
||||||
|
created?: ProcessPlanItemVO[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 委外可选工序项 */
|
/** 委外可选工序项 */
|
||||||
|
|||||||
@ -88,5 +88,43 @@ export const processRouteApi = {
|
|||||||
// 下载导入模板
|
// 下载导入模板
|
||||||
downloadTemplate() {
|
downloadTemplate() {
|
||||||
return request({ url: `/biz/processRoute/template`, method: 'get', responseType: 'blob' })
|
return request({ url: `/biz/processRoute/template`, method: 'get', responseType: 'blob' })
|
||||||
}
|
},
|
||||||
|
|
||||||
|
/** 按物料编码查询金蝶工艺路线选项(含工艺路线编码) */
|
||||||
|
kingdeeSelect(materialCode: string) {
|
||||||
|
return request<ProcessRouteSelectOption[]>({
|
||||||
|
url: '/biz/processRoute/kingdee/select',
|
||||||
|
method: 'get',
|
||||||
|
params: { materialCode },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 金蝶工艺路线下拉选项 */
|
||||||
|
export interface ProcessRouteSelectOption {
|
||||||
|
routingNo?: string
|
||||||
|
materialCode?: string
|
||||||
|
materialName?: string
|
||||||
|
stepCount?: number
|
||||||
|
processNames?: string[]
|
||||||
|
/** 工序明细 */
|
||||||
|
steps?: KingdeeProcessRouteStep[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 金蝶工艺路线工序行 */
|
||||||
|
export interface KingdeeProcessRouteStep {
|
||||||
|
routingNo?: string
|
||||||
|
materialCode?: string
|
||||||
|
materialName?: string
|
||||||
|
operNumber?: number
|
||||||
|
processCode?: string
|
||||||
|
processName?: string
|
||||||
|
operDescription?: string
|
||||||
|
workCenterName?: string
|
||||||
|
departmentName?: string
|
||||||
|
optCtrlCodeName?: string
|
||||||
|
activityUnit?: string
|
||||||
|
activityQty?: number
|
||||||
|
qualityInspection?: number
|
||||||
|
storageEntry?: number
|
||||||
}
|
}
|
||||||
|
|||||||
@ -208,6 +208,13 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/views/biz/maintenanceRecord/index.vue'),
|
component: () => import('@/views/biz/maintenanceRecord/index.vue'),
|
||||||
meta: { title: '设备维修保养记录', icon: 'BuildOutline' }
|
meta: { title: '设备维修保养记录', icon: 'BuildOutline' }
|
||||||
},
|
},
|
||||||
|
// 设备状态监控(独立页)
|
||||||
|
{
|
||||||
|
path: 'biz/device/board',
|
||||||
|
name: 'deviceRealtimeBoard',
|
||||||
|
component: () => import('@/views/biz/device/board.vue'),
|
||||||
|
meta: { title: '设备状态监控', icon: 'PulseOutline' }
|
||||||
|
},
|
||||||
// 开发工具
|
// 开发工具
|
||||||
{
|
{
|
||||||
path: 'tool/gen',
|
path: 'tool/gen',
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -62,7 +62,6 @@
|
|||||||
<!-- 表头 -->
|
<!-- 表头 -->
|
||||||
<div class="board-table-head">
|
<div class="board-table-head">
|
||||||
<div class="col-check" />
|
<div class="col-check" />
|
||||||
<div class="col-expand" />
|
|
||||||
<div class="col-product">产品 / 工单编号</div>
|
<div class="col-product">产品 / 工单编号</div>
|
||||||
<div class="col-qty">数量</div>
|
<div class="col-qty">数量</div>
|
||||||
<div class="col-mode">模式</div>
|
<div class="col-mode">模式</div>
|
||||||
@ -80,29 +79,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="order in tableData" :key="orderRowKey(order)" class="order-block">
|
<div v-for="order in tableData" :key="orderRowKey(order)" class="order-block">
|
||||||
<div class="order-row" :class="{ expanded: isExpanded(order), 'derived-expanded': isDerivedExpanded(order) }">
|
<div class="order-row" :class="{ expanded: isExpanded(order) }">
|
||||||
<div class="col-check">
|
<div class="col-check">
|
||||||
<n-checkbox
|
<n-checkbox
|
||||||
:checked="selectedOrderIds.includes(order.orderItemId!)"
|
:checked="selectedOrderIds.includes(orderRowKey(order))"
|
||||||
@update:checked="(v: boolean) => toggleSelect(order.orderItemId!, v)"
|
@update:checked="(v: boolean) => toggleSelect(orderRowKey(order), v)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-expand">
|
|
||||||
<n-button
|
|
||||||
quaternary
|
|
||||||
circle
|
|
||||||
size="tiny"
|
|
||||||
title="展开返工单 / 拆分工单"
|
|
||||||
@click="toggleDerivedExpand(order)"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<n-icon>
|
|
||||||
<RemoveOutline v-if="isDerivedExpanded(order)" />
|
|
||||||
<AddOutline v-else />
|
|
||||||
</n-icon>
|
|
||||||
</template>
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
<div class="col-product">
|
<div class="col-product">
|
||||||
<div class="product-name">{{ order.materialName || '-' }}</div>
|
<div class="product-name">{{ order.materialName || '-' }}</div>
|
||||||
<div class="product-code">
|
<div class="product-code">
|
||||||
@ -153,43 +136,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isDerivedExpanded(order)" class="order-derived-expand">
|
|
||||||
<div class="derived-section">
|
|
||||||
<div class="derived-section-head">
|
|
||||||
<span class="derived-section-title">返工单</span>
|
|
||||||
<n-button text type="primary" size="tiny" @click="onMoreSelect('createRework', order)">创建返工单</n-button>
|
|
||||||
</div>
|
|
||||||
<div v-if="!getDerivedOrders(order).rework.length" class="derived-empty">暂无返工单</div>
|
|
||||||
<div v-else class="derived-list">
|
|
||||||
<div
|
|
||||||
v-for="item in getDerivedOrders(order).rework"
|
|
||||||
:key="item.id"
|
|
||||||
class="derived-item"
|
|
||||||
>
|
|
||||||
<span class="derived-code">{{ item.code }}</span>
|
|
||||||
<span class="derived-meta">{{ item.quantity ?? '-' }} · {{ item.statusLabel }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="derived-section">
|
|
||||||
<div class="derived-section-head">
|
|
||||||
<span class="derived-section-title">拆分工单</span>
|
|
||||||
<n-button text type="primary" size="tiny" @click="onMoreSelect('splitWorkOrder', order)">拆分工单</n-button>
|
|
||||||
</div>
|
|
||||||
<div v-if="!getDerivedOrders(order).split.length" class="derived-empty">暂无拆分工单</div>
|
|
||||||
<div v-else class="derived-list">
|
|
||||||
<div
|
|
||||||
v-for="item in getDerivedOrders(order).split"
|
|
||||||
:key="item.id"
|
|
||||||
class="derived-item"
|
|
||||||
>
|
|
||||||
<span class="derived-code">{{ item.code }}</span>
|
|
||||||
<span class="derived-meta">{{ item.quantity ?? '-' }} · {{ item.statusLabel }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="isExpanded(order)" class="order-expand">
|
<div v-if="isExpanded(order)" class="order-expand">
|
||||||
<div v-if="!(order.processList?.length)" class="expand-empty">暂无工序明细</div>
|
<div v-if="!(order.processList?.length)" class="expand-empty">暂无工序明细</div>
|
||||||
<div v-else class="card-flow-scroll">
|
<div v-else class="card-flow-scroll">
|
||||||
@ -221,8 +167,6 @@
|
|||||||
<!--模型抽屉 -->
|
<!--模型抽屉 -->
|
||||||
<PlmModelDrawer ref="plmModelDrawerRef" />
|
<PlmModelDrawer ref="plmModelDrawerRef" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="board-pagination">
|
<div class="board-pagination">
|
||||||
<n-pagination
|
<n-pagination
|
||||||
v-model:page="pagination.page"
|
v-model:page="pagination.page"
|
||||||
@ -407,6 +351,13 @@
|
|||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
|
||||||
|
<!-- 工序工单拆单(整单:全部工序一起按数量拆) -->
|
||||||
|
<SplitProcessDialog
|
||||||
|
v-model:show="splitVisible"
|
||||||
|
:plan-id="splitPlanId"
|
||||||
|
@success="onSplitSuccess"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 派工 -->
|
<!-- 派工 -->
|
||||||
<n-modal v-model:show="dispatchmodal" title="工序派工" preset="card" style="width: 800px" :mask-closable="false">
|
<n-modal v-model:show="dispatchmodal" title="工序派工" preset="card" style="width: 800px" :mask-closable="false">
|
||||||
<n-divider />
|
<n-divider />
|
||||||
@ -664,13 +615,13 @@ import { ref, reactive, onMounted, h, computed, type Component } from 'vue'
|
|||||||
import {
|
import {
|
||||||
NButton, NSpace, NIcon, NTag, NSpin, NModal, NForm, NFormItem, NGrid, NGi,
|
NButton, NSpace, NIcon, NTag, NSpin, NModal, NForm, NFormItem, NGrid, NGi,
|
||||||
NInput, NInputNumber, NDatePicker, NCheckbox, NEmpty, NPagination, NDropdown, NDescriptions,
|
NInput, NInputNumber, NDatePicker, NCheckbox, NEmpty, NPagination, NDropdown, NDescriptions,
|
||||||
NDescriptionsItem, NAlert, NUpload, NText, NCard, NRadioGroup, NRadio, useMessage, useDialog,
|
NDescriptionsItem, NAlert, NUpload, NText, NCard, NRadioGroup, NRadio, NDivider, useMessage, useDialog,
|
||||||
type UploadCustomRequestOptions, type FormRules,
|
type UploadCustomRequestOptions, type FormRules,
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import {
|
import {
|
||||||
AddOutline, RemoveOutline, SearchOutline, RefreshOutline, CloudUploadOutline,
|
AddOutline, SearchOutline, RefreshOutline, CloudUploadOutline,
|
||||||
DownloadOutline, ChevronDownOutline, CheckmarkCircleOutline, CheckmarkCircle,
|
DownloadOutline, ChevronDownOutline, CheckmarkCircleOutline, CheckmarkCircle,
|
||||||
EllipseOutline, TimeOutline, PrintOutline, SyncOutline, PeopleOutline,
|
EllipseOutline, TimeOutline, PrintOutline, PeopleOutline,
|
||||||
GitBranchOutline, CloseCircleOutline, LockClosedOutline, TrashOutline, EyeSharp,
|
GitBranchOutline, CloseCircleOutline, LockClosedOutline, TrashOutline, EyeSharp,
|
||||||
MenuOutline,
|
MenuOutline,
|
||||||
} from '@vicons/ionicons5'
|
} from '@vicons/ionicons5'
|
||||||
@ -688,6 +639,7 @@ import {
|
|||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import ProcessFlowStepper from './components/ProcessFlowStepper.vue'
|
import ProcessFlowStepper from './components/ProcessFlowStepper.vue'
|
||||||
import ProcessCard from './components/ProcessCard.vue'
|
import ProcessCard from './components/ProcessCard.vue'
|
||||||
|
import SplitProcessDialog from './components/SplitProcessDialog.vue'
|
||||||
import PlmModelDrawer from '@/components/PlmModelDrawer.vue'
|
import PlmModelDrawer from '@/components/PlmModelDrawer.vue'
|
||||||
import type { PlmModelOpenContext } from '@/api/plmModel'
|
import type { PlmModelOpenContext } from '@/api/plmModel'
|
||||||
import Pgitem from './pgitem.vue'
|
import Pgitem from './pgitem.vue'
|
||||||
@ -706,9 +658,8 @@ const searchForm = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const tableData = ref<OrderProcessPlanVO[]>([])
|
const tableData = ref<OrderProcessPlanVO[]>([])
|
||||||
const expandedIds = ref<Set<number>>(new Set())
|
const expandedIds = ref<Set<string>>(new Set())
|
||||||
const derivedExpandedIds = ref<Set<number>>(new Set())
|
const selectedOrderIds = ref<string[]>([])
|
||||||
const selectedOrderIds = ref<number[]>([])
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const pagination = reactive({ page: 1, pageSize: 10, itemCount: 0 })
|
const pagination = reactive({ page: 1, pageSize: 10, itemCount: 0 })
|
||||||
|
|
||||||
@ -716,6 +667,10 @@ const detailVisible = ref(false)
|
|||||||
const detailLoading = ref(false)
|
const detailLoading = ref(false)
|
||||||
const detailData = ref<OrderProcessPlanVO | null>(null)
|
const detailData = ref<OrderProcessPlanVO | null>(null)
|
||||||
|
|
||||||
|
/** 工序工单拆单 */
|
||||||
|
const splitVisible = ref(false)
|
||||||
|
const splitPlanId = ref<number | null>(null)
|
||||||
|
|
||||||
/** 订单级工序重排弹窗状态 */
|
/** 订单级工序重排弹窗状态 */
|
||||||
const reorderVisible = ref(false)
|
const reorderVisible = ref(false)
|
||||||
const reorderLoading = ref(false)
|
const reorderLoading = ref(false)
|
||||||
@ -835,7 +790,6 @@ const moreOptions = [
|
|||||||
label: '派生操作',
|
label: '派生操作',
|
||||||
key: 'group-derived',
|
key: 'group-derived',
|
||||||
children: [
|
children: [
|
||||||
{ label: '创建返工单', key: 'createRework', icon: dropdownIcon(SyncOutline) },
|
|
||||||
{ label: '创建工序委外', key: 'createOutsource', icon: dropdownIcon(PeopleOutline) },
|
{ label: '创建工序委外', key: 'createOutsource', icon: dropdownIcon(PeopleOutline) },
|
||||||
{ label: '拆分工单', key: 'splitWorkOrder', icon: dropdownIcon(GitBranchOutline) },
|
{ label: '拆分工单', key: 'splitWorkOrder', icon: dropdownIcon(GitBranchOutline) },
|
||||||
{ label: '汇报详情', key: 'reportDetailView', icon: dropdownIcon(EyeSharp) },
|
{ label: '汇报详情', key: 'reportDetailView', icon: dropdownIcon(EyeSharp) },
|
||||||
@ -860,57 +814,78 @@ const moreOptions = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/** 列表行主键:按工单号(拆单后同订单多工单分行) */
|
||||||
function orderRowKey(row: OrderProcessPlanVO) {
|
function orderRowKey(row: OrderProcessPlanVO) {
|
||||||
return row.orderItemId ?? `order-${row.mainCode}-${formatWorkOrderCodes(row)}`
|
if (row.workOrderCode) return row.workOrderCode
|
||||||
|
return `oi-${row.orderItemId ?? 'x'}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 汇总展示订单下各道工序的主工单号 */
|
/** 订单级工单号展示 */
|
||||||
function formatWorkOrderCodes(order?: OrderProcessPlanVO | null) {
|
function formatWorkOrderCodes(order?: OrderProcessPlanVO | null) {
|
||||||
const codes = (order?.processList ?? []).map((p) => p.workOrderCode).filter(Boolean)
|
if (order?.workOrderCode) return order.workOrderCode
|
||||||
return codes.length ? codes.join('、') : '-'
|
const codes = (order?.processList ?? [])
|
||||||
|
.map((p) => p.workOrderCode)
|
||||||
|
.filter((c): c is string => !!c)
|
||||||
|
const unique = [...new Set(codes)]
|
||||||
|
return unique.length ? unique.join('、') : '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortedProcessList(list?: ProcessPlanItemVO[]) {
|
function sortedProcessList(list?: ProcessPlanItemVO[]) {
|
||||||
return [...(list ?? [])].sort((a, b) => (a.operNumber ?? 0) - (b.operNumber ?? 0))
|
return [...(list ?? [])].sort((a, b) => {
|
||||||
|
const oa = a.operNumber ?? 0
|
||||||
|
const ob = b.operNumber ?? 0
|
||||||
|
if (oa !== ob) return oa - ob
|
||||||
|
return (a.planId ?? 0) - (b.planId ?? 0)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function isExpanded(order: OrderProcessPlanVO) {
|
function isExpanded(order: OrderProcessPlanVO) {
|
||||||
return order.orderItemId != null && expandedIds.value.has(order.orderItemId)
|
return expandedIds.value.has(orderRowKey(order))
|
||||||
}
|
|
||||||
|
|
||||||
function isDerivedExpanded(order: OrderProcessPlanVO) {
|
|
||||||
return order.orderItemId != null && derivedExpandedIds.value.has(order.orderItemId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleExpand(order: OrderProcessPlanVO) {
|
function toggleExpand(order: OrderProcessPlanVO) {
|
||||||
if (order.orderItemId == null) return
|
const key = orderRowKey(order)
|
||||||
const next = new Set(expandedIds.value)
|
const next = new Set(expandedIds.value)
|
||||||
if (next.has(order.orderItemId)) next.delete(order.orderItemId)
|
if (next.has(key)) next.delete(key)
|
||||||
else next.add(order.orderItemId)
|
else next.add(key)
|
||||||
expandedIds.value = next
|
expandedIds.value = next
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleDerivedExpand(order: OrderProcessPlanVO) {
|
/** 整单拆分入口:有计划数量且未全部完工即可点开 */
|
||||||
if (order.orderItemId == null) return
|
function canSplitWorkOrder(order: OrderProcessPlanVO) {
|
||||||
const next = new Set(derivedExpandedIds.value)
|
const qty = order.quantity ?? 0
|
||||||
if (next.has(order.orderItemId)) next.delete(order.orderItemId)
|
if (qty <= 0) return false
|
||||||
else next.add(order.orderItemId)
|
if (order.orderStatus === 2) return false
|
||||||
derivedExpandedIds.value = next
|
return (order.processList?.length ?? 0) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 派生工单(返工 / 拆分),待后端字段接入后替换占位逻辑 */
|
function openSplitFromOrder(order: OrderProcessPlanVO) {
|
||||||
function getDerivedOrders(order: OrderProcessPlanVO) {
|
if (!hasPermission('biz:orderProcessPlan:edit')) {
|
||||||
const raw = order as OrderProcessPlanVO & {
|
message.warning('无拆单权限')
|
||||||
reworkOrders?: Array<{ id: string | number; code?: string; quantity?: number; statusLabel?: string }>
|
return
|
||||||
splitOrders?: Array<{ id: string | number; code?: string; quantity?: number; statusLabel?: string }>
|
|
||||||
}
|
}
|
||||||
return {
|
const list = sortedProcessList(order.processList)
|
||||||
rework: raw.reworkOrders ?? [],
|
const seed = list.find((p) => p.planId != null)
|
||||||
split: raw.splitOrders ?? [],
|
if (!seed?.planId) {
|
||||||
|
message.warning('当前工单暂无工序,无法拆单')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!canSplitWorkOrder(order)) {
|
||||||
|
message.warning('该工单不可拆(无数量或已完工)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
splitPlanId.value = seed.planId
|
||||||
|
splitVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSplitSuccess() {
|
||||||
|
loadData()
|
||||||
|
if (detailVisible.value && detailData.value?.orderItemId) {
|
||||||
|
openDetail(detailData.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSelect(id: number, checked: boolean) {
|
function toggleSelect(id: string, checked: boolean) {
|
||||||
if (checked) selectedOrderIds.value = [...selectedOrderIds.value, id]
|
if (checked) selectedOrderIds.value = [...selectedOrderIds.value, id]
|
||||||
else selectedOrderIds.value = selectedOrderIds.value.filter(v => v !== id)
|
else selectedOrderIds.value = selectedOrderIds.value.filter(v => v !== id)
|
||||||
}
|
}
|
||||||
@ -982,7 +957,7 @@ function handlePageSizeChange(pageSize: number) {
|
|||||||
|
|
||||||
function onBatchSelect(key: string) {
|
function onBatchSelect(key: string) {
|
||||||
if (key === 'expandAll') {
|
if (key === 'expandAll') {
|
||||||
expandedIds.value = new Set(tableData.value.map(o => o.orderItemId!).filter(Boolean))
|
expandedIds.value = new Set(tableData.value.map(o => orderRowKey(o)))
|
||||||
} else if (key === 'collapseAll') {
|
} else if (key === 'collapseAll') {
|
||||||
expandedIds.value = new Set()
|
expandedIds.value = new Set()
|
||||||
}
|
}
|
||||||
@ -997,11 +972,13 @@ function onMoreSelect(key: string, order: OrderProcessPlanVO) {
|
|||||||
openReportDetail(order)
|
openReportDetail(order)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (key === 'splitWorkOrder') {
|
||||||
|
openSplitFromOrder(order)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
print: '打印',
|
print: '打印',
|
||||||
createRework: '创建返工单',
|
|
||||||
splitWorkOrder: '拆分工单',
|
|
||||||
withdraw: '撤回',
|
withdraw: '撤回',
|
||||||
freeze: '冻结工单',
|
freeze: '冻结工单',
|
||||||
delete: '删除',
|
delete: '删除',
|
||||||
@ -1117,7 +1094,7 @@ async function openDetail(order: OrderProcessPlanVO) {
|
|||||||
detailLoading.value = true
|
detailLoading.value = true
|
||||||
detailData.value = null
|
detailData.value = null
|
||||||
try {
|
try {
|
||||||
detailData.value = await orderProcessPlanApi.detail(order.orderItemId)
|
detailData.value = await orderProcessPlanApi.detail(order.orderItemId, order.workOrderCode)
|
||||||
} catch {
|
} catch {
|
||||||
detailData.value = order
|
detailData.value = order
|
||||||
} finally {
|
} finally {
|
||||||
@ -1137,12 +1114,12 @@ async function openReorder(order: OrderProcessPlanVO) {
|
|||||||
reorderOrderMeta.value = order
|
reorderOrderMeta.value = order
|
||||||
reorderList.value = []
|
reorderList.value = []
|
||||||
try {
|
try {
|
||||||
const detail = await orderProcessPlanApi.detail(order.orderItemId)
|
const detail = await orderProcessPlanApi.detail(order.orderItemId, order.workOrderCode)
|
||||||
reorderOrderMeta.value = detail
|
reorderOrderMeta.value = detail
|
||||||
// 与看板展开区一致:按 operNumber(后端 sort)升序
|
// 仅当前工单工序,按 operNumber 升序
|
||||||
reorderList.value = sortedProcessList(detail.processList).map((p) => ({ ...p }))
|
reorderList.value = sortedProcessList(detail.processList).map((p) => ({ ...p }))
|
||||||
if (!reorderList.value.length) {
|
if (!reorderList.value.length) {
|
||||||
message.warning('当前订单暂无工序计划')
|
message.warning('当前工单暂无工序计划')
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 详情失败时降级用列表行数据,仍可本地排序后提交
|
// 详情失败时降级用列表行数据,仍可本地排序后提交
|
||||||
@ -1393,9 +1370,8 @@ onMounted(loadData)
|
|||||||
.board-table-head,
|
.board-table-head,
|
||||||
.order-row {
|
.order-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
/* 勾选 | 展开 | 产品工单 | 数量 | 模式 | 齐套率 | 工序轴 | 计划时间 | 生命周期 | 操作 */
|
/* 勾选 | 产品工单 | 数量 | 模式 | 齐套率 | 工序轴 | 计划时间 | 生命周期 | 操作 */
|
||||||
grid-template-columns:
|
grid-template-columns:
|
||||||
40px
|
|
||||||
40px
|
40px
|
||||||
minmax(220px, 1.6fr)
|
minmax(220px, 1.6fr)
|
||||||
64px
|
64px
|
||||||
@ -1408,7 +1384,7 @@ onMounted(loadData)
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
min-width: 1320px;
|
min-width: 1280px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.board-table-head {
|
.board-table-head {
|
||||||
@ -1438,16 +1414,7 @@ onMounted(loadData)
|
|||||||
background: rgba(32, 128, 240, 0.04);
|
background: rgba(32, 128, 240, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-row.derived-expanded {
|
.col-check {
|
||||||
background: rgba(24, 160, 88, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-row.expanded.derived-expanded {
|
|
||||||
background: linear-gradient(90deg, rgba(24, 160, 88, 0.04) 0%, rgba(32, 128, 240, 0.04) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.col-check,
|
|
||||||
.col-expand {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@ -1530,74 +1497,11 @@ onMounted(loadData)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.order-expand {
|
.order-expand {
|
||||||
padding: 12px 16px 16px 68px;
|
padding: 12px 16px 16px 56px;
|
||||||
background: linear-gradient(180deg, rgba(32, 128, 240, 0.03) 0%, #fff 100%);
|
background: linear-gradient(180deg, rgba(32, 128, 240, 0.03) 0%, #fff 100%);
|
||||||
border-top: 1px dashed #e8eaed;
|
border-top: 1px dashed #e8eaed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-derived-expand {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 12px 16px 12px 68px;
|
|
||||||
background: linear-gradient(180deg, rgba(24, 160, 88, 0.03) 0%, #fff 100%);
|
|
||||||
border-top: 1px dashed #e8eaed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.derived-section {
|
|
||||||
min-width: 0;
|
|
||||||
padding: 10px 12px;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #eef0f3;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.derived-section-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.derived-section-title {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1a1a1a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.derived-empty {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #8c8c8c;
|
|
||||||
padding: 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.derived-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.derived-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
background: #fafbfc;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.derived-code {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.derived-meta {
|
|
||||||
color: #8c8c8c;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-flow-scroll,
|
.card-flow-scroll,
|
||||||
.detail-cards {
|
.detail-cards {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -79,6 +79,13 @@
|
|||||||
ghost
|
ghost
|
||||||
@click="emit('assign', process)"
|
@click="emit('assign', process)"
|
||||||
>派工</n-button>
|
>派工</n-button>
|
||||||
|
<n-button
|
||||||
|
v-if="canSplit"
|
||||||
|
size="tiny"
|
||||||
|
type="primary"
|
||||||
|
ghost
|
||||||
|
@click="emit('split', process)"
|
||||||
|
>拆单</n-button>
|
||||||
<n-button
|
<n-button
|
||||||
v-if="canEdit"
|
v-if="canEdit"
|
||||||
size="tiny"
|
size="tiny"
|
||||||
@ -86,7 +93,6 @@
|
|||||||
@click="emit('edit', process)"
|
@click="emit('edit', process)"
|
||||||
>编辑</n-button>
|
>编辑</n-button>
|
||||||
<n-button size="tiny" type="info" ghost @click="emit('viewModel', process)">模型</n-button>
|
<n-button size="tiny" type="info" ghost @click="emit('viewModel', process)">模型</n-button>
|
||||||
<n-button size="tiny" quaternary disabled>返工</n-button>
|
|
||||||
<n-button
|
<n-button
|
||||||
v-if="canOutsource"
|
v-if="canOutsource"
|
||||||
size="tiny"
|
size="tiny"
|
||||||
@ -135,6 +141,7 @@ const props = defineProps<{
|
|||||||
isLast?: boolean
|
isLast?: boolean
|
||||||
canEdit?: boolean
|
canEdit?: boolean
|
||||||
canAssign?: boolean
|
canAssign?: boolean
|
||||||
|
canSplit?: boolean
|
||||||
canOutsource?: boolean
|
canOutsource?: boolean
|
||||||
canSubmitDetail?: boolean
|
canSubmitDetail?: boolean
|
||||||
}>()
|
}>()
|
||||||
@ -142,6 +149,7 @@ const props = defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
edit: [process: ProcessPlanItemVO]
|
edit: [process: ProcessPlanItemVO]
|
||||||
assign: [process: ProcessPlanItemVO]
|
assign: [process: ProcessPlanItemVO]
|
||||||
|
split: [process: ProcessPlanItemVO]
|
||||||
viewModel: [process: ProcessPlanItemVO]
|
viewModel: [process: ProcessPlanItemVO]
|
||||||
outsource: [process: ProcessPlanItemVO]
|
outsource: [process: ProcessPlanItemVO]
|
||||||
submitDetail: [process: ProcessPlanItemVO]
|
submitDetail: [process: ProcessPlanItemVO]
|
||||||
|
|||||||
@ -39,7 +39,12 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const steps = computed(() => {
|
const steps = computed(() => {
|
||||||
const list = [...(props.list ?? [])].sort((a, b) => (a.operNumber ?? 0) - (b.operNumber ?? 0))
|
const list = [...(props.list ?? [])].sort((a, b) => {
|
||||||
|
const oa = a.operNumber ?? 0
|
||||||
|
const ob = b.operNumber ?? 0
|
||||||
|
if (oa !== ob) return oa - ob
|
||||||
|
return (a.planId ?? 0) - (b.planId ?? 0)
|
||||||
|
})
|
||||||
return list.map((item, idx) => {
|
return list.map((item, idx) => {
|
||||||
const label = item.processName || item.processCode || `工序${idx + 1}`
|
const label = item.processName || item.processCode || `工序${idx + 1}`
|
||||||
const state = item.processStatus === 4 ? 'done' : item.processStatus != null && item.processStatus >= 2 ? 'active' : 'pending'
|
const state = item.processStatus === 4 ? 'done' : item.processStatus != null && item.processStatus >= 2 ? 'active' : 'pending'
|
||||||
|
|||||||
929
src/views/biz/orderProcessPlan/components/SplitProcessDialog.vue
Normal file
929
src/views/biz/orderProcessPlan/components/SplitProcessDialog.vue
Normal file
@ -0,0 +1,929 @@
|
|||||||
|
<template>
|
||||||
|
<n-modal
|
||||||
|
v-model:show="visible"
|
||||||
|
preset="card"
|
||||||
|
title="拆分工单"
|
||||||
|
:style="{ width: '1120px' }"
|
||||||
|
:content-style="{ paddingTop: '8px', paddingBottom: '4px' }"
|
||||||
|
:mask-closable="false"
|
||||||
|
@after-leave="resetState"
|
||||||
|
>
|
||||||
|
<n-spin :show="loading">
|
||||||
|
<div class="split-scroll">
|
||||||
|
<n-descriptions v-if="preview" :column="3" label-placement="left" size="small" class="split-desc">
|
||||||
|
<n-descriptions-item label="原工单编号">{{ preview.workOrderCode || '-' }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="物料编码">{{ preview.materialCode || '-' }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="物料/订单">{{ preview.orderItemName || '-' }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="计划数量">{{ preview.quantity ?? 0 }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="已进入生产">{{ preview.investedQty ?? 0 }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="可拆数量">{{ preview.splittableQty ?? 0 }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="原工序" :span="3">
|
||||||
|
{{ preview.processCount ?? 0 }} 道
|
||||||
|
<span v-if="preview.processNames?.length" class="process-names">
|
||||||
|
({{ preview.processNames.join('、') }})
|
||||||
|
</span>
|
||||||
|
</n-descriptions-item>
|
||||||
|
</n-descriptions>
|
||||||
|
|
||||||
|
<n-alert v-if="preview && !preview.canSplit" type="warning" :bordered="false" style="margin: 8px 0">
|
||||||
|
{{ preview.reason || '当前工单不允许拆单' }}
|
||||||
|
</n-alert>
|
||||||
|
|
||||||
|
<template v-if="preview?.canSplit">
|
||||||
|
<div class="split-section">
|
||||||
|
<div class="split-section-title">数量分配</div>
|
||||||
|
<div class="field-row">
|
||||||
|
<span class="field-label-inline">拆分方式</span>
|
||||||
|
<n-radio-group v-model:value="mode" name="splitMode" size="small">
|
||||||
|
<n-space>
|
||||||
|
<n-radio value="equal">等量拆分</n-radio>
|
||||||
|
<n-radio value="custom">指定数量</n-radio>
|
||||||
|
</n-space>
|
||||||
|
</n-radio-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 等量:按总份数均分计划数量,第1份留原单,其余新建 -->
|
||||||
|
<div v-if="mode === 'equal'" class="split-body">
|
||||||
|
<div class="field-row">
|
||||||
|
<span class="field-label-inline">拆成份数</span>
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="equalCount"
|
||||||
|
:min="2"
|
||||||
|
:max="Math.max(2, maxEqualParts)"
|
||||||
|
:precision="0"
|
||||||
|
size="small"
|
||||||
|
style="width: 140px"
|
||||||
|
/>
|
||||||
|
<span class="hint-inline">含原工单,第1份留原单</span>
|
||||||
|
</div>
|
||||||
|
<div class="hint-inline" style="margin-top: 6px">{{ equalHint }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 指定:手动设原单保留 + 各新工单数量 -->
|
||||||
|
<div v-else class="split-body">
|
||||||
|
<div class="field-row">
|
||||||
|
<span class="field-label-inline">原单保留</span>
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="keepQty"
|
||||||
|
:min="unsplittableQty"
|
||||||
|
:max="Math.max(unsplittableQty, (preview.quantity ?? 0) - 1)"
|
||||||
|
:precision="0"
|
||||||
|
size="small"
|
||||||
|
style="width: 140px"
|
||||||
|
@update:value="onKeepQtyUpdate"
|
||||||
|
/>
|
||||||
|
<span class="hint-inline">≥不可拆 {{ unsplittableQty }},拆出 {{ splitOutQty }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-for="(row, idx) in customRows" :key="idx" class="custom-row">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="customRows[idx]"
|
||||||
|
:min="1"
|
||||||
|
:precision="0"
|
||||||
|
size="small"
|
||||||
|
placeholder="新工单数量"
|
||||||
|
style="flex: 1"
|
||||||
|
/>
|
||||||
|
<n-button text type="error" :disabled="customRows.length <= 1" @click="removeCustomRow(idx)">
|
||||||
|
删除
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<n-button dashed block size="small" style="margin-top: 6px" @click="addCustomRow">+ 添加工单</n-button>
|
||||||
|
<div class="sum-line" :class="{ error: customSum !== splitOutQty }">
|
||||||
|
拆出合计: {{ customSum }} / {{ splitOutQty }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="split-section">
|
||||||
|
<div class="split-section-title">
|
||||||
|
新工单工艺路线
|
||||||
|
<n-radio-group
|
||||||
|
v-model:value="routeMode"
|
||||||
|
name="routeMode"
|
||||||
|
size="small"
|
||||||
|
style="margin-left: 12px; font-weight: 400"
|
||||||
|
>
|
||||||
|
<n-space>
|
||||||
|
<n-radio value="kingdee">金蝶工艺路线</n-radio>
|
||||||
|
<n-radio value="assemble">工序表组装</n-radio>
|
||||||
|
</n-space>
|
||||||
|
</n-radio-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="routeMode === 'kingdee'" class="split-body">
|
||||||
|
<div v-if="!preview?.materialCode" class="hint">当前订单无物料编码,无法查询金蝶工艺路线</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="kingdee-layout">
|
||||||
|
<div class="kingdee-left">
|
||||||
|
<div class="field-label">路线列表({{ routeList.length }})</div>
|
||||||
|
<n-spin :show="routeLoading">
|
||||||
|
<div v-if="!routeLoading && routeList.length === 0" class="hint">未查询到工艺路线</div>
|
||||||
|
<div v-else class="route-card-list">
|
||||||
|
<div
|
||||||
|
v-for="route in routeList"
|
||||||
|
:key="route.routingNo"
|
||||||
|
class="route-card"
|
||||||
|
:class="{ active: routingNo === route.routingNo }"
|
||||||
|
@click="routingNo = route.routingNo || null"
|
||||||
|
>
|
||||||
|
<div class="route-card-head">
|
||||||
|
<span class="route-code">{{ route.routingNo }}</span>
|
||||||
|
<span class="route-step-count">{{ route.stepCount ?? route.steps?.length ?? 0 }}道</span>
|
||||||
|
</div>
|
||||||
|
<div class="route-flow">
|
||||||
|
{{ (route.processNames || []).join(' → ') || '暂无工序' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-spin>
|
||||||
|
</div>
|
||||||
|
<div class="kingdee-right">
|
||||||
|
<div class="field-label">
|
||||||
|
工序明细
|
||||||
|
<span v-if="selectedRoute" class="route-detail-sub">
|
||||||
|
{{ selectedRoute.routingNo }} · {{ selectedSteps.length }} 道
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<n-data-table
|
||||||
|
v-if="selectedRoute"
|
||||||
|
size="small"
|
||||||
|
:bordered="true"
|
||||||
|
:single-line="true"
|
||||||
|
:columns="kingdeeStepColumns"
|
||||||
|
:data="selectedSteps"
|
||||||
|
:scroll-x="900"
|
||||||
|
:max-height="260"
|
||||||
|
:pagination="false"
|
||||||
|
/>
|
||||||
|
<div v-else class="route-detail-empty">点击左侧路线查看工序</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="split-body">
|
||||||
|
<div class="assemble-toolbar">
|
||||||
|
<span class="hint-inline">按新增工艺路线方式逐行维护;说明为空时默认用工序名称</span>
|
||||||
|
<n-button size="small" type="primary" secondary @click="addAssembleRow">+ 添加工序</n-button>
|
||||||
|
</div>
|
||||||
|
<n-data-table
|
||||||
|
size="small"
|
||||||
|
:bordered="true"
|
||||||
|
:single-line="false"
|
||||||
|
:columns="assembleColumns"
|
||||||
|
:data="assembleRows"
|
||||||
|
:scroll-x="1080"
|
||||||
|
:max-height="280"
|
||||||
|
:pagination="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</n-spin>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<n-space justify="end">
|
||||||
|
<n-button @click="visible = false">取消</n-button>
|
||||||
|
<n-button type="primary" :loading="submitting" :disabled="!canConfirm" @click="confirmSplit">
|
||||||
|
确认拆分
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 拆单弹窗:
|
||||||
|
* - 已进入生产数量(不可拆)留原单;只拆可拆数量
|
||||||
|
* - 等量拆分:按「拆成份数」均分计划数量,第1份留原单,其余新建(如 30÷3 → 原10 + 新10 + 新10)
|
||||||
|
* - 指定数量:手动设原单保留 + 各新工单数量
|
||||||
|
* - 金蝶工艺路线 / 工序表组装(对齐工艺路线字段)
|
||||||
|
*/
|
||||||
|
import { computed, h, ref, watch } from 'vue'
|
||||||
|
import {
|
||||||
|
NAlert,
|
||||||
|
NButton,
|
||||||
|
NDataTable,
|
||||||
|
NDescriptions,
|
||||||
|
NDescriptionsItem,
|
||||||
|
NInput,
|
||||||
|
NInputNumber,
|
||||||
|
NModal,
|
||||||
|
NRadio,
|
||||||
|
NRadioGroup,
|
||||||
|
NSelect,
|
||||||
|
NSpace,
|
||||||
|
NSpin,
|
||||||
|
NSwitch,
|
||||||
|
useMessage,
|
||||||
|
type DataTableColumns,
|
||||||
|
type SelectOption,
|
||||||
|
} from 'naive-ui'
|
||||||
|
import {
|
||||||
|
orderProcessPlanApi,
|
||||||
|
type ProcessPlanSplitPreview,
|
||||||
|
type ProcessPlanSplitStep,
|
||||||
|
} from '@/api/orderProcessPlan'
|
||||||
|
import {
|
||||||
|
processRouteApi,
|
||||||
|
type KingdeeProcessRouteStep,
|
||||||
|
type ProcessRouteSelectOption,
|
||||||
|
} from '@/api/processRoute'
|
||||||
|
import { basicProcessPlanApi, type BasicProcessPlan } from '@/api/basicProcessPlan'
|
||||||
|
|
||||||
|
interface AssembleRow extends ProcessPlanSplitStep {
|
||||||
|
_key: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
show: boolean
|
||||||
|
planId: number | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:show': [value: boolean]
|
||||||
|
success: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const loading = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const preview = ref<ProcessPlanSplitPreview | null>(null)
|
||||||
|
|
||||||
|
const mode = ref<'equal' | 'custom'>('equal')
|
||||||
|
const keepQty = ref(0)
|
||||||
|
/** 等量拆分:总份数(含原单),至少 2 = 原单 + 1 新单 */
|
||||||
|
const equalCount = ref(2)
|
||||||
|
const customRows = ref<(number | null)[]>([null])
|
||||||
|
|
||||||
|
const routeMode = ref<'kingdee' | 'assemble'>('kingdee')
|
||||||
|
const routingNo = ref<string | null>(null)
|
||||||
|
const routeList = ref<ProcessRouteSelectOption[]>([])
|
||||||
|
const routeLoading = ref(false)
|
||||||
|
const processLoading = ref(false)
|
||||||
|
|
||||||
|
const processOptions = ref<SelectOption[]>([])
|
||||||
|
const processMap = ref<Record<number, BasicProcessPlan>>({})
|
||||||
|
const assembleRows = ref<AssembleRow[]>([])
|
||||||
|
const ctrlCodeOptions: SelectOption[] = [
|
||||||
|
{ label: '汇报+免检', value: '汇报+免检' },
|
||||||
|
{ label: '汇报+质量', value: '汇报+质量' },
|
||||||
|
{ label: '委外+质量', value: '委外+质量' },
|
||||||
|
{ label: '委外+汇报', value: '委外+汇报' },
|
||||||
|
]
|
||||||
|
const activityUnitOptions = ref<SelectOption[]>([
|
||||||
|
{ label: '小时', value: '小时' },
|
||||||
|
{ label: '分钟', value: '分钟' },
|
||||||
|
{ label: '秒', value: '秒' },
|
||||||
|
])
|
||||||
|
|
||||||
|
const visible = computed({
|
||||||
|
get: () => props.show,
|
||||||
|
set: (v: boolean) => emit('update:show', v),
|
||||||
|
})
|
||||||
|
|
||||||
|
const unsplittableQty = computed(() => preview.value?.investedQty ?? 0)
|
||||||
|
const planQty = computed(() => preview.value?.quantity ?? 0)
|
||||||
|
const splitOutQty = computed(() => Math.max(planQty.value - (keepQty.value ?? 0), 0))
|
||||||
|
|
||||||
|
function onKeepQtyUpdate(v: number | null) {
|
||||||
|
const min = unsplittableQty.value
|
||||||
|
const max = Math.max(min, (preview.value?.quantity ?? 0) - 1)
|
||||||
|
if (v == null || v < min) {
|
||||||
|
keepQty.value = min
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (v > max) keepQty.value = max
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等量拆分:把整单计划数量均分成 N 份(含原单)。
|
||||||
|
* quantities = [原单保留, 新单1, 新单2, ...]
|
||||||
|
*/
|
||||||
|
const equalQuantities = computed(() => {
|
||||||
|
const q = planQty.value
|
||||||
|
const n = equalCount.value
|
||||||
|
if (!q || !n || n < 2 || q < n) return [] as number[]
|
||||||
|
const base = Math.floor(q / n)
|
||||||
|
const rem = q % n
|
||||||
|
const list = Array.from({ length: n }, () => base)
|
||||||
|
// 余数加在最后一份,避免中间份变成 0
|
||||||
|
list[n - 1] = base + rem
|
||||||
|
if (list.some((v) => v <= 0)) return []
|
||||||
|
// 第1份必须盖住不可拆(已进入生产)数量
|
||||||
|
if (list[0] < unsplittableQty.value) return []
|
||||||
|
return list
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 在「第1份 ≥ 不可拆」前提下,最多能拆成几份 */
|
||||||
|
const maxEqualParts = computed(() => {
|
||||||
|
const q = planQty.value
|
||||||
|
const inv = unsplittableQty.value
|
||||||
|
if (q < 2) return 2
|
||||||
|
let max = Math.min(q, Math.max(2, q - inv)) // 至少留 1 给某新单时可拆上限粗估
|
||||||
|
// 收紧:floor(q/n) >= inv(余数在末份时首份=floor)
|
||||||
|
while (max > 2 && Math.floor(q / max) < inv) {
|
||||||
|
max -= 1
|
||||||
|
}
|
||||||
|
// 还要保证至少能拆出 1 个新单:首份 < q
|
||||||
|
while (max > 2) {
|
||||||
|
const base = Math.floor(q / max)
|
||||||
|
if (base >= Math.max(1, inv) && base < q) break
|
||||||
|
max -= 1
|
||||||
|
}
|
||||||
|
return Math.max(2, max)
|
||||||
|
})
|
||||||
|
|
||||||
|
const equalHint = computed(() => {
|
||||||
|
const list = equalQuantities.value
|
||||||
|
if (!list.length) {
|
||||||
|
if (planQty.value < equalCount.value) return `计划数量不足以分成 ${equalCount.value} 份`
|
||||||
|
if (equalCount.value >= 2 && Math.floor(planQty.value / equalCount.value) < unsplittableQty.value) {
|
||||||
|
return `第1份无法覆盖不可拆数量 ${unsplittableQty.value},请减少份数`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const keep = list[0]
|
||||||
|
const news = list.slice(1)
|
||||||
|
const newCount = news.length
|
||||||
|
if (news.every((v) => v === news[0])) {
|
||||||
|
return `结果:原单保留 ${keep},新建 ${newCount} 张各 ${news[0]}(共 ${list.length} 张)`
|
||||||
|
}
|
||||||
|
return `结果:原单保留 ${keep},新建 ${newCount} 张(${news.join('、')})`
|
||||||
|
})
|
||||||
|
|
||||||
|
const customSum = computed(() =>
|
||||||
|
customRows.value.reduce((s, v) => s + (typeof v === 'number' && v > 0 ? v : 0), 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectedRoute = computed(() =>
|
||||||
|
routingNo.value ? routeList.value.find((r) => r.routingNo === routingNo.value) ?? null : null,
|
||||||
|
)
|
||||||
|
const selectedSteps = computed<KingdeeProcessRouteStep[]>(() => selectedRoute.value?.steps ?? [])
|
||||||
|
|
||||||
|
const kingdeeStepColumns: DataTableColumns<KingdeeProcessRouteStep> = [
|
||||||
|
{ title: '工序号', key: 'operNumber', width: 70, render: (r) => r.operNumber ?? '-' },
|
||||||
|
{ title: '工序编码', key: 'processCode', width: 110, ellipsis: { tooltip: true }, render: (r) => r.processCode || '-' },
|
||||||
|
{ title: '工序名称', key: 'processName', width: 120, ellipsis: { tooltip: true }, render: (r) => r.processName || '-' },
|
||||||
|
{ title: '工序说明', key: 'operDescription', minWidth: 140, ellipsis: { tooltip: true }, render: (r) => r.operDescription || '-' },
|
||||||
|
{ title: '工作中心', key: 'workCenterName', width: 110, ellipsis: { tooltip: true }, render: (r) => r.workCenterName || '-' },
|
||||||
|
{ title: '车间', key: 'departmentName', width: 100, ellipsis: { tooltip: true }, render: (r) => r.departmentName || '-' },
|
||||||
|
{ title: '控制码', key: 'optCtrlCodeName', width: 100, ellipsis: { tooltip: true }, render: (r) => r.optCtrlCodeName || '-' },
|
||||||
|
{
|
||||||
|
title: '标准工时',
|
||||||
|
key: 'activityQty',
|
||||||
|
width: 100,
|
||||||
|
render: (r) => (r.activityQty == null ? '-' : `${r.activityQty}${r.activityUnit ? ' ' + r.activityUnit : ''}`),
|
||||||
|
},
|
||||||
|
{ title: '质检', key: 'qualityInspection', width: 56, render: (r) => (r.qualityInspection === 1 ? '是' : '否') },
|
||||||
|
{ title: '入库', key: 'storageEntry', width: 56, render: (r) => (r.storageEntry === 1 ? '是' : '否') },
|
||||||
|
]
|
||||||
|
|
||||||
|
function createEmptyAssembleRow(operNumber = 10): AssembleRow {
|
||||||
|
return {
|
||||||
|
_key: `a-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||||
|
processId: null,
|
||||||
|
operNumber,
|
||||||
|
processCode: null,
|
||||||
|
processName: null,
|
||||||
|
operDescription: null,
|
||||||
|
sectionId: null,
|
||||||
|
workshopId: null,
|
||||||
|
activityUnit: '小时',
|
||||||
|
optCtrlCodeName: null,
|
||||||
|
activityQty: null,
|
||||||
|
qualityInspection: 0,
|
||||||
|
storageEntry: 0,
|
||||||
|
procurement: 0,
|
||||||
|
isOutsource: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renumberAssembleRows() {
|
||||||
|
assembleRows.value.forEach((row, i) => {
|
||||||
|
row.operNumber = (i + 1) * 10
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAssembleRow() {
|
||||||
|
assembleRows.value.push(createEmptyAssembleRow((assembleRows.value.length + 1) * 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAssembleRow(key: string) {
|
||||||
|
if (assembleRows.value.length <= 1) return
|
||||||
|
assembleRows.value = assembleRows.value.filter((r) => r._key !== key)
|
||||||
|
renumberAssembleRows()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAssembleProcessChange(row: AssembleRow, processId: number | null) {
|
||||||
|
row.processId = processId
|
||||||
|
if (processId == null) {
|
||||||
|
row.processCode = null
|
||||||
|
row.processName = null
|
||||||
|
row.operDescription = null
|
||||||
|
row.sectionId = null
|
||||||
|
row.workshopId = null
|
||||||
|
row.qualityInspection = 0
|
||||||
|
row.storageEntry = 0
|
||||||
|
row.procurement = 0
|
||||||
|
row.isOutsource = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const basic = processMap.value[processId]
|
||||||
|
if (!basic) return
|
||||||
|
row.processCode = basic.code || null
|
||||||
|
row.processName = basic.name || null
|
||||||
|
if (!row.operDescription) {
|
||||||
|
row.operDescription = basic.operDescription || basic.name || null
|
||||||
|
}
|
||||||
|
row.sectionId = basic.sectionId ?? null
|
||||||
|
row.workshopId = basic.workShopId ?? null
|
||||||
|
row.qualityInspection = basic.qualityInspection ?? 0
|
||||||
|
row.storageEntry = basic.storageEntry ?? 0
|
||||||
|
row.procurement = basic.procurement ?? 0
|
||||||
|
row.isOutsource = basic.outsource ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCtrlCodeChange(row: AssembleRow, val: string | null) {
|
||||||
|
row.optCtrlCodeName = val
|
||||||
|
if (!val) return
|
||||||
|
// 含「质量」→ 质检;含「委外」→ 委外;「免检」→ 不质检
|
||||||
|
row.qualityInspection = val.includes('质量') ? 1 : 0
|
||||||
|
row.isOutsource = val.includes('委外') ? 1 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const assembleColumns = computed<DataTableColumns<AssembleRow>>(() => [
|
||||||
|
{
|
||||||
|
title: '工序号',
|
||||||
|
key: 'operNumber',
|
||||||
|
width: 64,
|
||||||
|
render: (row) => row.operNumber ?? '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '工序',
|
||||||
|
key: 'processId',
|
||||||
|
width: 180,
|
||||||
|
render: (row) =>
|
||||||
|
h(NSelect, {
|
||||||
|
value: row.processId,
|
||||||
|
size: 'small',
|
||||||
|
filterable: true,
|
||||||
|
clearable: true,
|
||||||
|
options: processOptions.value,
|
||||||
|
loading: processLoading.value,
|
||||||
|
placeholder: '选择工序',
|
||||||
|
style: 'width: 100%',
|
||||||
|
onUpdateValue: (v: number | null) => onAssembleProcessChange(row, v),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '工序说明',
|
||||||
|
key: 'operDescription',
|
||||||
|
width: 150,
|
||||||
|
render: (row) =>
|
||||||
|
h(NInput, {
|
||||||
|
value: row.operDescription ?? '',
|
||||||
|
size: 'small',
|
||||||
|
clearable: true,
|
||||||
|
placeholder: row.processName || '默认工序名称',
|
||||||
|
onUpdateValue: (v: string) => {
|
||||||
|
row.operDescription = v
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '标准工时',
|
||||||
|
key: 'activityQty',
|
||||||
|
width: 100,
|
||||||
|
render: (row) =>
|
||||||
|
h(NInputNumber, {
|
||||||
|
value: row.activityQty,
|
||||||
|
size: 'small',
|
||||||
|
min: 0,
|
||||||
|
precision: 3,
|
||||||
|
style: 'width: 90px',
|
||||||
|
placeholder: '工时',
|
||||||
|
onUpdateValue: (v: number | null) => {
|
||||||
|
row.activityQty = v
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '单位',
|
||||||
|
key: 'activityUnit',
|
||||||
|
width: 100,
|
||||||
|
render: (row) =>
|
||||||
|
h(NSelect, {
|
||||||
|
value: row.activityUnit,
|
||||||
|
size: 'small',
|
||||||
|
filterable: true,
|
||||||
|
tag: true,
|
||||||
|
options: activityUnitOptions.value,
|
||||||
|
placeholder: '单位',
|
||||||
|
style: 'width: 100%',
|
||||||
|
onUpdateValue: (v: string | null) => {
|
||||||
|
row.activityUnit = v
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '控制码',
|
||||||
|
key: 'optCtrlCodeName',
|
||||||
|
width: 140,
|
||||||
|
render: (row) =>
|
||||||
|
h(NSelect, {
|
||||||
|
value: row.optCtrlCodeName,
|
||||||
|
size: 'small',
|
||||||
|
filterable: true,
|
||||||
|
clearable: true,
|
||||||
|
options: ctrlCodeOptions,
|
||||||
|
placeholder: '选择控制码',
|
||||||
|
style: 'width: 100%',
|
||||||
|
onUpdateValue: (v: string | null) => onCtrlCodeChange(row, v),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '质检',
|
||||||
|
key: 'qualityInspection',
|
||||||
|
width: 70,
|
||||||
|
render: (row) =>
|
||||||
|
h(NSwitch, {
|
||||||
|
value: row.qualityInspection === 1,
|
||||||
|
size: 'small',
|
||||||
|
onUpdateValue: (v: boolean) => {
|
||||||
|
row.qualityInspection = v ? 1 : 0
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '入库',
|
||||||
|
key: 'storageEntry',
|
||||||
|
width: 70,
|
||||||
|
render: (row) =>
|
||||||
|
h(NSwitch, {
|
||||||
|
value: row.storageEntry === 1,
|
||||||
|
size: 'small',
|
||||||
|
onUpdateValue: (v: boolean) => {
|
||||||
|
row.storageEntry = v ? 1 : 0
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'actions',
|
||||||
|
width: 64,
|
||||||
|
fixed: 'right',
|
||||||
|
render: (row) =>
|
||||||
|
h(
|
||||||
|
NButton,
|
||||||
|
{
|
||||||
|
text: true,
|
||||||
|
type: 'error',
|
||||||
|
size: 'small',
|
||||||
|
disabled: assembleRows.value.length <= 1,
|
||||||
|
onClick: () => removeAssembleRow(row._key),
|
||||||
|
},
|
||||||
|
{ default: () => '删除' },
|
||||||
|
),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const assembleValid = computed(() => {
|
||||||
|
if (!assembleRows.value.length) return false
|
||||||
|
return assembleRows.value.every((r) => r.processId != null || !!r.processName || !!r.processCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
const canConfirm = computed(() => {
|
||||||
|
if (!preview.value?.canSplit || submitting.value) return false
|
||||||
|
|
||||||
|
if (mode.value === 'equal') {
|
||||||
|
// 等量:整单均分,列表本身已含原单保留
|
||||||
|
if (!equalQuantities.value.length) return false
|
||||||
|
} else {
|
||||||
|
if (keepQty.value == null || keepQty.value < unsplittableQty.value) return false
|
||||||
|
if (splitOutQty.value <= 0) return false
|
||||||
|
if (customRows.value.length < 1) return false
|
||||||
|
if (customRows.value.some((v) => v == null || v <= 0)) return false
|
||||||
|
if (customSum.value !== splitOutQty.value) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (routeMode.value === 'kingdee') return !!routingNo.value
|
||||||
|
return assembleValid.value
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.show, props.planId] as const,
|
||||||
|
async ([show, planId]) => {
|
||||||
|
if (!show || planId == null) return
|
||||||
|
await loadPreview(planId)
|
||||||
|
await Promise.all([
|
||||||
|
loadRouteOptions(preview.value?.materialCode),
|
||||||
|
loadProcessOptions(),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(keepQty, () => {
|
||||||
|
if (mode.value === 'custom') {
|
||||||
|
customRows.value = [splitOutQty.value > 0 ? splitOutQty.value : null]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadPreview(planId: number) {
|
||||||
|
loading.value = true
|
||||||
|
preview.value = null
|
||||||
|
try {
|
||||||
|
preview.value = await orderProcessPlanApi.splitPreview(planId)
|
||||||
|
const unsplittable = preview.value?.investedQty ?? 0
|
||||||
|
keepQty.value = unsplittable
|
||||||
|
// 默认拆成 2 份(原单 + 1 新单);数量够时也可改成更多
|
||||||
|
equalCount.value = 2
|
||||||
|
customRows.value = [preview.value?.splittableQty ?? null]
|
||||||
|
mode.value = 'equal'
|
||||||
|
routeMode.value = 'kingdee'
|
||||||
|
routingNo.value = null
|
||||||
|
assembleRows.value = [createEmptyAssembleRow(10)]
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.message || '加载拆单预览失败')
|
||||||
|
visible.value = false
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRouteOptions(materialCode?: string | null) {
|
||||||
|
routeLoading.value = true
|
||||||
|
routeList.value = []
|
||||||
|
routingNo.value = null
|
||||||
|
try {
|
||||||
|
if (!materialCode) return
|
||||||
|
const list = (await processRouteApi.kingdeeSelect(materialCode)) as ProcessRouteSelectOption[]
|
||||||
|
routeList.value = (Array.isArray(list) ? list : []).filter((r) => !!r?.routingNo)
|
||||||
|
} catch (e: any) {
|
||||||
|
routeList.value = []
|
||||||
|
message.error(e?.message || '加载金蝶工艺路线失败')
|
||||||
|
} finally {
|
||||||
|
routeLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadProcessOptions() {
|
||||||
|
processLoading.value = true
|
||||||
|
try {
|
||||||
|
const page: any = await basicProcessPlanApi.page({ page: 1, pageSize: 500 })
|
||||||
|
const rows: BasicProcessPlan[] = page?.records ?? page?.list ?? (Array.isArray(page) ? page : [])
|
||||||
|
const map: Record<number, BasicProcessPlan> = {}
|
||||||
|
processOptions.value = rows
|
||||||
|
.filter((r) => r?.id != null)
|
||||||
|
.map((r) => {
|
||||||
|
const id = Number(r.id)
|
||||||
|
map[id] = r
|
||||||
|
return {
|
||||||
|
label: `${r.code || ''} ${r.name || ''}`.trim() || String(id),
|
||||||
|
value: id,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
processMap.value = map
|
||||||
|
} catch {
|
||||||
|
processOptions.value = []
|
||||||
|
processMap.value = {}
|
||||||
|
} finally {
|
||||||
|
processLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCustomRow() {
|
||||||
|
customRows.value.push(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCustomRow(idx: number) {
|
||||||
|
if (customRows.value.length <= 1) return
|
||||||
|
customRows.value.splice(idx, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetState() {
|
||||||
|
preview.value = null
|
||||||
|
mode.value = 'equal'
|
||||||
|
keepQty.value = 0
|
||||||
|
equalCount.value = 2
|
||||||
|
customRows.value = [null]
|
||||||
|
routeMode.value = 'kingdee'
|
||||||
|
routingNo.value = null
|
||||||
|
routeList.value = []
|
||||||
|
assembleRows.value = [createEmptyAssembleRow(10)]
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAssembleSteps(): ProcessPlanSplitStep[] {
|
||||||
|
return assembleRows.value.map((row, i) => {
|
||||||
|
const name = row.processName || processMap.value[row.processId!]?.name || null
|
||||||
|
const desc = (row.operDescription || '').trim() || name
|
||||||
|
return {
|
||||||
|
processId: row.processId,
|
||||||
|
operNumber: row.operNumber ?? (i + 1) * 10,
|
||||||
|
processCode: row.processCode,
|
||||||
|
processName: name,
|
||||||
|
operDescription: desc,
|
||||||
|
sectionId: row.sectionId,
|
||||||
|
workshopId: row.workshopId,
|
||||||
|
activityUnit: row.activityUnit,
|
||||||
|
optCtrlCodeName: row.optCtrlCodeName,
|
||||||
|
activityQty: row.activityQty,
|
||||||
|
qualityInspection: row.qualityInspection ?? 0,
|
||||||
|
storageEntry: row.storageEntry ?? 0,
|
||||||
|
procurement: row.procurement ?? 0,
|
||||||
|
isOutsource: row.isOutsource ?? 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmSplit() {
|
||||||
|
if (!props.planId || !canConfirm.value) return
|
||||||
|
// 等量:列表已含原单;指定:拼上 keepQty
|
||||||
|
const quantities =
|
||||||
|
mode.value === 'equal'
|
||||||
|
? equalQuantities.value
|
||||||
|
: [keepQty.value!, ...customRows.value.map((v) => Number(v))]
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
await orderProcessPlanApi.split({
|
||||||
|
planId: props.planId,
|
||||||
|
quantities,
|
||||||
|
routeMode: routeMode.value,
|
||||||
|
routingNo: routeMode.value === 'kingdee' ? routingNo.value : null,
|
||||||
|
assembleSteps: routeMode.value === 'assemble' ? buildAssembleSteps() : undefined,
|
||||||
|
})
|
||||||
|
message.success('拆单成功')
|
||||||
|
visible.value = false
|
||||||
|
emit('success')
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.message || '拆单失败')
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.split-scroll {
|
||||||
|
max-height: min(72vh, 680px);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
.split-desc {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.process-names {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.split-section {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px dashed #e8e8e8;
|
||||||
|
}
|
||||||
|
.split-section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.field-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.field-label-inline {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
min-width: 64px;
|
||||||
|
}
|
||||||
|
.split-body {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.field-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.hint,
|
||||||
|
.hint-inline {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.kingdee-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 280px 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.kingdee-left,
|
||||||
|
.kingdee-right {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.route-card-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
max-height: 280px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1px;
|
||||||
|
}
|
||||||
|
.route-card {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fafafa;
|
||||||
|
transition: border-color 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
.route-card:hover {
|
||||||
|
border-color: #93c5fd;
|
||||||
|
background: #f8fbff;
|
||||||
|
}
|
||||||
|
.route-card.active {
|
||||||
|
border-color: #2080f0;
|
||||||
|
background: #eff6ff;
|
||||||
|
}
|
||||||
|
.route-card-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.route-code {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.route-step-count {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #6b7280;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.route-flow {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #6b7280;
|
||||||
|
line-height: 1.35;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.route-detail-sub {
|
||||||
|
margin-left: 6px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
.route-detail-empty {
|
||||||
|
height: 260px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px dashed #e5e7eb;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
.assemble-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.custom-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.sum-line {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.sum-line.error {
|
||||||
|
color: #d03050;
|
||||||
|
}
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.kingdee-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.route-card-list {
|
||||||
|
max-height: 140px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -206,6 +206,13 @@
|
|||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
|
||||||
|
<!-- 工序工单拆单 -->
|
||||||
|
<SplitProcessDialog
|
||||||
|
v-model:show="splitVisible"
|
||||||
|
:plan-id="splitPlanId"
|
||||||
|
@success="loadData"
|
||||||
|
/>
|
||||||
|
|
||||||
<!--派工弹框-->
|
<!--派工弹框-->
|
||||||
<n-modal
|
<n-modal
|
||||||
v-model:show="dispatchmodal"
|
v-model:show="dispatchmodal"
|
||||||
@ -422,6 +429,7 @@ import {
|
|||||||
import { dictDataApi } from '@/api/org'
|
import { dictDataApi } from '@/api/org'
|
||||||
|
|
||||||
import Pgitem from './pgitem.vue'
|
import Pgitem from './pgitem.vue'
|
||||||
|
import SplitProcessDialog from './components/SplitProcessDialog.vue'
|
||||||
|
|
||||||
//import { userApi } from '@/api/system'
|
//import { userApi } from '@/api/system'
|
||||||
|
|
||||||
@ -433,6 +441,25 @@ const hasPermission = (permission: string) => userStore.hasPermission(permission
|
|||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
|
||||||
|
const splitVisible = ref(false)
|
||||||
|
const splitPlanId = ref<number | null>(null)
|
||||||
|
|
||||||
|
/** 列表拆单入口:是否可拆以后端预览为准,这里只做粗过滤 */
|
||||||
|
function canSplitProcess(proc: ProcessPlanItemVO) {
|
||||||
|
const qty = proc.quantity ?? 0
|
||||||
|
return qty > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSplit(process: ProcessPlanItemVO) {
|
||||||
|
if (!process.planId) {
|
||||||
|
message.warning('工序计划无效')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 后端按工单号拆整单:任意工序 id 均可定位,拆出工单带全部工序
|
||||||
|
splitPlanId.value = process.planId
|
||||||
|
splitVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// 搜索表单
|
// 搜索表单
|
||||||
const searchForm = reactive<any>({
|
const searchForm = reactive<any>({
|
||||||
id: '',
|
id: '',
|
||||||
@ -450,6 +477,7 @@ const orderScrollX = 2090
|
|||||||
const processScrollX = 2788
|
const processScrollX = 2788
|
||||||
|
|
||||||
function orderRowKey(row: OrderProcessPlanVO) {
|
function orderRowKey(row: OrderProcessPlanVO) {
|
||||||
|
if (row.workOrderCode) return row.workOrderCode
|
||||||
return row.orderItemId ?? `order-${row.mainCode}-${row.orderCode}`
|
return row.orderItemId ?? `order-${row.mainCode}-${row.orderCode}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -490,7 +518,7 @@ function buildProcessActionColumn(order: OrderProcessPlanVO): DataTableColumns<P
|
|||||||
return {
|
return {
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
width: 140,
|
width: 200,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render(row) {
|
render(row) {
|
||||||
@ -511,6 +539,14 @@ function buildProcessActionColumn(order: OrderProcessPlanVO): DataTableColumns<P
|
|||||||
onClick: () => disphand(row, order),
|
onClick: () => disphand(row, order),
|
||||||
}, { default: () => '派工' }))
|
}, { default: () => '派工' }))
|
||||||
}
|
}
|
||||||
|
if (hasPermission('biz:orderProcessPlan:edit') && canSplitProcess(row)) {
|
||||||
|
buttons.push(h(NButton, {
|
||||||
|
size: 'small',
|
||||||
|
type: 'info',
|
||||||
|
ghost: true,
|
||||||
|
onClick: () => openSplit(row),
|
||||||
|
}, { default: () => '拆单' }))
|
||||||
|
}
|
||||||
return buttons.length > 0 ? h(NSpace, { justify: 'center' }, { default: () => buttons }) : '-'
|
return buttons.length > 0 ? h(NSpace, { justify: 'center' }, { default: () => buttons }) : '-'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -602,8 +638,12 @@ const orderColumns: DataTableColumns<OrderProcessPlanVO> = [
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
ellipsis: { tooltip: true },
|
ellipsis: { tooltip: true },
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
const codes = (row.processList ?? []).map((p) => p.workOrderCode).filter(Boolean)
|
if (row.workOrderCode) return row.workOrderCode
|
||||||
return codes.length ? codes.join('、') : '-'
|
const codes = (row.processList ?? [])
|
||||||
|
.map((p) => p.workOrderCode)
|
||||||
|
.filter((c): c is string => !!c)
|
||||||
|
const unique = [...new Set(codes)]
|
||||||
|
return unique.length ? unique.join('、') : '-'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ title: '数量', key: 'quantity', width: 72, align: 'center' },
|
{ title: '数量', key: 'quantity', width: 72, align: 'center' },
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user