修改
This commit is contained in:
commit
fed651a202
1
.gitignore
vendored
1
.gitignore
vendored
@ -27,3 +27,4 @@ AGENTS.md
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
/src/views/biz/tool/usage.vue
|
||||
|
||||
@ -72,10 +72,11 @@ export const assingWorkDetailApi = {
|
||||
|
||||
|
||||
//根据派工id获取详情信息
|
||||
selectAssingWorkDetailListByAssingWorkId(assingWorkId:number) {
|
||||
selectAssingWorkDetailListByAssingWorkId(params:{page?:number;pageSize?:number;assingWorkId?:number}) {
|
||||
return request({
|
||||
url:`/biz/assingWorkDetail/getAssingWorkDetailList/${assingWorkId}`,
|
||||
method:"get"
|
||||
url:`/biz/assingWorkDetail/getAssingWorkDetailList`,
|
||||
method:"get",
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -22,6 +22,14 @@ export interface BasicProcessPlan {
|
||||
|
||||
departmentName?: string
|
||||
|
||||
sectionId?: number | null
|
||||
|
||||
workShopId?: number | null
|
||||
|
||||
sectionName?: string
|
||||
|
||||
workShopName?: string
|
||||
|
||||
operDescription?: string
|
||||
|
||||
createBy?: number
|
||||
|
||||
@ -31,15 +31,51 @@ export interface DeviceRealtimeVO {
|
||||
displayName: string
|
||||
synEquipId: number
|
||||
bound: boolean
|
||||
/** connected:机台是否在线 */
|
||||
connected?: number | null
|
||||
/** cnc_status:控制器状态 */
|
||||
cncStatus?: number | null
|
||||
cncStatusText?: string
|
||||
/** part_count:目前工件数 */
|
||||
partCount?: number | null
|
||||
/** part_count_required:需求工件数 */
|
||||
partCountRequired?: number | null
|
||||
/** main_program:各轴群的当前加工号码 */
|
||||
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
|
||||
alarmMessages?: string[]
|
||||
dataTimestamp?: number
|
||||
sectionId?: number | null
|
||||
sectionName?: string
|
||||
workshopId?: number | null
|
||||
workshopName?: string
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -14,15 +14,15 @@ export interface MaintenanceRecord {
|
||||
|
||||
executorId?: number
|
||||
|
||||
startTime?: string
|
||||
startTime?: string | number
|
||||
|
||||
endTime?: string
|
||||
endTime?: string | number
|
||||
|
||||
recordStatus?: string
|
||||
|
||||
createTime?: string
|
||||
createTime?: string | number
|
||||
|
||||
updateTime?: string
|
||||
updateTime?: string | number
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -101,3 +101,11 @@ export function exportNccode(params:any) {
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
//加载全部NC代码
|
||||
export function loadAllNcCode() {
|
||||
return request({
|
||||
url: `biz/ncCode/loadAllNcCode`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
@ -52,6 +52,9 @@ export interface OrderItem {
|
||||
routeCode?: string
|
||||
|
||||
workshopId?: number | null
|
||||
|
||||
/** 开工后关联的执行侧 mes_order_item.id */
|
||||
orderItemId?: number | null
|
||||
}
|
||||
|
||||
export interface KingdeePrdMo {
|
||||
@ -131,8 +134,8 @@ export const orderItemApi = {
|
||||
return request({ url: `/biz/orderItem/${id}/startWork`, method: 'post' })
|
||||
},
|
||||
|
||||
/** 按工艺路线编码查询工艺路线(含工序);可带 orderItemId 精确匹配 */
|
||||
getRoute(params: { routeCode?: string; orderItemId?: number | string }) {
|
||||
/** 按工艺路线编码查询;orderItemId/planId 均为计划订单 id(mes_order_item_plan.id) */
|
||||
getRoute(params: { routeCode?: string; orderItemId?: number | string; planId?: number | string }) {
|
||||
return request({ url: '/biz/orderItem/route', method: 'get', params })
|
||||
},
|
||||
//根据生产编号查询预生产订单
|
||||
|
||||
@ -70,6 +70,9 @@ export interface ProcessPlanItemVO {
|
||||
|
||||
workOrderCode?: string
|
||||
|
||||
/** 拆单来源工序计划ID */
|
||||
splitFromId?: number | null
|
||||
|
||||
processCode?: string
|
||||
|
||||
operNumber?: number
|
||||
@ -150,6 +153,9 @@ export interface OrderProcessPlanVO {
|
||||
|
||||
orderItemId?: number
|
||||
|
||||
/** 工单号(列表按工单分组) */
|
||||
workOrderCode?: string
|
||||
|
||||
mainCode?: string
|
||||
|
||||
materialCode?: string
|
||||
@ -290,7 +296,7 @@ export function toOrderProcessPlanPayload(data: Record<string, unknown>): OrderP
|
||||
|
||||
const displayKeys = [
|
||||
|
||||
'mainCode', 'materialCode', 'materialName', 'orderCode', 'routeCode', 'workshopName',
|
||||
'mainCode', 'materialCode', 'materialName', 'orderCode', 'routeCode', 'workshopName', 'workOrderCode',
|
||||
|
||||
'actualStartTime', 'actualEndTime', 'completedQty', 'orderStatus', 'currentProcessName',
|
||||
|
||||
@ -302,7 +308,7 @@ export function toOrderProcessPlanPayload(data: Record<string, unknown>): OrderP
|
||||
|
||||
'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>({
|
||||
|
||||
@ -364,6 +370,8 @@ export const orderProcessPlanApi = {
|
||||
|
||||
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[]
|
||||
}
|
||||
|
||||
/** 委外可选工序项 */
|
||||
|
||||
72
src/api/permissionConfig.ts
Normal file
72
src/api/permissionConfig.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
// 数据权限配置 类型定义
|
||||
export interface PermissionConfig {
|
||||
id?: number
|
||||
|
||||
mapperKey?: string
|
||||
|
||||
controlMethod?: string
|
||||
|
||||
permissionControl?: string
|
||||
|
||||
}
|
||||
|
||||
// 数据权限配置 API
|
||||
export const permissionConfigApi = {
|
||||
// 分页查询
|
||||
page(params: { page: number; pageSize: number; mapperKey?: string; controlMethod?: string }) {
|
||||
return request({ url: '/biz/permissionConfig/page', method: 'get', params })
|
||||
},
|
||||
|
||||
// 新增
|
||||
options() {
|
||||
return request({ url: '/biz/permissionConfig/options', method: 'get' })
|
||||
},
|
||||
|
||||
// 获取详情
|
||||
detail(id: string) {
|
||||
return request({ url: `/biz/permissionConfig/${id}`, method: 'get' })
|
||||
},
|
||||
|
||||
// 新增
|
||||
create(data: PermissionConfig) {
|
||||
return request({ url: '/biz/permissionConfig', method: 'post', data })
|
||||
},
|
||||
|
||||
// 修改
|
||||
update(data: PermissionConfig) {
|
||||
return request({ url: '/biz/permissionConfig', method: 'put', data })
|
||||
},
|
||||
|
||||
// 删除
|
||||
delete(ids: string[]) {
|
||||
return request({ url: `/biz/permissionConfig/${ids.join(',')}`, method: 'delete' })
|
||||
},
|
||||
|
||||
// 导出
|
||||
export(params?: { ids?: string[]; mapperKey?: string; controlMethod?: string }) {
|
||||
const p: Record<string, any> = {}
|
||||
if (params?.ids?.length) p.ids = params.ids.join(',')
|
||||
if (params?.mapperKey !== undefined && params?.mapperKey !== null) p.mapperKey = params.mapperKey
|
||||
if (params?.controlMethod !== undefined && params?.controlMethod !== null) p.controlMethod = params.controlMethod
|
||||
return request({ url: `/biz/permissionConfig/export`, method: 'get', params: p, responseType: 'blob' })
|
||||
},
|
||||
|
||||
// 导入
|
||||
importData(file: File) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return request<{ success: number; fail: number; errors: string[] }>({
|
||||
url: `/biz/permissionConfig/import`,
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
},
|
||||
|
||||
// 下载导入模板
|
||||
downloadTemplate() {
|
||||
return request({ url: `/biz/permissionConfig/template`, method: 'get', responseType: 'blob' })
|
||||
}
|
||||
}
|
||||
@ -88,5 +88,43 @@ export const processRouteApi = {
|
||||
// 下载导入模板
|
||||
downloadTemplate() {
|
||||
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
|
||||
}
|
||||
|
||||
@ -106,7 +106,7 @@ export const qualityTestingApi = {
|
||||
},
|
||||
|
||||
//加载质检单
|
||||
loadQualityTestingList(assingId:number) {
|
||||
return request({url:`/mes/qc/loadQualityTestingList/${assingId}`,method:'get'})
|
||||
loadQualityTestingList(params:{page?:number,pageSize?:number,assingId?:number}) {
|
||||
return request({url:`/mes/qc/loadQualityTestingList`,method:'get', params})
|
||||
}
|
||||
}
|
||||
@ -99,7 +99,8 @@ export const submitLogApi = {
|
||||
},
|
||||
|
||||
//加载汇报列表
|
||||
loadSumbitLog(assingId?:number){
|
||||
return request({url:`/mes/report/loadSumbitLog/${assingId}`,method:'get'})
|
||||
loadSumbitLog(params:{assingId:number,page?:number,pageSize?:number}){
|
||||
console.log(params)
|
||||
return request({url:`/mes/report/loadSumbitLog`,method:'get',params})
|
||||
}
|
||||
}
|
||||
|
||||
@ -201,7 +201,27 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('@/views/test/test/index.vue'),
|
||||
meta: { title: '测试菜单', icon: 'StarOutline' }
|
||||
},
|
||||
// 设备维修保养记录
|
||||
{
|
||||
path: 'biz/maintenanceRecord',
|
||||
name: 'MaintenanceRecord',
|
||||
component: () => import('@/views/biz/maintenanceRecord/index.vue'),
|
||||
meta: { title: '设备维修保养记录', icon: 'BuildOutline' }
|
||||
},
|
||||
// 设备状态监控(独立页)
|
||||
{
|
||||
path: 'biz/device/board',
|
||||
name: 'deviceRealtimeBoard',
|
||||
component: () => import('@/views/biz/device/board.vue'),
|
||||
meta: { title: '设备状态监控', icon: 'PulseOutline' }
|
||||
},
|
||||
// 开发工具
|
||||
{
|
||||
path: 'biz/permissionConfig',
|
||||
name: 'permissionConfig',
|
||||
component: () => import('@/views/biz/permissionConfig/index.vue'),
|
||||
meta: { title: '数据权限配置', icon: 'ListOutline' }
|
||||
},
|
||||
{
|
||||
path: 'tool/gen',
|
||||
name: 'ToolGen',
|
||||
|
||||
@ -35,8 +35,12 @@
|
||||
value: '2'
|
||||
},
|
||||
{
|
||||
label: '其他异常',
|
||||
label: '设备异常',
|
||||
value: '3'
|
||||
},
|
||||
{
|
||||
label: '其他异常',
|
||||
value: '4'
|
||||
}
|
||||
]
|
||||
|
||||
@ -170,16 +174,11 @@
|
||||
<n-form inline :model="searchForm" label-placement="left">
|
||||
<n-grid :x-gap="8" :cols="4">
|
||||
<n-gi>
|
||||
<n-form-item label="异常编号">
|
||||
<n-input v-model:value="searchForm.abnormalCode" placeholder="请输入异常编号" clearable style="width: 200px"/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="操作人员">
|
||||
<n-form-item label="报警异常">
|
||||
<n-select
|
||||
v-model:value="searchForm.abnormalType"
|
||||
:options="abnormalTypes"
|
||||
placeholder="请选择异常类型"
|
||||
placeholder="请选择报警异常"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -379,7 +379,13 @@ const columns: DataTableColumns<Device> = [
|
||||
title: '新代ID', key: 'synEquipId', width: 90,
|
||||
render: (row) => row.synEquipId ?? '-'
|
||||
},
|
||||
{title: '设备类型', key: 'deviceType'},
|
||||
{title: '设备类型', key: 'deviceTypeId',
|
||||
render: (row) =>{
|
||||
let val = row.deviceTypeId
|
||||
const opt = deviceTypeList.value.find(o => o.value === val || String(o.value) === String(val))
|
||||
return h(NTag, {type: opt.class, size: 'small'}, {default: () => opt.label})
|
||||
}
|
||||
},
|
||||
{title: '设备负责人', key: 'deviceManagerName'},
|
||||
{title: '所属工段', key: 'sectionName'},
|
||||
{title: '设备型号', key: 'spec'},
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-key="(row) => row.recordId"
|
||||
:row-key="(row: MaintenanceRecord) => row.recordId"
|
||||
:scroll-x="1200"
|
||||
@update:page="handlePageChange"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
@ -59,31 +59,31 @@
|
||||
</n-card>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 600px">
|
||||
<n-form ref="formRef" :model="formData" label-placement="left" label-width="100px">
|
||||
<n-form-item label="设备ID,关联mes_equipment设备表主键" path="equipmentId">
|
||||
<n-input v-model:value="formData.equipmentId" placeholder="请输入设备ID,关联mes_equipment设备表主键" />
|
||||
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 560px">
|
||||
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="100px">
|
||||
<n-form-item label="设备ID" path="equipmentId">
|
||||
<n-input v-model:value="formData.equipmentId" placeholder="请输入设备ID" />
|
||||
</n-form-item>
|
||||
<n-form-item label="记录类型:maintain=保养,repair=维修" path="recordType">
|
||||
<n-select v-model:value="formData.recordType" placeholder="请选择记录类型:maintain=保养,repair=维修" :options="[]" />
|
||||
<n-form-item label="记录类型" path="recordType">
|
||||
<n-select v-model:value="formData.recordType" placeholder="请选择记录类型" :options="recordTypeOptions" />
|
||||
</n-form-item>
|
||||
<n-form-item label="故障现象/保养项目详情" path="itemContent">
|
||||
<n-input v-model:value="formData.itemContent" type="textarea" placeholder="请输入故障现象/保养项目详情" />
|
||||
<n-form-item label="故障现象" path="itemContent">
|
||||
<n-input v-model:value="formData.itemContent" type="textarea" placeholder="请输入故障现象/保养项目" :autosize="{ minRows: 2, maxRows: 4 }" />
|
||||
</n-form-item>
|
||||
<n-form-item label="维修处理措施/保养执行内容" path="handleContent">
|
||||
<n-input v-model:value="formData.handleContent" type="textarea" placeholder="请输入维修处理措施/保养执行内容" />
|
||||
<n-form-item label="处理措施" path="handleContent">
|
||||
<n-input v-model:value="formData.handleContent" type="textarea" placeholder="请输入维修处理措施/保养执行内容" :autosize="{ minRows: 2, maxRows: 4 }" />
|
||||
</n-form-item>
|
||||
<n-form-item label="执行人ID,关联sys_user系统用户表主键" path="executorId">
|
||||
<n-input v-model:value="formData.executorId" placeholder="请输入执行人ID,关联sys_user系统用户表主键" />
|
||||
<n-form-item label="执行人" path="executorId">
|
||||
<n-input v-model:value="formData.executorId" placeholder="请输入执行人ID" />
|
||||
</n-form-item>
|
||||
<n-form-item label="维保作业开始时间" path="startTime">
|
||||
<n-date-picker v-model:value="formData.startTime" type="datetime" clearable style="width: 100%" />
|
||||
<n-form-item label="开始时间" path="startTime">
|
||||
<n-date-picker v-model:value="formData.startTime" type="datetime" placeholder="请选择开始时间" clearable style="width: 100%" />
|
||||
</n-form-item>
|
||||
<n-form-item label="维保作业结束时间,待处理单据为空" path="endTime">
|
||||
<n-date-picker v-model:value="formData.endTime" type="datetime" clearable style="width: 100%" />
|
||||
<n-form-item label="结束时间" path="endTime">
|
||||
<n-date-picker v-model:value="formData.endTime" type="datetime" placeholder="请选择结束时间" clearable style="width: 100%" />
|
||||
</n-form-item>
|
||||
<n-form-item label="单据状态:pending=待处理,finished=已完成" path="recordStatus">
|
||||
<n-select v-model:value="formData.recordStatus" placeholder="请选择单据状态:pending=待处理,finished=已完成" :options="recordStatusOptions" />
|
||||
<n-form-item label="单据状态" path="recordStatus">
|
||||
<n-select v-model:value="formData.recordStatus" placeholder="请选择单据状态" :options="recordStatusOptions" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
@ -132,7 +132,6 @@ import { ref, reactive, h, onMounted } from 'vue'
|
||||
import { NButton, NSpace, NIcon, NUpload, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions } from 'naive-ui'
|
||||
import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline } from '@vicons/ionicons5'
|
||||
import { maintenanceRecordApi, type MaintenanceRecord } from '@/api/maintenanceRecord'
|
||||
import { dictDataApi } from '@/api/org'
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
@ -145,7 +144,7 @@ const searchForm = reactive({
|
||||
// 表格数据
|
||||
const tableData = ref<MaintenanceRecord[]>([])
|
||||
const loading = ref(false)
|
||||
const selectedIds = ref<number[]>([])
|
||||
const selectedIds = ref<string[]>([])
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
@ -171,8 +170,15 @@ const defaultFormData: MaintenanceRecord = {
|
||||
}
|
||||
const formData = reactive<MaintenanceRecord>({ ...defaultFormData })
|
||||
|
||||
// 字典选项(下拉框/单选框/复选框关联字典时使用)
|
||||
const recordStatusOptions = ref<{ label: string; value: any }[]>([])
|
||||
// 字典选项
|
||||
const recordTypeOptions = ref<{ label: string; value: any }[]>([
|
||||
{ label: '保养', value: 'maintain' },
|
||||
{ label: '维修', value: 'repair' }
|
||||
])
|
||||
const recordStatusOptions = ref<{ label: string; value: any }[]>([
|
||||
{ label: '待处理', value: 'pending' },
|
||||
{ label: '已完成', value: 'finished' }
|
||||
])
|
||||
|
||||
// 表单校验规则
|
||||
const formRules = {
|
||||
@ -185,23 +191,27 @@ const formRules = {
|
||||
// 表格列
|
||||
const columns: DataTableColumns<MaintenanceRecord> = [
|
||||
{ type: 'selection' },
|
||||
{ title: '维保记录编号', key: 'recordId' },
|
||||
{ title: '设备ID', key: 'equipmentId' },
|
||||
{ title: '记录类型', key: 'recordType' },
|
||||
{ title: '故障现象/保养项目', key: 'itemContent' },
|
||||
{ title: '维修处理措施/保养执行内容', key: 'handleContent' },
|
||||
{ title: '执行人ID', key: 'executorId' },
|
||||
{ title: '维保作业开始时间', key: 'startTime' },
|
||||
{ title: '维保作业结束时间', key: 'endTime' },
|
||||
{ title: '单据状态', key: 'recordStatus',
|
||||
{ title: '维保记录编号', key: 'recordId', width: 160 },
|
||||
{ title: '设备ID', key: 'equipmentId', width: 100 },
|
||||
{ title: '记录类型', key: 'recordType', width: 100,
|
||||
render(row) {
|
||||
const opt = recordTypeOptions.value.find(o => o.value === row.recordType)
|
||||
return opt ? opt.label : (row.recordType ?? '-')
|
||||
}
|
||||
},
|
||||
{ title: '故障现象/保养项目', key: 'itemContent', ellipsis: { tooltip: true } },
|
||||
{ title: '维修处理措施/保养执行内容', key: 'handleContent', ellipsis: { tooltip: true } },
|
||||
{ title: '执行人ID', key: 'executorId', width: 100 },
|
||||
{ title: '维保作业开始时间', key: 'startTime', width: 170 },
|
||||
{ title: '维保作业结束时间', key: 'endTime', width: 170 },
|
||||
{ title: '单据状态', key: 'recordStatus', width: 100,
|
||||
render(row) {
|
||||
const val = row.recordStatus
|
||||
const opt = recordStatusOptions.value.find(o => o.value === val || String(o.value) === String(val))
|
||||
return opt ? opt.label : (val ?? '-')
|
||||
}
|
||||
},
|
||||
{ title: '单据创建时间', key: 'createTime', width: 180 },
|
||||
{ title: '单据最后更新时间', key: 'updateTime', width: 180 },
|
||||
{ title: '单据创建时间', key: 'createTime', width: 170 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
@ -229,8 +239,8 @@ async function loadData() {
|
||||
pageSize: pagination.pageSize,
|
||||
recordId: searchForm.recordId || undefined,
|
||||
})
|
||||
tableData.value = res.list
|
||||
pagination.itemCount = res.total
|
||||
tableData.value = res.records || res.list || []
|
||||
pagination.itemCount = res.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@ -263,7 +273,7 @@ function handlePageSizeChange(pageSize: number) {
|
||||
|
||||
// 选择
|
||||
function handleCheck(keys: Array<string | number>) {
|
||||
selectedIds.value = keys as number[]
|
||||
selectedIds.value = keys.map(String)
|
||||
}
|
||||
|
||||
// 新增
|
||||
@ -416,17 +426,8 @@ async function handleImportUpload({ file }: UploadCustomRequestOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载字典选项
|
||||
async function loadDictOptions() {
|
||||
try {
|
||||
const data = await dictDataApi.listByType('sys_status')
|
||||
recordStatusOptions.value = data.map(d => ({ label: d.dictLabel, value: d.dictValue }))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadDictOptions()
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -439,19 +440,3 @@ onMounted(() => {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
.maintenance-page {
|
||||
padding: 16px;
|
||||
.search-card, .btn-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
// 全局卡片统一圆角
|
||||
:deep(.el-card) {
|
||||
border-radius: 8px;
|
||||
}
|
||||
// 表格单元格加宽内边距
|
||||
:deep(.el-table__cell) {
|
||||
padding: 12px 10px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
52
src/views/biz/maintenanceRecord/simple.vue
Normal file
52
src/views/biz/maintenanceRecord/simple.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div style="padding: 20px;">
|
||||
<h2>设备维修保养记录 - 简易版</h2>
|
||||
<n-button type="primary" :loading="loading" @click="loadData">加载数据</n-button>
|
||||
<n-data-table :columns="columns" :data="tableData" :loading="loading" style="margin-top: 20px;" />
|
||||
<pre style="margin-top: 20px; white-space: pre-wrap; background: #f5f5f5; padding: 10px; border-radius: 4px;">调试信息: {{ debug }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { NButton, useMessage, type DataTableColumns } from 'naive-ui'
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
const message = useMessage()
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
const debug = ref('')
|
||||
|
||||
const columns: DataTableColumns<any> = [
|
||||
{ title: '记录编号', key: 'recordId' },
|
||||
{ title: '设备ID', key: 'equipmentId' },
|
||||
{ title: '记录类型', key: 'recordType' },
|
||||
{ title: '内容', key: 'itemContent' },
|
||||
{ title: '状态', key: 'recordStatus' },
|
||||
{ title: '创建时间', key: 'createTime' },
|
||||
]
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
debug.value = '请求中...'
|
||||
try {
|
||||
const res = await request({
|
||||
url: '/biz/maintenanceRecord/page',
|
||||
method: 'get',
|
||||
params: { page: 1, pageSize: 10 }
|
||||
})
|
||||
debug.value = '成功: ' + JSON.stringify(res).substring(0, 500)
|
||||
tableData.value = res.records || res.list || []
|
||||
message.success('加载成功')
|
||||
} catch (err: any) {
|
||||
debug.value = '失败: ' + (err?.message || String(err))
|
||||
message.error('加载失败: ' + (err?.message || String(err)))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
@ -619,7 +619,7 @@ async function openRouteDrawer(row: any) {
|
||||
try {
|
||||
const data: any = await orderItemApi.getRoute({
|
||||
routeCode: code,
|
||||
orderItemId: row.id,
|
||||
planId: row.id,
|
||||
})
|
||||
if (data) {
|
||||
routeDetail.value = data
|
||||
@ -1278,7 +1278,7 @@ function handleDelete(row: OrderItem) {
|
||||
function handleStartWork(row: OrderItem) {
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: `确定开工订单【${row.orderCode || row.name}】?开工后将生成工单。`,
|
||||
content: `确定开工订单【${row.orderCode || row.name}】?开工后将生成执行订单与工单,派工请在工单侧操作。`,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
|
||||
@ -62,7 +62,6 @@
|
||||
<!-- 表头 -->
|
||||
<div class="board-table-head">
|
||||
<div class="col-check" />
|
||||
<div class="col-expand" />
|
||||
<div class="col-product">产品 / 工单编号</div>
|
||||
<div class="col-qty">数量</div>
|
||||
<div class="col-mode">模式</div>
|
||||
@ -80,29 +79,13 @@
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<n-checkbox
|
||||
:checked="selectedOrderIds.includes(order.orderItemId!)"
|
||||
@update:checked="(v: boolean) => toggleSelect(order.orderItemId!, v)"
|
||||
:checked="selectedOrderIds.includes(orderRowKey(order))"
|
||||
@update:checked="(v: boolean) => toggleSelect(orderRowKey(order), v)"
|
||||
/>
|
||||
</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="product-name">{{ order.materialName || '-' }}</div>
|
||||
<div class="product-code">
|
||||
@ -153,43 +136,6 @@
|
||||
</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="!(order.processList?.length)" class="expand-empty">暂无工序明细</div>
|
||||
<div v-else class="card-flow-scroll">
|
||||
@ -221,8 +167,6 @@
|
||||
<!--模型抽屉 -->
|
||||
<PlmModelDrawer ref="plmModelDrawerRef" />
|
||||
|
||||
|
||||
|
||||
<div class="board-pagination">
|
||||
<n-pagination
|
||||
v-model:page="pagination.page"
|
||||
@ -407,6 +351,13 @@
|
||||
</template>
|
||||
</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-divider />
|
||||
@ -664,13 +615,13 @@ import { ref, reactive, onMounted, h, computed, type Component } from 'vue'
|
||||
import {
|
||||
NButton, NSpace, NIcon, NTag, NSpin, NModal, NForm, NFormItem, NGrid, NGi,
|
||||
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,
|
||||
} from 'naive-ui'
|
||||
import {
|
||||
AddOutline, RemoveOutline, SearchOutline, RefreshOutline, CloudUploadOutline,
|
||||
AddOutline, SearchOutline, RefreshOutline, CloudUploadOutline,
|
||||
DownloadOutline, ChevronDownOutline, CheckmarkCircleOutline, CheckmarkCircle,
|
||||
EllipseOutline, TimeOutline, PrintOutline, SyncOutline, PeopleOutline,
|
||||
EllipseOutline, TimeOutline, PrintOutline, PeopleOutline,
|
||||
GitBranchOutline, CloseCircleOutline, LockClosedOutline, TrashOutline, EyeSharp,
|
||||
MenuOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
@ -688,6 +639,7 @@ import {
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import ProcessFlowStepper from './components/ProcessFlowStepper.vue'
|
||||
import ProcessCard from './components/ProcessCard.vue'
|
||||
import SplitProcessDialog from './components/SplitProcessDialog.vue'
|
||||
import PlmModelDrawer from '@/components/PlmModelDrawer.vue'
|
||||
import type { PlmModelOpenContext } from '@/api/plmModel'
|
||||
import Pgitem from './pgitem.vue'
|
||||
@ -706,9 +658,8 @@ const searchForm = reactive({
|
||||
})
|
||||
|
||||
const tableData = ref<OrderProcessPlanVO[]>([])
|
||||
const expandedIds = ref<Set<number>>(new Set())
|
||||
const derivedExpandedIds = ref<Set<number>>(new Set())
|
||||
const selectedOrderIds = ref<number[]>([])
|
||||
const expandedIds = ref<Set<string>>(new Set())
|
||||
const selectedOrderIds = ref<string[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, itemCount: 0 })
|
||||
|
||||
@ -716,6 +667,10 @@ const detailVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detailData = ref<OrderProcessPlanVO | null>(null)
|
||||
|
||||
/** 工序工单拆单 */
|
||||
const splitVisible = ref(false)
|
||||
const splitPlanId = ref<number | null>(null)
|
||||
|
||||
/** 订单级工序重排弹窗状态 */
|
||||
const reorderVisible = ref(false)
|
||||
const reorderLoading = ref(false)
|
||||
@ -742,7 +697,7 @@ const dispatchLoading = ref(false)
|
||||
const dispatchformRef = ref()
|
||||
const dispatchform = reactive<any>({
|
||||
id: '', name: '', beginTime: '', endTime: '', quantity: '', sectionId: null as number | null, workshopId: null as number | null,
|
||||
list: [{ sectionId:'',deviceId:'', quantity: '' }],
|
||||
list: [{ sectionId:'',deviceId:'', quantity: '', ncId:'' }],
|
||||
})
|
||||
const dispatchrules = {}
|
||||
|
||||
@ -835,7 +790,6 @@ const moreOptions = [
|
||||
label: '派生操作',
|
||||
key: 'group-derived',
|
||||
children: [
|
||||
{ label: '创建返工单', key: 'createRework', icon: dropdownIcon(SyncOutline) },
|
||||
{ label: '创建工序委外', key: 'createOutsource', icon: dropdownIcon(PeopleOutline) },
|
||||
{ label: '拆分工单', key: 'splitWorkOrder', icon: dropdownIcon(GitBranchOutline) },
|
||||
{ label: '汇报详情', key: 'reportDetailView', icon: dropdownIcon(EyeSharp) },
|
||||
@ -860,57 +814,78 @@ const moreOptions = [
|
||||
},
|
||||
]
|
||||
|
||||
/** 列表行主键:按工单号(拆单后同订单多工单分行) */
|
||||
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) {
|
||||
const codes = (order?.processList ?? []).map((p) => p.workOrderCode).filter(Boolean)
|
||||
return codes.length ? codes.join('、') : '-'
|
||||
if (order?.workOrderCode) return order.workOrderCode
|
||||
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[]) {
|
||||
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) {
|
||||
return order.orderItemId != null && expandedIds.value.has(order.orderItemId)
|
||||
}
|
||||
|
||||
function isDerivedExpanded(order: OrderProcessPlanVO) {
|
||||
return order.orderItemId != null && derivedExpandedIds.value.has(order.orderItemId)
|
||||
return expandedIds.value.has(orderRowKey(order))
|
||||
}
|
||||
|
||||
function toggleExpand(order: OrderProcessPlanVO) {
|
||||
if (order.orderItemId == null) return
|
||||
const key = orderRowKey(order)
|
||||
const next = new Set(expandedIds.value)
|
||||
if (next.has(order.orderItemId)) next.delete(order.orderItemId)
|
||||
else next.add(order.orderItemId)
|
||||
if (next.has(key)) next.delete(key)
|
||||
else next.add(key)
|
||||
expandedIds.value = next
|
||||
}
|
||||
|
||||
function toggleDerivedExpand(order: OrderProcessPlanVO) {
|
||||
if (order.orderItemId == null) return
|
||||
const next = new Set(derivedExpandedIds.value)
|
||||
if (next.has(order.orderItemId)) next.delete(order.orderItemId)
|
||||
else next.add(order.orderItemId)
|
||||
derivedExpandedIds.value = next
|
||||
/** 整单拆分入口:有计划数量且未全部完工即可点开 */
|
||||
function canSplitWorkOrder(order: OrderProcessPlanVO) {
|
||||
const qty = order.quantity ?? 0
|
||||
if (qty <= 0) return false
|
||||
if (order.orderStatus === 2) return false
|
||||
return (order.processList?.length ?? 0) > 0
|
||||
}
|
||||
|
||||
/** 派生工单(返工 / 拆分),待后端字段接入后替换占位逻辑 */
|
||||
function getDerivedOrders(order: OrderProcessPlanVO) {
|
||||
const raw = order as OrderProcessPlanVO & {
|
||||
reworkOrders?: Array<{ id: string | number; code?: string; quantity?: number; statusLabel?: string }>
|
||||
splitOrders?: Array<{ id: string | number; code?: string; quantity?: number; statusLabel?: string }>
|
||||
function openSplitFromOrder(order: OrderProcessPlanVO) {
|
||||
if (!hasPermission('biz:orderProcessPlan:edit')) {
|
||||
message.warning('无拆单权限')
|
||||
return
|
||||
}
|
||||
return {
|
||||
rework: raw.reworkOrders ?? [],
|
||||
split: raw.splitOrders ?? [],
|
||||
const list = sortedProcessList(order.processList)
|
||||
const seed = list.find((p) => p.planId != null)
|
||||
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]
|
||||
else selectedOrderIds.value = selectedOrderIds.value.filter(v => v !== id)
|
||||
}
|
||||
@ -982,7 +957,7 @@ function handlePageSizeChange(pageSize: number) {
|
||||
|
||||
function onBatchSelect(key: string) {
|
||||
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') {
|
||||
expandedIds.value = new Set()
|
||||
}
|
||||
@ -997,11 +972,13 @@ function onMoreSelect(key: string, order: OrderProcessPlanVO) {
|
||||
openReportDetail(order)
|
||||
return;
|
||||
}
|
||||
if (key === 'splitWorkOrder') {
|
||||
openSplitFromOrder(order)
|
||||
return
|
||||
}
|
||||
|
||||
const labels: Record<string, string> = {
|
||||
print: '打印',
|
||||
createRework: '创建返工单',
|
||||
splitWorkOrder: '拆分工单',
|
||||
withdraw: '撤回',
|
||||
freeze: '冻结工单',
|
||||
delete: '删除',
|
||||
@ -1117,7 +1094,7 @@ async function openDetail(order: OrderProcessPlanVO) {
|
||||
detailLoading.value = true
|
||||
detailData.value = null
|
||||
try {
|
||||
detailData.value = await orderProcessPlanApi.detail(order.orderItemId)
|
||||
detailData.value = await orderProcessPlanApi.detail(order.orderItemId, order.workOrderCode)
|
||||
} catch {
|
||||
detailData.value = order
|
||||
} finally {
|
||||
@ -1137,12 +1114,12 @@ async function openReorder(order: OrderProcessPlanVO) {
|
||||
reorderOrderMeta.value = order
|
||||
reorderList.value = []
|
||||
try {
|
||||
const detail = await orderProcessPlanApi.detail(order.orderItemId)
|
||||
const detail = await orderProcessPlanApi.detail(order.orderItemId, order.workOrderCode)
|
||||
reorderOrderMeta.value = detail
|
||||
// 与看板展开区一致:按 operNumber(后端 sort)升序
|
||||
// 仅当前工单工序,按 operNumber 升序
|
||||
reorderList.value = sortedProcessList(detail.processList).map((p) => ({ ...p }))
|
||||
if (!reorderList.value.length) {
|
||||
message.warning('当前订单暂无工序计划')
|
||||
message.warning('当前工单暂无工序计划')
|
||||
}
|
||||
} catch {
|
||||
// 详情失败时降级用列表行数据,仍可本地排序后提交
|
||||
@ -1267,11 +1244,11 @@ function disphand(process: ProcessPlanItemVO, order?: OrderProcessPlanVO) {
|
||||
dispatchform.sort = entity.sort
|
||||
dispatchform.sectionId = entity.sectionId ?? process.sectionId ?? null
|
||||
dispatchform.workshopId = entity.workshopId ?? process.workshopId ?? order?.workshopId ?? null
|
||||
dispatchform.list = [{ sectionId: '', deviceId: '', quantity: remainQty || '' }]
|
||||
dispatchform.list = [{ sectionId: '', deviceId: '', quantity: remainQty || '',ncId:'' }]
|
||||
}
|
||||
|
||||
function addpgnum() {
|
||||
dispatchform.list.push({ sectionId: '', deviceId: '', quantity: '' })
|
||||
dispatchform.list.push({ sectionId: '', deviceId: '', quantity: '',ncId:'' })
|
||||
}
|
||||
|
||||
function deletenum(index: number) {
|
||||
@ -1393,9 +1370,8 @@ onMounted(loadData)
|
||||
.board-table-head,
|
||||
.order-row {
|
||||
display: grid;
|
||||
/* 勾选 | 展开 | 产品工单 | 数量 | 模式 | 齐套率 | 工序轴 | 计划时间 | 生命周期 | 操作 */
|
||||
/* 勾选 | 产品工单 | 数量 | 模式 | 齐套率 | 工序轴 | 计划时间 | 生命周期 | 操作 */
|
||||
grid-template-columns:
|
||||
40px
|
||||
40px
|
||||
minmax(220px, 1.6fr)
|
||||
64px
|
||||
@ -1408,7 +1384,7 @@ onMounted(loadData)
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 16px;
|
||||
min-width: 1320px;
|
||||
min-width: 1280px;
|
||||
}
|
||||
|
||||
.board-table-head {
|
||||
@ -1438,16 +1414,7 @@ onMounted(loadData)
|
||||
background: rgba(32, 128, 240, 0.04);
|
||||
}
|
||||
|
||||
.order-row.derived-expanded {
|
||||
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 {
|
||||
.col-check {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
@ -1530,74 +1497,11 @@ onMounted(loadData)
|
||||
}
|
||||
|
||||
.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%);
|
||||
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,
|
||||
.detail-cards {
|
||||
display: flex;
|
||||
|
||||
@ -79,6 +79,13 @@
|
||||
ghost
|
||||
@click="emit('assign', process)"
|
||||
>派工</n-button>
|
||||
<n-button
|
||||
v-if="canSplit"
|
||||
size="tiny"
|
||||
type="primary"
|
||||
ghost
|
||||
@click="emit('split', process)"
|
||||
>拆单</n-button>
|
||||
<n-button
|
||||
v-if="canEdit"
|
||||
size="tiny"
|
||||
@ -86,7 +93,6 @@
|
||||
@click="emit('edit', 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
|
||||
v-if="canOutsource"
|
||||
size="tiny"
|
||||
@ -135,6 +141,7 @@ const props = defineProps<{
|
||||
isLast?: boolean
|
||||
canEdit?: boolean
|
||||
canAssign?: boolean
|
||||
canSplit?: boolean
|
||||
canOutsource?: boolean
|
||||
canSubmitDetail?: boolean
|
||||
}>()
|
||||
@ -142,6 +149,7 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{
|
||||
edit: [process: ProcessPlanItemVO]
|
||||
assign: [process: ProcessPlanItemVO]
|
||||
split: [process: ProcessPlanItemVO]
|
||||
viewModel: [process: ProcessPlanItemVO]
|
||||
outsource: [process: ProcessPlanItemVO]
|
||||
submitDetail: [process: ProcessPlanItemVO]
|
||||
|
||||
@ -39,7 +39,12 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
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) => {
|
||||
const label = item.processName || item.processCode || `工序${idx + 1}`
|
||||
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>
|
||||
</n-modal>
|
||||
|
||||
<!-- 工序工单拆单 -->
|
||||
<SplitProcessDialog
|
||||
v-model:show="splitVisible"
|
||||
:plan-id="splitPlanId"
|
||||
@success="loadData"
|
||||
/>
|
||||
|
||||
<!--派工弹框-->
|
||||
<n-modal
|
||||
v-model:show="dispatchmodal"
|
||||
@ -422,6 +429,7 @@ import {
|
||||
import { dictDataApi } from '@/api/org'
|
||||
|
||||
import Pgitem from './pgitem.vue'
|
||||
import SplitProcessDialog from './components/SplitProcessDialog.vue'
|
||||
|
||||
//import { userApi } from '@/api/system'
|
||||
|
||||
@ -433,6 +441,25 @@ const hasPermission = (permission: string) => userStore.hasPermission(permission
|
||||
const message = useMessage()
|
||||
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>({
|
||||
id: '',
|
||||
@ -450,6 +477,7 @@ const orderScrollX = 2090
|
||||
const processScrollX = 2788
|
||||
|
||||
function orderRowKey(row: OrderProcessPlanVO) {
|
||||
if (row.workOrderCode) return row.workOrderCode
|
||||
return row.orderItemId ?? `order-${row.mainCode}-${row.orderCode}`
|
||||
}
|
||||
|
||||
@ -490,7 +518,7 @@ function buildProcessActionColumn(order: OrderProcessPlanVO): DataTableColumns<P
|
||||
return {
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 140,
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
render(row) {
|
||||
@ -511,6 +539,14 @@ function buildProcessActionColumn(order: OrderProcessPlanVO): DataTableColumns<P
|
||||
onClick: () => disphand(row, order),
|
||||
}, { 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 }) : '-'
|
||||
},
|
||||
}
|
||||
@ -602,8 +638,12 @@ const orderColumns: DataTableColumns<OrderProcessPlanVO> = [
|
||||
align: 'center',
|
||||
ellipsis: { tooltip: true },
|
||||
render: (row) => {
|
||||
const codes = (row.processList ?? []).map((p) => p.workOrderCode).filter(Boolean)
|
||||
return codes.length ? codes.join('、') : '-'
|
||||
if (row.workOrderCode) return row.workOrderCode
|
||||
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' },
|
||||
|
||||
@ -1,195 +1,211 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-grid>
|
||||
<n-gi :span="10">
|
||||
<n-form-item
|
||||
label="指派工段"
|
||||
:path="`${listname}[${index}].sectionId`"
|
||||
>
|
||||
<n-select
|
||||
v-model:value="obj.sectionId"
|
||||
filterable
|
||||
placeholder="请选择工段"
|
||||
:options="sectionList"
|
||||
:loading="loadingRef"
|
||||
disabled
|
||||
remote
|
||||
:clear-filter-after-select="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="10">
|
||||
<n-form-item
|
||||
label="指派工位"
|
||||
:path="`${listname}[${index}].deviceId`"
|
||||
:rule="{
|
||||
<div>
|
||||
<n-grid>
|
||||
<n-gi :span="10">
|
||||
<n-form-item
|
||||
label="指派工段"
|
||||
:path="`${listname}[${index}].sectionId`"
|
||||
>
|
||||
<n-select
|
||||
v-model:value="obj.sectionId"
|
||||
filterable
|
||||
placeholder="请选择工段"
|
||||
:options="sectionList"
|
||||
:loading="loadingRef"
|
||||
disabled
|
||||
remote
|
||||
:clear-filter-after-select="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="10">
|
||||
<n-form-item
|
||||
label="指派工位"
|
||||
:path="`${listname}[${index}].deviceId`"
|
||||
:rule="{
|
||||
required:true,
|
||||
message:`请选择工位`,
|
||||
trigger: 'blur'
|
||||
}"
|
||||
>
|
||||
<n-select
|
||||
v-model:value="obj.deviceId"
|
||||
filterable
|
||||
placeholder="请选择工位"
|
||||
:options="deviceList"
|
||||
:loading="loadingRef"
|
||||
clearable
|
||||
remote
|
||||
:clear-filter-after-select="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
|
||||
</n-grid>
|
||||
<n-grid>
|
||||
|
||||
<n-gi :span="10">
|
||||
<n-form-item
|
||||
label="数量"
|
||||
:path="`${listname}[${index}].quantity`"
|
||||
:rule="{
|
||||
>
|
||||
<n-select
|
||||
v-model:value="obj.deviceId"
|
||||
filterable
|
||||
placeholder="请选择工位"
|
||||
:options="deviceList"
|
||||
:loading="loadingRef"
|
||||
clearable
|
||||
remote
|
||||
:clear-filter-after-select="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<n-grid>
|
||||
<n-gi :span="10">
|
||||
<n-form-item
|
||||
label="数量"
|
||||
:path="`${listname}[${index}].quantity`"
|
||||
:rule="{
|
||||
required: true,
|
||||
message: `请输入数量`,
|
||||
trigger: 'blur',
|
||||
}"
|
||||
>
|
||||
<n-input v-model:value="obj.quantity" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="4" style="padding-left:10px;">
|
||||
<n-icon
|
||||
v-if="index == 0"
|
||||
size="25"
|
||||
style="padding-top: 5px;cursor: pointer;" color="#1060c9"
|
||||
@click="addpgnum"
|
||||
>
|
||||
<AddOutline />
|
||||
</n-icon>
|
||||
>
|
||||
<n-input v-model:value="obj.quantity"/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="10">
|
||||
<n-form-item
|
||||
label="NC下发"
|
||||
:path="`${listname}[${index}].ncId`"
|
||||
>
|
||||
<n-select
|
||||
v-model:value="obj.ncId"
|
||||
filterable
|
||||
placeholder="请选择下发NC"
|
||||
:options="ncList"
|
||||
clearable
|
||||
remote
|
||||
:clear-filter-after-select="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="4" style="padding-left:10px;">
|
||||
<n-icon
|
||||
v-if="index == 0"
|
||||
size="25"
|
||||
style="padding-top: 5px;cursor: pointer;" color="#1060c9"
|
||||
@click="addpgnum"
|
||||
>
|
||||
<AddOutline/>
|
||||
</n-icon>
|
||||
|
||||
<n-icon
|
||||
v-else
|
||||
size="18"
|
||||
style="padding:7px 0 0 2px;cursor: pointer;" color="#d03050"
|
||||
@click="deletenum(index)"
|
||||
>
|
||||
<TrashOutline />
|
||||
</n-icon>
|
||||
</n-gi>
|
||||
<n-icon
|
||||
v-else
|
||||
size="18"
|
||||
style="padding:7px 0 0 2px;cursor: pointer;" color="#d03050"
|
||||
@click="deletenum(index)"
|
||||
>
|
||||
<TrashOutline/>
|
||||
</n-icon>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref,reactive } from 'vue'
|
||||
import { userApi } from '@/api/system'
|
||||
import {sectionApi, type Section} from '@/api/section'
|
||||
import {
|
||||
AddOutline,
|
||||
TrashOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
import {ref, reactive} from 'vue'
|
||||
import {userApi} from '@/api/system'
|
||||
import {sectionApi, type Section} from '@/api/section'
|
||||
import {
|
||||
AddOutline,
|
||||
TrashOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
import {loadAllNcCode} from '@/api/nccode.ts'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
listname:any
|
||||
obj:any,
|
||||
index:any
|
||||
sectionId?: number | string | null
|
||||
workshopId?: number | string | null
|
||||
}>(), {
|
||||
obj:{}
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
addhand:[],
|
||||
delehand:[index:any],
|
||||
update: [index:any,obj:any]
|
||||
}>()
|
||||
const props = withDefaults(defineProps<{
|
||||
listname: any
|
||||
obj: any,
|
||||
index: any
|
||||
sectionId?: number | string | null
|
||||
workshopId?: number | string | null
|
||||
}>(), {
|
||||
obj: {}
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
addhand: [],
|
||||
delehand: [index: any],
|
||||
update: [index: any, obj: any]
|
||||
}>()
|
||||
|
||||
|
||||
// function updatehand() {
|
||||
// emit('update')
|
||||
// }
|
||||
|
||||
// function updatehand() {
|
||||
// emit('update')
|
||||
// }
|
||||
function addpgnum() {
|
||||
emit('addhand')
|
||||
}
|
||||
|
||||
function addpgnum() {
|
||||
emit('addhand')
|
||||
}
|
||||
function deletenum(index:any){
|
||||
emit('delehand',index)
|
||||
}
|
||||
function deletenum(index: any) {
|
||||
emit('delehand', index)
|
||||
}
|
||||
|
||||
let sectionList = reactive<{label:string,value:any}[]>([])
|
||||
let yglist = ref<any>([])
|
||||
let sectionList = reactive<{ label: string, value: any }[]>([])
|
||||
let yglist = ref<any>([])
|
||||
|
||||
let loadingRef = ref(false)
|
||||
let loadingRef = ref(false)
|
||||
|
||||
let deviceList = ref<any>([])
|
||||
let deviceList = ref<any>([])
|
||||
|
||||
let ncList = ref<any>([])
|
||||
|
||||
function slehand(v?:any) {
|
||||
loadingRef.value = true
|
||||
yglist.value.splice(0)
|
||||
// dispatchform.userlist = [{
|
||||
// id:'',
|
||||
// num:''
|
||||
// }]
|
||||
userApi.page({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
username: v,
|
||||
}).then((rps:any) =>{
|
||||
if(rps.list && rps.list.length > 0){
|
||||
yglist.value = rps.list.map((n:any) => {
|
||||
return {
|
||||
label:n.username,
|
||||
value:n.id+''
|
||||
}
|
||||
})
|
||||
}
|
||||
console.log(yglist.value,'12')
|
||||
loadingRef.value = false
|
||||
}).catch(() => {
|
||||
loadingRef.value = false
|
||||
})
|
||||
function slehand(v?: any) {
|
||||
loadingRef.value = true
|
||||
yglist.value.splice(0)
|
||||
// dispatchform.userlist = [{
|
||||
// id:'',
|
||||
// num:''
|
||||
// }]
|
||||
userApi.page({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
username: v,
|
||||
}).then((rps: any) => {
|
||||
if (rps.list && rps.list.length > 0) {
|
||||
yglist.value = rps.list.map((n: any) => {
|
||||
return {
|
||||
label: n.username,
|
||||
value: n.id + ''
|
||||
}
|
||||
})
|
||||
}
|
||||
console.log(yglist.value, '12')
|
||||
loadingRef.value = false
|
||||
}).catch(() => {
|
||||
loadingRef.value = false
|
||||
})
|
||||
}
|
||||
|
||||
//加载工段(按 sectionId 查部门与设备)
|
||||
async function loadSection() {
|
||||
if (props.sectionId == null || props.sectionId === '') return
|
||||
const res = await sectionApi.list({sectionId: props.sectionId})
|
||||
const mesSection = res?.MesSection;
|
||||
const mesDevice = res?.MesDevice || []
|
||||
if (!mesSection) return
|
||||
//加载工段(按 sectionId 查部门与设备)
|
||||
async function loadSection() {
|
||||
if (props.sectionId == null || props.sectionId === '') return
|
||||
const res = await sectionApi.list({sectionId: props.sectionId})
|
||||
const mesSection = res?.MesSection;
|
||||
const mesDevice = res?.MesDevice || []
|
||||
if (!mesSection) return
|
||||
|
||||
sectionList.push({
|
||||
label:mesSection.deptName,
|
||||
value:mesSection.id
|
||||
})
|
||||
props.obj.sectionId = mesSection.id
|
||||
sectionList.push({
|
||||
label: mesSection.deptName,
|
||||
value: mesSection.id
|
||||
})
|
||||
props.obj.sectionId = mesSection.id
|
||||
|
||||
deviceList.value = mesDevice.map((n:any)=>{
|
||||
return {
|
||||
label:n.deviceName,
|
||||
value:n.id+''
|
||||
}
|
||||
})
|
||||
}
|
||||
deviceList.value = mesDevice.map((n: any) => {
|
||||
return {
|
||||
label: n.deviceName,
|
||||
value: n.id + ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// function searchSection(sectionId?:number){
|
||||
// if(!sectionId) return
|
||||
// props.obj.deviceId = ''
|
||||
// deviceList.value = []
|
||||
// deviceApi.list(sectionId).then(res=>{
|
||||
// deviceList.value = res.map((n:any)=>{
|
||||
// return {
|
||||
// label:n.name,
|
||||
// value:n.id+''
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
//加载下发数据
|
||||
async function loadNc() {
|
||||
const res = await loadAllNcCode()
|
||||
ncList.value = res.map((n: any) => {
|
||||
return {
|
||||
label: n.ncName,
|
||||
value: n.id
|
||||
}
|
||||
})
|
||||
console.log(ncList.value)
|
||||
}
|
||||
|
||||
|
||||
loadSection()
|
||||
slehand()
|
||||
loadNc()
|
||||
|
||||
loadSection()
|
||||
slehand()
|
||||
// handleDevice()
|
||||
</script>
|
||||
452
src/views/biz/permissionConfig/index.vue
Normal file
452
src/views/biz/permissionConfig/index.vue
Normal file
@ -0,0 +1,452 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<n-card>
|
||||
<!-- 搜索表单 -->
|
||||
<div class="search-form">
|
||||
<n-form inline :model="searchForm" label-placement="left">
|
||||
<n-form-item label="mapper">
|
||||
<n-select v-model:value="searchForm.mapperKey" placeholder="请选择mapper" clearable style="width: 300px" :options="mapperOptions"
|
||||
@update:value="mapperSearchUpdate" />
|
||||
</n-form-item>
|
||||
<n-form-item label="控制方法">
|
||||
<n-select v-model:value="searchForm.controlMethod" placeholder="请选择控制方法" clearable style="width: 200px" :options="methodSearchOptions" />
|
||||
</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="1200"
|
||||
@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" :mask-closable=false>
|
||||
<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="mapper" path="mapperKey">
|
||||
<n-select v-model:value="formData.mapperKey" placeholder="请选择mapper" :options="mapperOptions" @update:value="mapperUpdate" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="控制方法" path="controlMethod">
|
||||
<n-select v-model:value="formData.controlMethod" multiple placeholder="请选择控制方法" :options="methodOptions" />
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
<n-grid :cols="1" :x-gap="24">
|
||||
<n-form-item-gi label="权限控制方案" path="permissionControl">
|
||||
<n-dynamic-input
|
||||
v-model:value="permissionControl"
|
||||
key-field="sole"
|
||||
preset="pair"
|
||||
:min="1"
|
||||
:max="5"
|
||||
@create="handleCreate"
|
||||
>
|
||||
<template #default="{ value,index }">
|
||||
<n-space>
|
||||
<n-select
|
||||
v-model:value="value.key"
|
||||
:options="roleOptions"
|
||||
placeholder="请选择角色"
|
||||
style="width: 150px"
|
||||
/>
|
||||
<n-input
|
||||
v-model:value="value.value"
|
||||
placeholder="请输入权限内容"
|
||||
readonly
|
||||
style="width: 400px"
|
||||
@click="clickPermission(index)"
|
||||
/>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-dynamic-input>
|
||||
</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="updatePermissionModal" :draggable="true" :on-after-leave="closePermission">
|
||||
<n-card style="width: 600px" :bordered="false" size="huge" role="dialog" aria-modal="true" >
|
||||
<n-input v-model:value="updatePermissionContent" placeholder="请输入权限内容" type="textarea" />
|
||||
</n-card>
|
||||
</n-modal>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, h, onMounted } from 'vue'
|
||||
import { NButton, NSpace, NIcon, NUpload, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions } from 'naive-ui'
|
||||
import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline } from '@vicons/ionicons5'
|
||||
import { permissionConfigApi, type PermissionConfig } from '@/api/permissionConfig'
|
||||
import { roleApi, SysRole } from '@/api/system'
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
mapperKey: '',
|
||||
controlMethod: '',
|
||||
})
|
||||
|
||||
const permissionControl = ref([
|
||||
{
|
||||
sole:Date.now(),
|
||||
key: '',
|
||||
value: '123'
|
||||
}
|
||||
])
|
||||
|
||||
// 下拉选项
|
||||
const roleOptions = ref<Array<{ label: string; value: number; disabled:false }>>([])
|
||||
|
||||
const handleCreate = () => {
|
||||
return {
|
||||
sole: Date.now() + Math.random(), // key‑field使用的唯一主键
|
||||
key:"",
|
||||
value: null // select绑定的业务字段,不要和key混淆
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<PermissionConfig[]>([])
|
||||
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 formRef = ref()
|
||||
const defaultFormData: PermissionConfig = {
|
||||
mapperKey: '',
|
||||
controlMethod: '',
|
||||
permissionControl: '',
|
||||
}
|
||||
const formData = reactive<PermissionConfig>({ ...defaultFormData })
|
||||
|
||||
// 字典选项(下拉框/单选框/复选框关联字典时使用)
|
||||
|
||||
// 表单校验规则
|
||||
const formRules = {
|
||||
}
|
||||
|
||||
// 表格列
|
||||
const columns: DataTableColumns<PermissionConfig> = [
|
||||
{ type: 'selection' },
|
||||
{ title: 'mapper', key: 'mapperKey' },
|
||||
{ title: '控制方法', key: 'controlMethod' },
|
||||
{ title: '权限控制方案', key: 'permissionControl' },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 140,
|
||||
fixed: 'right',
|
||||
render(row) {
|
||||
return h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'nowrap' } }, [
|
||||
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) }), ' 删除']
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
const updatePermissionIndex = ref<number>(-1)
|
||||
const updatePermissionContent = ref<string>('')
|
||||
const updatePermissionModal = ref(false)
|
||||
|
||||
function clickPermission(index:number){
|
||||
updatePermissionIndex.value = index
|
||||
updatePermissionContent.value = permissionControl.value[index].value;
|
||||
updatePermissionModal.value = true
|
||||
}
|
||||
function closePermission(){
|
||||
permissionControl.value[updatePermissionIndex.value].value = updatePermissionContent.value;
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await permissionConfigApi.page({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
mapperKey: searchForm.mapperKey || undefined,
|
||||
controlMethod: searchForm.controlMethod || undefined,
|
||||
})
|
||||
tableData.value = res.list
|
||||
pagination.itemCount = res.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchForm.mapperKey = ''
|
||||
|
||||
searchForm.controlMethod = ''
|
||||
|
||||
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: PermissionConfig) {
|
||||
modalTitle.value = '编辑数据权限配置'
|
||||
Object.assign(formData, row)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 提交
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate()
|
||||
try {
|
||||
const submitData = { ...formData } as PermissionConfig
|
||||
if (submitData.id) {
|
||||
await permissionConfigApi.update(submitData)
|
||||
message.success('修改成功')
|
||||
} else {
|
||||
await permissionConfigApi.create(submitData)
|
||||
message.success('新增成功')
|
||||
}
|
||||
modalVisible.value = false
|
||||
loadData()
|
||||
} catch (error) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
function handleDelete(row: PermissionConfig) {
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: '确定要删除该记录吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await permissionConfigApi.delete([row.id!])
|
||||
message.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
function handleBatchDelete() {
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: `确定要删除选中的 ${selectedIds.value.length} 条记录吗?`,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await permissionConfigApi.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.mapperKey) params.mapperKey = searchForm.mapperKey
|
||||
if (searchForm.controlMethod) params.controlMethod = searchForm.controlMethod
|
||||
const blob = await permissionConfigApi.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 permissionConfigApi.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 permissionConfigApi.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) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
const mapperOptions = ref<{ label: string; value: any }[]>([])
|
||||
const methodSearchOptions = ref<{ label: string; value: any }[]>([])
|
||||
const methodOptions = ref<{ label: string; value: any }[]>([])
|
||||
function mapperUpdate(v:any,opt:any) {
|
||||
formData.controlMethod=''
|
||||
methodOptions.value = opt.childList;
|
||||
}
|
||||
|
||||
function mapperSearchUpdate(v:any,opt:any) {
|
||||
searchForm.controlMethod=''
|
||||
methodSearchOptions.value = opt.childList;
|
||||
}
|
||||
|
||||
// 加载字典选项
|
||||
async function loadDictOptions() {
|
||||
try {
|
||||
mapperOptions.value = await permissionConfigApi.options();
|
||||
}catch{}
|
||||
try {
|
||||
const roles = await roleApi.list()
|
||||
roleOptions.value = roles.map((role: SysRole) => ({
|
||||
label: role.name,
|
||||
value: role.id!,
|
||||
disabled:false
|
||||
}))
|
||||
} catch (error) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadDictOptions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@ -217,7 +217,6 @@ const columns: DataTableColumns<Device> = [
|
||||
{ type: 'selection' },
|
||||
{ title: '工位编码', key: 'deviceCode' },
|
||||
{ title: '工位名称', key: 'deviceName' },
|
||||
{ title: '工位类型', key: 'deviceType' },
|
||||
{ title: '所属工段', key: 'sectionName' },
|
||||
{ title: '状态', key: 'status',
|
||||
render:(row) =>{
|
||||
|
||||
@ -606,7 +606,6 @@ async function doLogin() {
|
||||
if (rememberMeEnabled.value) {
|
||||
loginData.rememberMe = formData.rememberMe
|
||||
}
|
||||
|
||||
await userStore.login(loginData)
|
||||
message.success('登录成功')
|
||||
const redirect = route.query.redirect as string
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
:max-height="1000"
|
||||
size="small"
|
||||
style="margin-bottom:15px"
|
||||
@update-expanded-row-keys="handleExpandChange"
|
||||
/>
|
||||
</n-scrollbar>
|
||||
|
||||
@ -105,7 +106,7 @@ import {
|
||||
|
||||
import { mytasks,type AssingWork } from '@/api/production';
|
||||
import {
|
||||
NButton, NSpace, NTag, NDataTable, DataTableColumns, NDescriptions, NDescriptionsItem
|
||||
NButton, NSpace, NTag, NDataTable, DataTableColumns, NDescriptions, NDescriptionsItem, NPagination
|
||||
} from 'naive-ui'
|
||||
import { dictDataApi } from '@/api/org'
|
||||
import { assingWorkDetailApi,AssingWorkDetail } from '@/api/AssingWorkDetail'
|
||||
@ -360,6 +361,7 @@ const AssingWorkDetailColums:DataTableColumns<AssingWorkDetail> = [
|
||||
render(row:any) {
|
||||
const val = row.status
|
||||
const opt = qcAssingWorkDetailStatusOptions.value.find(o => o.value === val || String(o.value) === String(val))
|
||||
|
||||
if (!opt) return val ?? '-'
|
||||
|
||||
return h(NTag, { type: opt.class, size: 'small' }, { default: () => opt.label })
|
||||
@ -449,48 +451,112 @@ function AssingWorkToggleExpandAll() {
|
||||
}
|
||||
|
||||
// 缓存:key=派工id,value=明细数组
|
||||
const expandDataMap = new Map<number, any[]>()
|
||||
interface DetailPageState {
|
||||
list: AssingWorkDetail[]
|
||||
page: number
|
||||
pageSize: number
|
||||
total: number
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
// key:父行id
|
||||
const expandDetailMap = new Map<number, DetailPageState>()
|
||||
|
||||
|
||||
function renderAssingWorkExpand(row: AssingWork) {
|
||||
// 1. 首次展开,发起请求
|
||||
if (!expandDataMap.has(row.id)) {
|
||||
expandDataMap.set(row.id, [])
|
||||
loadingRowIds.value.add(row.id)
|
||||
const rowId = row.id
|
||||
|
||||
// 异步请求,不阻塞当前渲染
|
||||
getAssingWorkDetail(row.id).then((detailList) => {
|
||||
expandDataMap.set(row.id, detailList)
|
||||
loadingRowIds.value.delete(row.id)
|
||||
})
|
||||
if (!expandDetailMap.has(rowId)) {
|
||||
expandDetailMap.set(rowId, reactive({
|
||||
list: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
loading: true
|
||||
}))
|
||||
loadDetail(rowId)
|
||||
}
|
||||
|
||||
// 2. 根据缓存状态渲染
|
||||
if (loadingRowIds.value.has(row.id)) {
|
||||
return h('div', { style: 'padding:12px' }, '加载中...')
|
||||
}
|
||||
const state = expandDetailMap.get(rowId)!
|
||||
console.log(state)
|
||||
console.log(state.list)
|
||||
|
||||
// 加载中提示
|
||||
// if (state.loading) {
|
||||
// return h('div', {style: 'padding:12px'}, '加载中...')
|
||||
// }
|
||||
|
||||
|
||||
const tableData = expandDataMap.get(row.id)!
|
||||
return h('div', { style: 'padding: 0 16px 12px' }, [
|
||||
h(NDataTable, {
|
||||
columns: AssingWorkDetailColums,
|
||||
data: tableData,
|
||||
data: state.list,
|
||||
rowKey: DetailRowKey,
|
||||
size: 'small',
|
||||
bordered: true,
|
||||
striped: true,
|
||||
scrollX: 1360
|
||||
})
|
||||
}),
|
||||
// ✅ 复刻你原来的分页容器 + n‑pagination + prefix插槽
|
||||
h('div', {
|
||||
class: 'pagination-container',
|
||||
style: 'display: flex; justify-content: flex-end; margin-top: 12px'
|
||||
}, [
|
||||
h(NPagination, {
|
||||
page: state.page,
|
||||
pageSize: state.pageSize,
|
||||
itemCount: state.total,
|
||||
onUpdatePage: (p: number) => handleDetailPageChange(rowId, p),
|
||||
onUpdatePageSize: (sz: number) => handleDetailPageSizeChange(rowId, sz)
|
||||
}, {
|
||||
// ✅ #prefix 插槽
|
||||
prefix: () => h('span', `共 ${state.total} 条`)
|
||||
})
|
||||
])
|
||||
])
|
||||
}
|
||||
// 分页切换事件,点击页码/每页大小
|
||||
async function handleDetailPageChange(rowId: number,page:number) {
|
||||
const state = expandDetailMap.get(rowId)
|
||||
if (!state) return
|
||||
state.page = page
|
||||
loadDetail(rowId)
|
||||
}
|
||||
|
||||
async function handleDetailPageSizeChange(rowId: number,pageSize: number) {
|
||||
const state = expandDetailMap.get(rowId)
|
||||
if (!state) return
|
||||
state.pageSize = pageSize
|
||||
state.page = 1
|
||||
loadDetail(rowId)
|
||||
}
|
||||
|
||||
function handleExpandChange(openKeys: number[]){
|
||||
for (const [rowId] of expandDetailMap) {
|
||||
if (!openKeys.includes(rowId)) {
|
||||
expandDetailMap.delete(rowId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function getAssingWorkDetail(assingWorkId:number) {
|
||||
const res = await assingWorkDetailApi.selectAssingWorkDetailListByAssingWorkId(assingWorkId)
|
||||
return res
|
||||
}
|
||||
|
||||
const loadDetail = async (rowId: number)=>{
|
||||
const state = expandDetailMap.get(rowId)
|
||||
if (!state) return
|
||||
state.loading = true
|
||||
try {
|
||||
const res = await assingWorkDetailApi.selectAssingWorkDetailListByAssingWorkId({
|
||||
assingWorkId: rowId,
|
||||
page: state.page,
|
||||
pageSize: state.pageSize
|
||||
})
|
||||
state.list = res.list
|
||||
state.total = res.total
|
||||
}finally {
|
||||
state.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载字典选项
|
||||
async function loadDictOptions() {
|
||||
|
||||
@ -31,12 +31,27 @@
|
||||
remote
|
||||
:scroll-x="600"
|
||||
/>
|
||||
|
||||
<div class="pagination-container" style="display: flex; justify-content: flex-end; margin-top: 12px">
|
||||
<n-pagination
|
||||
v-model:page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:item-count="pagination.itemCount"
|
||||
:page-sizes="[2,10, 20, 50, 100]"
|
||||
show-size-picker
|
||||
show-quick-jumper
|
||||
@update:page="handlePageChange"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
>
|
||||
<template #prefix>
|
||||
共 {{ pagination.itemCount }} 条
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
</n-card>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { NCard, NDataTable, NGi,NTag, NGrid,DataTableColumns} from "naive-ui";
|
||||
import {onMounted, ref,h} from "vue";
|
||||
import {onMounted, ref, h, reactive} from "vue";
|
||||
import { dictDataApi } from '@/api/org'
|
||||
|
||||
import {QualityTesting,qualityTestingApi} from "@/api/qualityTesting.ts";
|
||||
@ -47,6 +62,12 @@ const props =defineProps(
|
||||
baseInfo:Object
|
||||
}
|
||||
)
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0
|
||||
})
|
||||
|
||||
|
||||
//业务来源字典
|
||||
const qcSourceTypeOptions = ref<{ label: string; value: any;class:any }[]>([])
|
||||
@ -93,8 +114,24 @@ async function loadDictOptions() {
|
||||
|
||||
const qualityTestingList = ref<QualityTesting[]>([])
|
||||
async function loadQualityTestingList() {
|
||||
const res = await qualityTestingApi.loadQualityTestingList(props.assingId)
|
||||
qualityTestingList.value = res
|
||||
const res = await qualityTestingApi.loadQualityTestingList({
|
||||
assingId:props.assingId,
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
qualityTestingList.value = res.list
|
||||
pagination.itemCount = res.total
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
pagination.page = page
|
||||
loadQualityTestingList()
|
||||
}
|
||||
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
pagination.pageSize = pageSize
|
||||
pagination.page = 1
|
||||
loadQualityTestingList()
|
||||
}
|
||||
|
||||
onMounted(()=>{
|
||||
|
||||
@ -32,11 +32,27 @@
|
||||
:scroll-x="600"
|
||||
/>
|
||||
|
||||
<div class="pagination-container" style="display: flex; justify-content: flex-end; margin-top: 12px">
|
||||
<n-pagination
|
||||
v-model:page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:item-count="pagination.itemCount"
|
||||
:page-sizes="[2,10, 20, 50, 100]"
|
||||
show-size-picker
|
||||
show-quick-jumper
|
||||
@update:page="handlePageChange"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
>
|
||||
<template #prefix>
|
||||
共 {{ pagination.itemCount }} 条
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
</n-card>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { NCard, NDataTable, NGi,NTag, NGrid,DataTableColumns} from "naive-ui";
|
||||
import {onMounted, ref,h} from "vue";
|
||||
import {onMounted, ref, h, reactive} from "vue";
|
||||
import { dictDataApi } from '@/api/org'
|
||||
import { SubmitLog,submitLogApi} from '@/api/submitLog'
|
||||
|
||||
@ -51,6 +67,12 @@ const props =defineProps(
|
||||
const submitStatusOptions = ref<{ label: string; value: any ;class:any}[]>([])
|
||||
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0
|
||||
})
|
||||
|
||||
//汇报信息表格
|
||||
const reportRecordsColums:DataTableColumns<SubmitLog> = [
|
||||
|
||||
@ -79,8 +101,24 @@ async function loadDictOptions() {
|
||||
//加载汇报信息
|
||||
const submitLogList = ref<SubmitLog[]>([])
|
||||
async function load() {
|
||||
const res = await submitLogApi.loadSumbitLog(props.assingId)
|
||||
submitLogList.value = res
|
||||
const res = await submitLogApi.loadSumbitLog({
|
||||
assingId:props.assingId,
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
submitLogList.value = res.list
|
||||
pagination.itemCount = res.total
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
pagination.page = page
|
||||
load()
|
||||
}
|
||||
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
pagination.pageSize = pageSize
|
||||
pagination.page = 1
|
||||
load()
|
||||
}
|
||||
|
||||
onMounted(()=>{
|
||||
|
||||
@ -16,16 +16,16 @@ export default defineConfig({
|
||||
emptyOutDir: true
|
||||
},
|
||||
server: {
|
||||
// host:'192.168.12.11', //true,
|
||||
host:'192.168.12.4', //true,
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
//target:'http://192.168.12.230:8888/', //'http://192.168.5.230:8888',
|
||||
target:'http://192.168.12.4:8888/',
|
||||
//target:'http://192.168.12.4:8888/',
|
||||
// target:'http://192.168.12.4:8888/',
|
||||
//target:'http://192.168.12.230:8888',
|
||||
//target:'http://192.168.5.230:8888',
|
||||
//target:'http://192.168.5.232:8888/',
|
||||
//target:'http://localhost:8888',
|
||||
target:'http://localhost:8888',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/druid': {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user