修改
This commit is contained in:
commit
1382a19d3d
@ -20,7 +20,7 @@ export interface ConcessionApply {
|
||||
|
||||
instanceId?: string
|
||||
|
||||
approveStatus?: number
|
||||
approveStatus?: string
|
||||
|
||||
createTime?: string
|
||||
|
||||
@ -61,7 +61,7 @@ export const concessionApplyApi = {
|
||||
},
|
||||
|
||||
// 删除
|
||||
delete(ids: string[]) {
|
||||
delete(ids: number[]) {
|
||||
return request({ url: `/biz/concessionApply/${ids.join(',')}`, method: 'delete' })
|
||||
},
|
||||
|
||||
|
||||
@ -1,20 +1,24 @@
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
// 工序计划 类型定义
|
||||
|
||||
|
||||
/** 工序计划实体(新增/修改/派工) */
|
||||
|
||||
export interface OrderProcessPlan {
|
||||
|
||||
id?: number
|
||||
|
||||
name?: string
|
||||
name?: string
|
||||
|
||||
code?: string
|
||||
code?: string
|
||||
|
||||
sort?: number
|
||||
|
||||
orderItemId?: number
|
||||
|
||||
beginTime?: string
|
||||
beginTime?: string
|
||||
|
||||
endTime?: string
|
||||
endTime?: string
|
||||
|
||||
procurement?: number
|
||||
|
||||
@ -26,9 +30,9 @@ export interface OrderProcessPlan {
|
||||
|
||||
transferOutNum?: number
|
||||
|
||||
createTime?: string
|
||||
createTime?: string
|
||||
|
||||
createBy?: string
|
||||
createBy?: string
|
||||
|
||||
qualityInspection?: number
|
||||
|
||||
@ -36,63 +40,407 @@ export interface OrderProcessPlan {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** 派工明细 */
|
||||
|
||||
export interface AssignWorkItem {
|
||||
|
||||
userId?: number | string
|
||||
|
||||
quantity?: number | string
|
||||
|
||||
assingStatus?: number
|
||||
|
||||
[key: string]: unknown
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** 工序计划明细 VO(processList 项) */
|
||||
|
||||
export interface ProcessPlanItemVO {
|
||||
|
||||
planId?: number
|
||||
|
||||
processCode?: string
|
||||
|
||||
operNumber?: number
|
||||
|
||||
workCenterName?: string
|
||||
|
||||
departmentName?: string
|
||||
|
||||
processName?: string
|
||||
|
||||
operDescription?: string
|
||||
|
||||
activityUnit?: string
|
||||
|
||||
optCtrlCodeName?: string
|
||||
|
||||
activityQty?: number
|
||||
|
||||
planStartTime?: string
|
||||
|
||||
planFinishTime?: string
|
||||
|
||||
actualStartTime?: string | null
|
||||
|
||||
actualFinishTime?: string | null
|
||||
|
||||
quantity?: number
|
||||
|
||||
transferInQty?: number
|
||||
|
||||
transferOutQty?: number
|
||||
|
||||
completedQty?: number
|
||||
|
||||
scrapQty?: number
|
||||
|
||||
reworkQty?: number
|
||||
|
||||
procurement?: number
|
||||
|
||||
outsource?: number
|
||||
|
||||
qualityInspection?: number
|
||||
|
||||
storageEntry?: number
|
||||
|
||||
assign?: number
|
||||
|
||||
assignByName?: string | null
|
||||
|
||||
assignTime?: string | null
|
||||
|
||||
processStatus?: number
|
||||
|
||||
prevPlanId?: number | null
|
||||
|
||||
nextPlanId?: number | null
|
||||
|
||||
assignWorkList?: AssignWorkItem[]
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** 按生产订单分组的工序计划 VO(分页/详情) */
|
||||
|
||||
export interface OrderProcessPlanVO {
|
||||
|
||||
orderItemId?: number
|
||||
|
||||
mainCode?: string
|
||||
|
||||
materialCode?: string
|
||||
|
||||
materialName?: string
|
||||
|
||||
orderCode?: string
|
||||
|
||||
quantity?: number
|
||||
|
||||
routeCode?: string
|
||||
|
||||
proWorkshop?: string
|
||||
|
||||
beginTime?: string
|
||||
|
||||
endTime?: string
|
||||
|
||||
actualStartTime?: string | null
|
||||
|
||||
actualEndTime?: string | null
|
||||
|
||||
completedQty?: number
|
||||
|
||||
orderStatus?: number
|
||||
|
||||
currentProcessName?: string
|
||||
|
||||
currentOperNumber?: number
|
||||
|
||||
processList?: ProcessPlanItemVO[]
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const ORDER_STATUS_MAP: Record<number, { label: string; type: 'default' | 'info' | 'success' | 'warning' | 'error' }> = {
|
||||
|
||||
0: { label: '未开始', type: 'default' },
|
||||
|
||||
1: { label: '进行中', type: 'info' },
|
||||
|
||||
2: { label: '已完成', type: 'success' },
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const PROCESS_STATUS_MAP: Record<number, { label: string; type: 'default' | 'info' | 'success' | 'warning' | 'error' }> = {
|
||||
|
||||
0: { label: '待派工', type: 'default' },
|
||||
|
||||
1: { label: '待执行', type: 'info' },
|
||||
|
||||
2: { label: '执行中', type: 'warning' },
|
||||
|
||||
3: { label: '质检中', type: 'warning' },
|
||||
|
||||
4: { label: '已完成', type: 'success' },
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const ASSIGN_STATUS_MAP: Record<number, { label: string; type: 'default' | 'info' | 'success' | 'warning' | 'error' }> = {
|
||||
|
||||
1: { label: '待执行', type: 'info' },
|
||||
|
||||
2: { label: '执行中', type: 'warning' },
|
||||
|
||||
3: { label: '质检中', type: 'warning' },
|
||||
|
||||
4: { label: '已汇报', type: 'success' },
|
||||
|
||||
5: { label: '已关闭', type: 'default' },
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** 工序 VO 转实体(编辑/派工提交用) */
|
||||
|
||||
export function processItemToEntity(
|
||||
|
||||
item: ProcessPlanItemVO,
|
||||
|
||||
orderItemId?: number,
|
||||
|
||||
): OrderProcessPlan {
|
||||
|
||||
return {
|
||||
|
||||
id: item.planId,
|
||||
|
||||
name: item.processName,
|
||||
|
||||
code: item.processCode,
|
||||
|
||||
sort: item.operNumber,
|
||||
|
||||
orderItemId,
|
||||
|
||||
beginTime: item.planStartTime,
|
||||
|
||||
endTime: item.planFinishTime,
|
||||
|
||||
procurement: item.procurement,
|
||||
|
||||
outsource: item.outsource,
|
||||
|
||||
quantity: item.quantity,
|
||||
|
||||
transferNum: item.transferInQty,
|
||||
|
||||
transferOutNum: item.transferOutQty,
|
||||
|
||||
qualityInspection: item.qualityInspection,
|
||||
|
||||
storageEntry: item.storageEntry,
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** 提交前剔除展示字段,保留 OrderProcessPlan 实体 */
|
||||
|
||||
export function toOrderProcessPlanPayload(data: Record<string, unknown>): OrderProcessPlan {
|
||||
|
||||
const payload = { ...data }
|
||||
|
||||
const displayKeys = [
|
||||
|
||||
'mainCode', 'materialCode', 'materialName', 'orderCode', 'routeCode', 'proWorkshop',
|
||||
|
||||
'actualStartTime', 'actualEndTime', 'completedQty', 'orderStatus', 'currentProcessName',
|
||||
|
||||
'currentOperNumber', 'processList', 'planId', 'processName', 'processCode', 'operNumber',
|
||||
|
||||
'workCenterName', 'departmentName', 'operDescription', 'activityUnit', 'optCtrlCodeName',
|
||||
|
||||
'activityQty', 'planStartTime', 'planFinishTime', 'actualFinishTime', 'transferInQty',
|
||||
|
||||
'transferOutQty', 'scrapQty', 'reworkQty', 'assign', 'assignByName', 'assignTime',
|
||||
|
||||
'processStatus', 'prevPlanId', 'nextPlanId', 'assignWorkList',
|
||||
|
||||
]
|
||||
|
||||
for (const key of displayKeys) {
|
||||
|
||||
delete payload[key]
|
||||
|
||||
}
|
||||
|
||||
return payload as OrderProcessPlan
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 工序计划 API
|
||||
|
||||
export const orderProcessPlanApi = {
|
||||
// 分页查询
|
||||
page(params:any) { //{ page: number; pageSize: number; id?: number; name?: string; code?: string; orderItemId?: number }
|
||||
return request({ url: '/biz/orderProcessPlan/page', method: 'get', params })
|
||||
|
||||
// 分页查询(按生产订单分组)
|
||||
|
||||
page(params: {
|
||||
|
||||
page: number
|
||||
|
||||
pageSize: number
|
||||
|
||||
id?: number | string
|
||||
|
||||
name?: string
|
||||
|
||||
code?: string
|
||||
|
||||
orderItemId?: number | string
|
||||
|
||||
}) {
|
||||
|
||||
return request<{ list: OrderProcessPlanVO[]; total: number; page: number; pageSize: number }>({
|
||||
|
||||
url: '/biz/orderProcessPlan/page',
|
||||
|
||||
method: 'get',
|
||||
|
||||
params,
|
||||
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
// 获取详情
|
||||
detail(id: string) {
|
||||
return request({ url: `/biz/orderProcessPlan/${id}`, method: 'get' })
|
||||
|
||||
|
||||
// 获取详情(路径参数为生产订单 id)
|
||||
|
||||
detail(orderItemId: number | string) {
|
||||
|
||||
return request<OrderProcessPlanVO>({
|
||||
|
||||
url: `/biz/orderProcessPlan/${orderItemId}`,
|
||||
|
||||
method: 'get',
|
||||
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
// 新增
|
||||
create(data: any) {
|
||||
|
||||
create(data: OrderProcessPlan) {
|
||||
|
||||
return request({ url: '/biz/orderProcessPlan', method: 'post', data })
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
// 修改
|
||||
update(data: any) {
|
||||
|
||||
update(data: OrderProcessPlan) {
|
||||
|
||||
return request({ url: '/biz/orderProcessPlan', method: 'put', data })
|
||||
|
||||
},
|
||||
|
||||
// 删除
|
||||
delete(ids: string[]) {
|
||||
|
||||
|
||||
// 删除(工序计划 id)
|
||||
|
||||
delete(ids: (number | string)[]) {
|
||||
|
||||
return request({ url: `/biz/orderProcessPlan/${ids.join(',')}`, method: 'delete' })
|
||||
|
||||
},
|
||||
|
||||
//派工
|
||||
assignWork(data: any) {
|
||||
|
||||
|
||||
// 派工
|
||||
|
||||
assignWork(data: OrderProcessPlan & { list?: AssignWorkItem[] }) {
|
||||
|
||||
return request({ url: '/biz/orderProcessPlan/assignWork', method: 'put', data })
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
// 导出
|
||||
export(params?: { ids?: string[]; id?: number; name?: string; code?: string; orderItemId?: number }) {
|
||||
const p: Record<string, any> = {}
|
||||
|
||||
export(params?: { ids?: string[]; id?: number; name?: string; code?: string; orderItemId?: number }) {
|
||||
|
||||
const p: Record<string, unknown> = {}
|
||||
|
||||
if (params?.ids?.length) p.ids = params.ids.join(',')
|
||||
|
||||
if (params?.id !== undefined && params?.id !== null) p.id = params.id
|
||||
|
||||
if (params?.name !== undefined && params?.name !== null) p.name = params.name
|
||||
|
||||
if (params?.code !== undefined && params?.code !== null) p.code = params.code
|
||||
|
||||
if (params?.orderItemId !== undefined && params?.orderItemId !== null) p.orderItemId = params.orderItemId
|
||||
return request({ url: `/biz/orderProcessPlan/export`, method: 'get', params: p, responseType: 'blob' })
|
||||
|
||||
return request({ url: '/biz/orderProcessPlan/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/orderProcessPlan/import`,
|
||||
|
||||
url: '/biz/orderProcessPlan/import',
|
||||
|
||||
method: 'post',
|
||||
|
||||
data: formData,
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
// 下载导入模板
|
||||
|
||||
downloadTemplate() {
|
||||
return request({ url: `/biz/orderProcessPlan/template`, method: 'get', responseType: 'blob' })
|
||||
}
|
||||
|
||||
return request({ url: '/biz/orderProcessPlan/template', method: 'get', responseType: 'blob' })
|
||||
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
88
src/api/processRoute.ts
Normal file
88
src/api/processRoute.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
// 工艺路线主表 类型定义
|
||||
export interface ProcessRoute {
|
||||
id?: number
|
||||
|
||||
code?: string
|
||||
|
||||
name?: string
|
||||
|
||||
materialCode?: string
|
||||
|
||||
materialName?: string
|
||||
|
||||
version?: string
|
||||
|
||||
status?: number
|
||||
|
||||
source?: number
|
||||
|
||||
sourceId?: string
|
||||
|
||||
remark?: string
|
||||
|
||||
createTime?: string
|
||||
|
||||
createBy?: string
|
||||
|
||||
changeTime?: string
|
||||
|
||||
changeBy?: string
|
||||
|
||||
}
|
||||
|
||||
// 工艺路线主表 API
|
||||
export const processRouteApi = {
|
||||
// 分页查询
|
||||
page(params: { page: number; pageSize: number; id?: number; name?: string; status?: number }) {
|
||||
return request({ url: '/biz/processRoute/page', method: 'get', params })
|
||||
},
|
||||
|
||||
// 获取详情
|
||||
detail(id: string) {
|
||||
return request({ url: `/biz/processRoute/${id}`, method: 'get' })
|
||||
},
|
||||
|
||||
// 新增
|
||||
create(data: ProcessRoute) {
|
||||
return request({ url: '/biz/processRoute', method: 'post', data })
|
||||
},
|
||||
|
||||
// 修改
|
||||
update(data: ProcessRoute) {
|
||||
return request({ url: '/biz/processRoute', method: 'put', data })
|
||||
},
|
||||
|
||||
// 删除
|
||||
delete(ids: string[]) {
|
||||
return request({ url: `/biz/processRoute/${ids.join(',')}`, method: 'delete' })
|
||||
},
|
||||
|
||||
// 导出
|
||||
export(params?: { ids?: string[]; id?: number; name?: string; status?: number }) {
|
||||
const p: Record<string, any> = {}
|
||||
if (params?.ids?.length) p.ids = params.ids.join(',')
|
||||
if (params?.id !== undefined && params?.id !== null) p.id = params.id
|
||||
if (params?.name !== undefined && params?.name !== null) p.name = params.name
|
||||
if (params?.status !== undefined && params?.status !== null) p.status = params.status
|
||||
return request({ url: `/biz/processRoute/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/processRoute/import`,
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
},
|
||||
|
||||
// 下载导入模板
|
||||
downloadTemplate() {
|
||||
return request({ url: `/biz/processRoute/template`, method: 'get', responseType: 'blob' })
|
||||
}
|
||||
}
|
||||
89
src/api/processRouteStep.ts
Normal file
89
src/api/processRouteStep.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
// 工艺路线工序明细 类型定义
|
||||
export interface ProcessRouteStep {
|
||||
id?: number
|
||||
|
||||
routeId?: number
|
||||
|
||||
operNumber?: number
|
||||
|
||||
processName?: string
|
||||
|
||||
operDescription?: string
|
||||
|
||||
workCenterName?: string
|
||||
|
||||
departmentName?: string
|
||||
|
||||
activityUnit?: string
|
||||
|
||||
optCtrlCodeName?: string
|
||||
|
||||
activityQty?: string
|
||||
|
||||
sort?: number
|
||||
|
||||
isOutsource?: number
|
||||
|
||||
status?: number
|
||||
|
||||
createTime?: string
|
||||
|
||||
changeTime?: string
|
||||
|
||||
}
|
||||
|
||||
// 工艺路线工序明细 API
|
||||
export const processRouteStepApi = {
|
||||
// 分页查询
|
||||
page(params: { page: number; pageSize: number; id?: number; status?: number }) {
|
||||
return request({ url: '/biz/processRouteStep/page', method: 'get', params })
|
||||
},
|
||||
|
||||
// 获取详情
|
||||
detail(id: number) {
|
||||
return request({ url: `/biz/processRouteStep/${id}`, method: 'get' })
|
||||
},
|
||||
|
||||
// 新增
|
||||
create(data: ProcessRouteStep) {
|
||||
return request({ url: '/biz/processRouteStep', method: 'post', data })
|
||||
},
|
||||
|
||||
// 修改
|
||||
update(data: ProcessRouteStep) {
|
||||
return request({ url: '/biz/processRouteStep', method: 'put', data })
|
||||
},
|
||||
|
||||
// 删除
|
||||
delete(ids: number[]) {
|
||||
return request({ url: `/biz/processRouteStep/${ids.join(',')}`, method: 'delete' })
|
||||
},
|
||||
|
||||
// 导出
|
||||
export(params?: { ids?: number[]; id?: number; status?: number }) {
|
||||
const p: Record<string, any> = {}
|
||||
if (params?.ids?.length) p.ids = params.ids.join(',')
|
||||
if (params?.id !== undefined && params?.id !== null) p.id = params.id
|
||||
if (params?.status !== undefined && params?.status !== null) p.status = params.status
|
||||
return request({ url: `/biz/processRouteStep/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/processRouteStep/import`,
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
},
|
||||
|
||||
// 下载导入模板
|
||||
downloadTemplate() {
|
||||
return request({ url: `/biz/processRouteStep/template`, method: 'get', responseType: 'blob' })
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,7 @@ export interface QcResultDetail {
|
||||
|
||||
resultType?: number
|
||||
|
||||
qty?: string
|
||||
qty?: number
|
||||
|
||||
traceCode?: string
|
||||
|
||||
|
||||
@ -14,6 +14,14 @@ export interface SubmitLog {
|
||||
|
||||
createBy?: string
|
||||
|
||||
materialName?:string
|
||||
|
||||
materialCode?:string
|
||||
|
||||
materialQty?:number
|
||||
|
||||
assingStatus?: number
|
||||
|
||||
}
|
||||
|
||||
// 派工工单汇报记录 API
|
||||
@ -39,7 +47,7 @@ export const submitLogApi = {
|
||||
},
|
||||
|
||||
// 删除
|
||||
delete(ids: string[]) {
|
||||
delete(ids: number[]) {
|
||||
return request({ url: `/biz/submitLog/${ids.join(',')}`, method: 'delete' })
|
||||
},
|
||||
|
||||
|
||||
@ -1,31 +1,31 @@
|
||||
<!--
|
||||
站点 Logo 展示:优先显示图片,无 URL 或加载失败时回退为站点名称首字母。
|
||||
用于 layout、login、register 等页面;src 不传则使用 siteStore.siteLogo。
|
||||
-->
|
||||
<template>
|
||||
<img
|
||||
v-if="showSiteLogoImg"
|
||||
:src="siteLogo"
|
||||
:class="imgClass"
|
||||
alt="Logo"
|
||||
@error="handleLogoError"
|
||||
/>
|
||||
<div v-else :class="iconClass" :style="iconStyle">{{ fallbackLetter }}</div>
|
||||
</template>
|
||||
<!--
|
||||
站点 Logo 展示:优先显示图片,无 URL 或加载失败时回退为站点名称首字母。
|
||||
用于 layout、login、register 等页面;src 不传则使用 siteStore.siteLogo。
|
||||
-->
|
||||
<template>
|
||||
<img
|
||||
v-if="showSiteLogoImg"
|
||||
:src="siteLogo"
|
||||
:class="imgClass"
|
||||
alt="Logo"
|
||||
@error="handleLogoError"
|
||||
/>
|
||||
<div v-else :class="iconClass" :style="iconStyle">{{ fallbackLetter }}</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSiteLogo } from '@/composables/useSiteLogo'
|
||||
<script setup lang="ts">
|
||||
import { useSiteLogo } from '@/composables/useSiteLogo'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
/** 自定义 Logo 地址,默认取 siteStore.siteLogo */
|
||||
src?: string
|
||||
/** 图片模式下的 class */
|
||||
imgClass?: string
|
||||
/** 首字母 fallback 模式下的 class */
|
||||
iconClass?: string
|
||||
/** 首字母 fallback 的内联样式 */
|
||||
iconStyle?: Record<string, string>
|
||||
}>(), {
|
||||
const props = withDefaults(defineProps<{
|
||||
/** 自定义 Logo 地址,默认取 siteStore.siteLogo */
|
||||
src?: string
|
||||
/** 图片模式下的 class */
|
||||
imgClass?: string
|
||||
/** 首字母 fallback 模式下的 class */
|
||||
iconClass?: string
|
||||
/** 首字母 fallback 的内联样式 */
|
||||
iconStyle?: Record<string, string>
|
||||
}>(), {
|
||||
imgClass: 'logo-img',
|
||||
iconClass: 'logo-icon',
|
||||
})
|
||||
|
||||
69
src/utils/kingdeeSchedule.ts
Normal file
69
src/utils/kingdeeSchedule.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { normalizeKingdeeProcessRoute, type KingdeePrdMo, type KingdeeProcessRoute } from '@/api/orderProject'
|
||||
|
||||
export type KingdeeMoTableRow = Omit<KingdeePrdMo, 'children'> & { processRoutes: KingdeeProcessRoute[] }
|
||||
|
||||
export function mapKingdeeMoListToTableRows(data: KingdeePrdMo[]): KingdeeMoTableRow[] {
|
||||
return (Array.isArray(data) ? data : []).map((mo) => {
|
||||
const { children, ...rest } = mo
|
||||
const routes = [...(children ?? [])]
|
||||
.sort((a, b) => (a.operNumber ?? 0) - (b.operNumber ?? 0))
|
||||
.map((r) => normalizeKingdeeProcessRoute({
|
||||
...r,
|
||||
planStartTime: r.planStartTime ?? null,
|
||||
planFinishTime: r.planFinishTime ?? null,
|
||||
}))
|
||||
return { ...rest, processRoutes: routes }
|
||||
})
|
||||
}
|
||||
|
||||
export function tableRowsToKingdeeDraft(rows: KingdeeMoTableRow[]): KingdeePrdMo[] {
|
||||
return rows.map((mo) => {
|
||||
const { processRoutes, ...rest } = mo
|
||||
return { ...rest, children: processRoutes } as KingdeePrdMo
|
||||
})
|
||||
}
|
||||
|
||||
function parsePlanTime(val: string | null | undefined): number | null {
|
||||
if (!val) return null
|
||||
const ts = new Date(val.replace(' ', 'T')).getTime()
|
||||
return Number.isNaN(ts) ? null : ts
|
||||
}
|
||||
|
||||
export function validateKingdeeSchedule(rows: KingdeeMoTableRow[]): string[] {
|
||||
const errors: string[] = []
|
||||
if (!rows.length) {
|
||||
errors.push('没有可同步的生产订单数据')
|
||||
return errors
|
||||
}
|
||||
|
||||
rows.forEach((mo, moIdx) => {
|
||||
const moLabel = mo.billNo || mo.productionOrderNo || `第 ${moIdx + 1} 条`
|
||||
const moStart = parsePlanTime(mo.planStartTime)
|
||||
const moEnd = parsePlanTime(mo.planFinishTime)
|
||||
if (moStart == null || moEnd == null) {
|
||||
errors.push(`生产订单 ${moLabel}:计划开工/完工时间未填写`)
|
||||
} else if (moEnd <= moStart) {
|
||||
errors.push(`生产订单 ${moLabel}:计划完工须晚于计划开工`)
|
||||
}
|
||||
|
||||
const routes = mo.processRoutes ?? []
|
||||
if (!routes.length) {
|
||||
errors.push(`生产订单 ${moLabel}:没有工序计划`)
|
||||
}
|
||||
|
||||
routes.forEach((route, ri) => {
|
||||
const routeLabel = route.operNumber ?? ri + 1
|
||||
const rStart = parsePlanTime(route.planStartTime)
|
||||
const rEnd = parsePlanTime(route.planFinishTime)
|
||||
if (rStart == null || rEnd == null) {
|
||||
errors.push(`生产订单 ${moLabel} 工序 ${routeLabel}:计划开始/结束时间未填写`)
|
||||
} else if (rEnd <= rStart) {
|
||||
errors.push(`生产订单 ${moLabel} 工序 ${routeLabel}:结束时间须晚于开始时间`)
|
||||
} else if (moStart != null && moEnd != null && (rStart < moStart || rEnd > moEnd)) {
|
||||
errors.push(`生产订单 ${moLabel} 工序 ${routeLabel}:计划时间超出生产订单时间范围`)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return errors
|
||||
}
|
||||
@ -394,9 +394,7 @@ const columns: DataTableColumns<ConcessionApply> = [
|
||||
{ title: '审批完成时间', key: 'approveTime',align:"center",minWidth:"250" },
|
||||
{ title: '审批状态', key: 'approveStatus',align:"center",minWidth:"150" },
|
||||
{ title: '审批结果', key: 'approveResult',align:"center",minWidth:"150",
|
||||
render(row:any){
|
||||
console.log(row.approveResult);
|
||||
|
||||
render(row:any){
|
||||
if(row.approveResult == '待审批') return h(NTag, { type: 'default', size: 'small' }, { default: () => row.approveResult })
|
||||
if(row.approveResult == '已驳回') return h(NTag, { type: 'error', size: 'small' }, { default: () => row.approveResult })
|
||||
if(row.approveResult == '已通过') return h(NTag, { type: 'success', size: 'small' }, { default: () => row.approveResult })
|
||||
@ -649,26 +647,9 @@ async function loadDictOptions() {
|
||||
approveStatusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class: d.listClass }))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const getTimelineType = (type) => {
|
||||
console.log(type);
|
||||
|
||||
const map = {
|
||||
'START_PROCESS_INSTANCE': 'info', //发起-蓝色
|
||||
'AGREE': 'success',//通过-绿色
|
||||
'REFUSE': 'error',//驳回-红色
|
||||
'TERMINATE_PROCESS_INSTANCE':'warning',//撤回-橙黄
|
||||
'grey':'default', //兼容grey
|
||||
'info':'default'
|
||||
}
|
||||
return map[type] || 'default'
|
||||
}
|
||||
const getTitleAttribute = (item) =>{
|
||||
return item.username +" "+item.operationResult
|
||||
}
|
||||
|
||||
|
||||
function getApproveResultColor(status) {
|
||||
function getApproveResultColor(status:string) {
|
||||
switch(status){
|
||||
case '已通过':
|
||||
return "green"
|
||||
@ -679,11 +660,6 @@ function getApproveResultColor(status) {
|
||||
}
|
||||
}
|
||||
|
||||
const getRemark=(item) =>{
|
||||
console.log(item.remark);
|
||||
|
||||
}
|
||||
|
||||
async function downloadOutline() {
|
||||
const model = detailModel.value
|
||||
if (!model) {
|
||||
@ -827,7 +803,7 @@ async function downloadOutline() {
|
||||
}
|
||||
|
||||
|
||||
async function handleCancel(row) {
|
||||
async function handleCancel(row:any) {
|
||||
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
|
||||
@ -225,8 +225,20 @@
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="派工接收人" path="assingWorkId">
|
||||
<n-input v-model:value="formData.assingWorkId" placeholder="请输入派工接收人" />
|
||||
<n-form-item label="派工接收人" path="assingWorkList">
|
||||
<!-- <n-input v-model:value="formData.assingWorkId" placeholder="请选择派工接收人" /> -->
|
||||
<n-select
|
||||
multiple
|
||||
filterable
|
||||
v-model:value="formData.assingWorkList"
|
||||
placeholder="请输入用户名称搜索"
|
||||
:options="yglist"
|
||||
:loading="loadingRef"
|
||||
clearable
|
||||
remote
|
||||
:clear-filter-after-select="false"
|
||||
@search="slehand"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
@ -314,17 +326,16 @@
|
||||
<n-form-item label="工单名称">
|
||||
<n-input v-model:value="dispatchform.name" disabled />
|
||||
</n-form-item>
|
||||
<n-form-item label="指派员工" path="userid">
|
||||
<n-form-item label="指派员工" path="assingWorkList">
|
||||
<!-- <n-select
|
||||
v-model:value="dispatchform.userid"
|
||||
:options="yglist"
|
||||
clearable
|
||||
placeholder="请选择员工"
|
||||
/> -->
|
||||
|
||||
<!--v-model:value="dispatchform.assingWorkList"-->
|
||||
<n-select
|
||||
v-model:value="dispatchform.userid"
|
||||
|
||||
multiple
|
||||
filterable
|
||||
placeholder="请输入用户名称搜索"
|
||||
:options="yglist"
|
||||
@ -333,6 +344,7 @@
|
||||
remote
|
||||
:clear-filter-after-select="false"
|
||||
@search="slehand"
|
||||
@update:value="updatesle"
|
||||
/>
|
||||
|
||||
<!-- <n-input v-model:value="dispatchform.userid" placeholder="请选择员工" /> -->
|
||||
@ -441,14 +453,15 @@ const defaultFormData = {
|
||||
projectId: '',
|
||||
projectName:'',
|
||||
parentId: '',
|
||||
sfProduct: '',
|
||||
procurement: '',
|
||||
sfProduct: 0,
|
||||
procurement: 0,
|
||||
beginTime: null,
|
||||
endTime: null,
|
||||
actualStartTime: null,
|
||||
actualEndTime: null,
|
||||
assingWorkId: '',
|
||||
assingWorkTime: null,
|
||||
assingWorkList:[],
|
||||
assingWorkTime: null,
|
||||
assingWorkOperationId: '',
|
||||
assingWorkOperationTime: null,
|
||||
starter: '',
|
||||
@ -881,6 +894,17 @@ function handleEdit(row: OrderItem) {
|
||||
modalVisible.value = true
|
||||
planList.value.splice(0)
|
||||
Object.assign(formData, row)
|
||||
if(formData.sfProduct == null){
|
||||
formData.sfProduct = 0
|
||||
}
|
||||
if(formData.procurement == null){
|
||||
formData.procurement = 0
|
||||
}
|
||||
|
||||
if(formData.assingWorkId){
|
||||
formData.assingWorkList = formData.assingWorkId.split(',')
|
||||
}
|
||||
|
||||
if (formData.beginTime && typeof formData.beginTime === 'string') {
|
||||
formData.beginTime = new Date(formData.beginTime.replace(' ', 'T')).getTime()
|
||||
}
|
||||
@ -902,7 +926,9 @@ function handleEdit(row: OrderItem) {
|
||||
if (formData.assingWorkOperationTime && typeof formData.assingWorkOperationTime === 'string') {
|
||||
formData.assingWorkOperationTime = new Date(formData.assingWorkOperationTime.replace(' ', 'T')).getTime()
|
||||
}
|
||||
|
||||
|
||||
slehand()
|
||||
|
||||
}
|
||||
|
||||
// 提交
|
||||
@ -961,15 +987,22 @@ let dispatchmodal = ref(false)
|
||||
let dispatchform = reactive<any>({
|
||||
id:'',
|
||||
name:'',
|
||||
userid:'',
|
||||
username:''
|
||||
//userid:'',
|
||||
username:'',
|
||||
assingWorkList:''
|
||||
})
|
||||
|
||||
let yglist = ref<any>([])
|
||||
|
||||
const dispatchrules = {
|
||||
userid:[{ required: true, message: '请选择员工', trigger: 'blur' }]
|
||||
assingWorkList:[{ required: true, message: '请选择员工', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
function updatesle(v:any) {
|
||||
dispatchform.assingWorkList = v.join()
|
||||
console.log(v)
|
||||
}
|
||||
|
||||
let dispatchLoading = ref(false)
|
||||
let dispatchddbtn = ref(false)
|
||||
|
||||
@ -981,7 +1014,7 @@ function slehand(v?:any) {
|
||||
loadingRef.value = true
|
||||
userApi.page({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
pageSize: 200,
|
||||
username: v,
|
||||
}).then((rps:any) =>{
|
||||
if(rps.list && rps.list.length > 0){
|
||||
@ -1004,12 +1037,13 @@ function disphand(v:any){
|
||||
dispatchformRef.value?.restoreValidation()
|
||||
dispatchform.id = v.id
|
||||
dispatchform.name = v.name
|
||||
dispatchform.userid = ''
|
||||
//dispatchform.userid = ''
|
||||
dispatchform.assingWorkList = ''
|
||||
slehand()
|
||||
}
|
||||
|
||||
function dispatchSubmit() {
|
||||
console.log(dispatchform.userid,'dispatchform.userid')
|
||||
|
||||
dispatchformRef.value?.validate((v:any) => {
|
||||
if(!v){
|
||||
// dispatchLoading.value = true
|
||||
@ -1022,7 +1056,7 @@ function dispatchSubmit() {
|
||||
// dispatchddbtn.value = false
|
||||
// loadData()
|
||||
// },1000)
|
||||
orderItemApi.assignWork(dispatchform).then(() => {
|
||||
orderItemApi.assignWork(Object.assign({},dispatchform,{assingWorkList:dispatchform.assingWorkList.split(',')}) ).then(() => {
|
||||
message.success('操作成功!')
|
||||
setTimeout(()=> {
|
||||
dispatchmodal.value = false
|
||||
|
||||
847
src/views/biz/orderProcessPlan/board.vue
Normal file
847
src/views/biz/orderProcessPlan/board.vue
Normal file
@ -0,0 +1,847 @@
|
||||
<template>
|
||||
<div class="plan-board-page">
|
||||
<!-- 顶部工具栏 -->
|
||||
<div class="board-toolbar">
|
||||
<n-space :size="8" align="center">
|
||||
<n-button type="primary" size="small" @click="handleAdd">
|
||||
<template #icon><n-icon><AddOutline /></n-icon></template>
|
||||
新增工序
|
||||
</n-button>
|
||||
<n-button size="small" type="success" ghost disabled title="待后端接口支持">
|
||||
<template #icon><n-icon><CheckmarkCircleOutline /></n-icon></template>
|
||||
齐套自动下达
|
||||
</n-button>
|
||||
<n-button size="small" type="error" ghost disabled title="待后端接口支持">批量删除</n-button>
|
||||
<n-button size="small" type="primary" ghost disabled title="待后端接口支持">批量下达</n-button>
|
||||
<n-button size="small" quaternary disabled title="待后端接口支持">合并为组工单</n-button>
|
||||
<n-dropdown :options="batchOptions" trigger="click" @select="onBatchSelect">
|
||||
<n-button size="small" quaternary>
|
||||
批量操作
|
||||
<n-icon style="margin-left: 4px"><ChevronDownOutline /></n-icon>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</n-space>
|
||||
<n-space :size="8" align="center">
|
||||
<n-button size="small" quaternary @click="importModalVisible = true">
|
||||
<template #icon><n-icon><CloudUploadOutline /></n-icon></template>
|
||||
导入
|
||||
</n-button>
|
||||
<n-button size="small" quaternary @click="handleExport">
|
||||
<template #icon><n-icon><DownloadOutline /></n-icon></template>
|
||||
导出
|
||||
</n-button>
|
||||
<n-button size="small" quaternary @click="loadData">
|
||||
<template #icon><n-icon><RefreshOutline /></n-icon></template>
|
||||
同步
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<!-- 筛选 -->
|
||||
<div class="board-filter">
|
||||
<n-form inline :model="searchForm" label-placement="left" size="small">
|
||||
<n-form-item label="工序名称">
|
||||
<n-input v-model:value="searchForm.name" placeholder="工序名称" clearable style="width: 140px" />
|
||||
</n-form-item>
|
||||
<n-form-item label="工序编码">
|
||||
<n-input v-model:value="searchForm.code" placeholder="工序编码" clearable style="width: 140px" />
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-space>
|
||||
<n-button type="primary" size="small" @click="handleSearch">
|
||||
<template #icon><n-icon><SearchOutline /></n-icon></template>
|
||||
搜索
|
||||
</n-button>
|
||||
<n-button size="small" @click="handleReset">重置</n-button>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
|
||||
<div class="board-table-wrap">
|
||||
<!-- 表头 -->
|
||||
<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>
|
||||
<div class="col-kit">齐套率</div>
|
||||
<div class="col-flow">工序</div>
|
||||
<div class="col-plan">计划开始 / 结束</div>
|
||||
<div class="col-life">生命周期</div>
|
||||
<div class="col-actions">操作</div>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<n-spin :show="loading">
|
||||
<div v-if="!tableData.length && !loading" class="board-empty">
|
||||
<n-empty description="暂无工序计划数据" />
|
||||
</div>
|
||||
|
||||
<div v-for="order in tableData" :key="orderRowKey(order)" class="order-block">
|
||||
<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)"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-expand">
|
||||
<n-button
|
||||
quaternary
|
||||
circle
|
||||
size="tiny"
|
||||
@click="toggleExpand(order)"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<RemoveOutline v-if="isExpanded(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">
|
||||
<span>{{ order.orderCode || order.mainCode || '-' }}</span>
|
||||
<n-tag v-if="isOverdue(order)" size="tiny" type="error" :bordered="false">逾期</n-tag>
|
||||
<n-tag v-if="hasOutsource(order)" size="tiny" type="warning" :bordered="false">委外</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-qty">{{ order.quantity ?? '-' }}</div>
|
||||
<div class="col-mode">
|
||||
<n-tag v-if="order.routeCode" size="tiny" :bordered="false">工艺型</n-tag>
|
||||
<n-tag v-if="order.proWorkshop" size="tiny" type="info" :bordered="false">{{ order.proWorkshop }}</n-tag>
|
||||
</div>
|
||||
<div class="col-kit">-</div>
|
||||
<div class="col-flow">
|
||||
<ProcessFlowStepper :list="order.processList" />
|
||||
</div>
|
||||
<div class="col-plan">
|
||||
<div>{{ formatShortTime(order.beginTime) }}</div>
|
||||
<div class="sub-text">{{ formatShortTime(order.endTime) }}</div>
|
||||
</div>
|
||||
<div class="col-life">
|
||||
<div class="life-cell">
|
||||
<n-icon :color="lifeIcon(order).color" size="18">
|
||||
<component :is="lifeIcon(order).icon" />
|
||||
</n-icon>
|
||||
<span>{{ lifeLabel(order) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-actions">
|
||||
<n-button text type="primary" size="tiny" @click="openDetail(order)">详情</n-button>
|
||||
<n-button text type="primary" size="tiny" disabled title="订单级编辑待支持">编辑</n-button>
|
||||
<n-dropdown :options="moreOptions" trigger="click" @select="(k) => onMoreSelect(k, order)">
|
||||
<n-button text size="tiny">更多</n-button>
|
||||
</n-dropdown>
|
||||
</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">
|
||||
<ProcessCard
|
||||
v-for="(proc, idx) in sortedProcessList(order.processList)"
|
||||
:key="proc.planId ?? idx"
|
||||
:process="proc"
|
||||
:is-last="idx === (order.processList?.length ?? 0) - 1"
|
||||
:can-edit="hasPermission('biz:orderItem:edit')"
|
||||
:can-assign="hasPermission('biz:orderItem:assign')"
|
||||
@edit="(p) => handleEdit(p, order)"
|
||||
@assign="(p) => disphand(p, order)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-spin>
|
||||
</div>
|
||||
|
||||
<div class="board-pagination">
|
||||
<n-pagination
|
||||
v-model:page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:item-count="pagination.itemCount"
|
||||
:page-sizes="[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-modal v-model:show="detailVisible" preset="card" title="工序计划详情" style="width: 900px">
|
||||
<n-spin :show="detailLoading">
|
||||
<template v-if="detailData">
|
||||
<n-descriptions :column="3" bordered size="small" label-placement="left">
|
||||
<n-descriptions-item label="生产令号">{{ detailData.mainCode || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="订单编码">{{ detailData.orderCode || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="物料">{{ detailData.materialName || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="数量">{{ detailData.quantity ?? '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="工艺路线">{{ detailData.routeCode || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="车间">{{ detailData.proWorkshop || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="计划开工">{{ detailData.beginTime || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="计划完工">{{ detailData.endTime || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="订单状态">{{ lifeLabel(detailData) }}</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
<div class="detail-cards">
|
||||
<ProcessCard
|
||||
v-for="(proc, idx) in sortedProcessList(detailData.processList)"
|
||||
:key="proc.planId ?? idx"
|
||||
:process="proc"
|
||||
:is-last="idx === (detailData.processList?.length ?? 0) - 1"
|
||||
:can-edit="hasPermission('biz:orderItem:edit')"
|
||||
:can-assign="hasPermission('biz:orderItem:assign')"
|
||||
@edit="(p) => handleEdit(p, detailData!)"
|
||||
@assign="(p) => disphand(p, detailData!)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</n-spin>
|
||||
</n-modal>
|
||||
|
||||
<!-- 新增/编辑 -->
|
||||
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 800px">
|
||||
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="120px">
|
||||
<n-grid :cols="2">
|
||||
<n-gi>
|
||||
<n-form-item label="工序名称" path="name">
|
||||
<n-input v-model:value="formData.name" placeholder="请输入工序名称" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="工序编码" path="code">
|
||||
<n-input v-model:value="formData.code" placeholder="请输入工序编码" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="工序顺序" path="sort">
|
||||
<n-input v-model:value="formData.sort" placeholder="请输入工序顺序" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="生产订单id" path="orderItemId">
|
||||
<n-input v-model:value="formData.orderItemId" placeholder="请输入生产订单id" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="生产开始时间" path="beginTime">
|
||||
<n-date-picker v-model:value="formData.beginTime" type="datetime" clearable style="width: 100%" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="生产结束时间" path="endTime">
|
||||
<n-date-picker v-model:value="formData.endTime" type="datetime" clearable style="width: 100%" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="生产总数" path="quantity">
|
||||
<n-input v-model:value="formData.quantity" placeholder="请输入生产总数" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="转入数量" path="transferNum">
|
||||
<n-input v-model:value="formData.transferNum" placeholder="请输入转入数量" />
|
||||
</n-form-item>
|
||||
</n-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="dispatchmodal" title="派工" preset="card" style="width: 800px" :mask-closable="false">
|
||||
<n-form ref="dispatchformRef" :model="dispatchform" label-placement="left" :rules="dispatchrules" label-width="80">
|
||||
<n-grid :cols="2">
|
||||
<n-gi>
|
||||
<n-form-item label="工序名称">
|
||||
<n-input v-model:value="dispatchform.name" disabled />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="生产数量">
|
||||
<n-input v-model:value="dispatchform.quantity" disabled />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="开始时间">
|
||||
<n-input v-model:value="dispatchform.beginTime" disabled />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="结束时间">
|
||||
<n-input v-model:value="dispatchform.endTime" disabled />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="24">
|
||||
<Pgitem
|
||||
v-for="(n, i) in dispatchform.list"
|
||||
:key="i"
|
||||
:obj="n"
|
||||
:index="i"
|
||||
listname="list"
|
||||
@addhand="addpgnum"
|
||||
@delehand="deletenum(i)"
|
||||
/>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button size="small" @click="dispatchmodal = false">取消</n-button>
|
||||
<n-button size="small" type="primary" :loading="dispatchLoading" @click="dispatchSubmit">确定</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<!-- 导入 -->
|
||||
<n-modal v-model:show="importModalVisible" preset="card" title="导入工序计划" style="width: 500px">
|
||||
<n-space vertical>
|
||||
<n-alert type="info">
|
||||
<template #header>导入说明</template>
|
||||
请先下载模板,按格式填写后上传 .xlsx / .xls 文件。
|
||||
</n-alert>
|
||||
<n-button type="primary" @click="handleDownloadTemplate">
|
||||
<template #icon><n-icon><DownloadOutline /></n-icon></template>
|
||||
下载模板
|
||||
</n-button>
|
||||
<n-upload :max="1" accept=".xlsx,.xls" :custom-request="handleImportUpload">
|
||||
<n-upload-dragger>
|
||||
<n-text>点击或拖拽文件到此处上传</n-text>
|
||||
</n-upload-dragger>
|
||||
</n-upload>
|
||||
</n-space>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import {
|
||||
NButton, NSpace, NIcon, NTag, NSpin, NModal, NForm, NFormItem, NGrid, NGi,
|
||||
NInput, NDatePicker, NCheckbox, NEmpty, NPagination, NDropdown, NDescriptions,
|
||||
NDescriptionsItem, NAlert, NUpload, NText, useMessage, useDialog,
|
||||
type UploadCustomRequestOptions,
|
||||
} from 'naive-ui'
|
||||
import {
|
||||
AddOutline, RemoveOutline, SearchOutline, RefreshOutline, CloudUploadOutline,
|
||||
DownloadOutline, ChevronDownOutline, CheckmarkCircleOutline, CheckmarkCircle,
|
||||
EllipseOutline, TimeOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
import {
|
||||
orderProcessPlanApi,
|
||||
toOrderProcessPlanPayload,
|
||||
processItemToEntity,
|
||||
ORDER_STATUS_MAP,
|
||||
type OrderProcessPlanVO,
|
||||
type ProcessPlanItemVO,
|
||||
} from '@/api/orderProcessPlan'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import ProcessFlowStepper from './components/ProcessFlowStepper.vue'
|
||||
import ProcessCard from './components/ProcessCard.vue'
|
||||
import Pgitem from './pgitem.vue'
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const userStore = useUserStore()
|
||||
const hasPermission = (p: string) => userStore.hasPermission(p)
|
||||
|
||||
const searchForm = reactive({
|
||||
name: '',
|
||||
code: '',
|
||||
})
|
||||
|
||||
const tableData = ref<OrderProcessPlanVO[]>([])
|
||||
const expandedIds = ref<Set<number>>(new Set())
|
||||
const selectedOrderIds = ref<number[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, itemCount: 0 })
|
||||
|
||||
const detailVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detailData = ref<OrderProcessPlanVO | null>(null)
|
||||
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formRef = ref()
|
||||
const defaultFormData = {
|
||||
name: '', code: '', sort: '', orderItemId: '',
|
||||
beginTime: null as number | null, endTime: null as number | null,
|
||||
quantity: '', transferNum: '', transferOutNum: '',
|
||||
procurement: '', outsource: '', qualityInspection: '', storageEntry: '',
|
||||
}
|
||||
const formData = reactive<any>({ ...defaultFormData })
|
||||
const formRules = {}
|
||||
|
||||
const dispatchmodal = ref(false)
|
||||
const dispatchLoading = ref(false)
|
||||
const dispatchformRef = ref()
|
||||
const dispatchform = reactive<any>({
|
||||
id: '', name: '', beginTime: '', endTime: '', quantity: '',
|
||||
list: [{ userId: '', quantity: '' }],
|
||||
})
|
||||
const dispatchrules = {}
|
||||
|
||||
const importModalVisible = ref(false)
|
||||
|
||||
const batchOptions = [
|
||||
{ label: '全部展开', key: 'expandAll' },
|
||||
{ label: '全部收起', key: 'collapseAll' },
|
||||
]
|
||||
const moreOptions = [
|
||||
{ label: '展开工序', key: 'expand' },
|
||||
{ label: '收起工序', key: 'collapse' },
|
||||
]
|
||||
|
||||
function orderRowKey(row: OrderProcessPlanVO) {
|
||||
return row.orderItemId ?? `order-${row.orderCode}`
|
||||
}
|
||||
|
||||
function sortedProcessList(list?: ProcessPlanItemVO[]) {
|
||||
return [...(list ?? [])].sort((a, b) => (a.operNumber ?? 0) - (b.operNumber ?? 0))
|
||||
}
|
||||
|
||||
function isExpanded(order: OrderProcessPlanVO) {
|
||||
return order.orderItemId != null && expandedIds.value.has(order.orderItemId)
|
||||
}
|
||||
|
||||
function toggleExpand(order: OrderProcessPlanVO) {
|
||||
if (order.orderItemId == null) return
|
||||
const next = new Set(expandedIds.value)
|
||||
if (next.has(order.orderItemId)) next.delete(order.orderItemId)
|
||||
else next.add(order.orderItemId)
|
||||
expandedIds.value = next
|
||||
}
|
||||
|
||||
function toggleSelect(id: number, checked: boolean) {
|
||||
if (checked) selectedOrderIds.value = [...selectedOrderIds.value, id]
|
||||
else selectedOrderIds.value = selectedOrderIds.value.filter(v => v !== id)
|
||||
}
|
||||
|
||||
function formatShortTime(val?: string | null) {
|
||||
if (!val) return '-'
|
||||
return val.length > 16 ? val.slice(0, 16) : val
|
||||
}
|
||||
|
||||
function isOverdue(order: OrderProcessPlanVO) {
|
||||
if (order.orderStatus === 2 || !order.endTime) return false
|
||||
return new Date(order.endTime.replace(' ', 'T')).getTime() < Date.now()
|
||||
}
|
||||
|
||||
function hasOutsource(order: OrderProcessPlanVO) {
|
||||
return order.processList?.some(p => p.outsource === 1)
|
||||
}
|
||||
|
||||
function lifeLabel(order: OrderProcessPlanVO) {
|
||||
const s = order.orderStatus
|
||||
if (s == null) return '-'
|
||||
return ORDER_STATUS_MAP[s]?.label ?? String(s)
|
||||
}
|
||||
|
||||
function lifeIcon(order: OrderProcessPlanVO) {
|
||||
const s = order.orderStatus
|
||||
if (s === 2) return { icon: CheckmarkCircle, color: '#18a058' }
|
||||
if (s === 1) return { icon: TimeOutline, color: '#2080f0' }
|
||||
return { icon: EllipseOutline, color: '#8c8c8c' }
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await orderProcessPlanApi.page({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
name: searchForm.name || undefined,
|
||||
code: searchForm.code || undefined,
|
||||
})
|
||||
tableData.value = res.list ?? []
|
||||
pagination.itemCount = res.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.name = ''
|
||||
searchForm.code = ''
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
pagination.page = page
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
pagination.pageSize = pageSize
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function onBatchSelect(key: string) {
|
||||
if (key === 'expandAll') {
|
||||
expandedIds.value = new Set(tableData.value.map(o => o.orderItemId!).filter(Boolean))
|
||||
} else if (key === 'collapseAll') {
|
||||
expandedIds.value = new Set()
|
||||
}
|
||||
}
|
||||
|
||||
function onMoreSelect(key: string, order: OrderProcessPlanVO) {
|
||||
if (key === 'expand') toggleExpand(order)
|
||||
if (key === 'collapse' && isExpanded(order)) toggleExpand(order)
|
||||
}
|
||||
|
||||
async function openDetail(order: OrderProcessPlanVO) {
|
||||
if (!order.orderItemId) return
|
||||
detailVisible.value = true
|
||||
detailLoading.value = true
|
||||
detailData.value = null
|
||||
try {
|
||||
detailData.value = await orderProcessPlanApi.detail(order.orderItemId)
|
||||
} catch {
|
||||
detailData.value = order
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
modalTitle.value = '新增工序计划'
|
||||
Object.assign(formData, defaultFormData)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
function parseDateTime(val: string | null | undefined) {
|
||||
if (!val) return null
|
||||
const ts = new Date(val.replace(' ', 'T')).getTime()
|
||||
return Number.isNaN(ts) ? null : ts
|
||||
}
|
||||
|
||||
function handleEdit(process: ProcessPlanItemVO, order?: OrderProcessPlanVO) {
|
||||
modalTitle.value = '编辑工序计划'
|
||||
const entity = processItemToEntity(process, order?.orderItemId)
|
||||
Object.assign(formData, {
|
||||
...defaultFormData,
|
||||
...entity,
|
||||
sort: entity.sort != null ? String(entity.sort) : '',
|
||||
orderItemId: entity.orderItemId != null ? String(entity.orderItemId) : '',
|
||||
quantity: entity.quantity != null ? String(entity.quantity) : '',
|
||||
transferNum: entity.transferNum != null ? String(entity.transferNum) : '',
|
||||
beginTime: parseDateTime(entity.beginTime),
|
||||
endTime: parseDateTime(entity.endTime),
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate()
|
||||
const submitData = toOrderProcessPlanPayload({ ...formData })
|
||||
if (typeof submitData.beginTime === 'number') {
|
||||
submitData.beginTime = new Date(submitData.beginTime).toISOString().slice(0, 19).replace('T', ' ')
|
||||
}
|
||||
if (typeof submitData.endTime === 'number') {
|
||||
submitData.endTime = new Date(submitData.endTime).toISOString().slice(0, 19).replace('T', ' ')
|
||||
}
|
||||
if (submitData.id) {
|
||||
await orderProcessPlanApi.update(submitData)
|
||||
message.success('修改成功')
|
||||
} else {
|
||||
await orderProcessPlanApi.create(submitData)
|
||||
message.success('新增成功')
|
||||
}
|
||||
modalVisible.value = false
|
||||
loadData()
|
||||
}
|
||||
|
||||
function disphand(process: ProcessPlanItemVO, order?: OrderProcessPlanVO) {
|
||||
const entity = processItemToEntity(process, order?.orderItemId)
|
||||
dispatchmodal.value = true
|
||||
dispatchformRef.value?.restoreValidation()
|
||||
dispatchform.id = entity.id
|
||||
dispatchform.name = entity.name
|
||||
dispatchform.beginTime = entity.beginTime
|
||||
dispatchform.endTime = entity.endTime
|
||||
dispatchform.quantity = entity.quantity
|
||||
dispatchform.list = [{ userId: '', quantity: '' }]
|
||||
}
|
||||
|
||||
function addpgnum() {
|
||||
dispatchform.list.push({ userId: '', quantity: '' })
|
||||
}
|
||||
|
||||
function deletenum(index: number) {
|
||||
dispatchform.list.splice(index, 1)
|
||||
}
|
||||
|
||||
function dispatchSubmit() {
|
||||
let total = 0
|
||||
dispatchform.list.forEach((n: any) => { total += Number(n.quantity) || 0 })
|
||||
dispatchformRef.value?.validate((v: boolean) => {
|
||||
if (v) return
|
||||
if (total !== Number(dispatchform.quantity)) {
|
||||
message.error('派工总数量需等于生产总数', { duration: 4000 })
|
||||
return
|
||||
}
|
||||
dispatchLoading.value = true
|
||||
orderProcessPlanApi.assignWork(dispatchform).then(() => {
|
||||
message.success('派工成功')
|
||||
dispatchmodal.value = false
|
||||
loadData()
|
||||
}).finally(() => { dispatchLoading.value = false })
|
||||
})
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
const blob = await orderProcessPlanApi.export({
|
||||
name: searchForm.name || undefined,
|
||||
code: searchForm.code || undefined,
|
||||
})
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = '工序计划数据.xlsx'
|
||||
link.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
async function handleDownloadTemplate() {
|
||||
const blob = await orderProcessPlanApi.downloadTemplate()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = '工序计划导入模板.xlsx'
|
||||
link.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
async function handleImportUpload({ file }: UploadCustomRequestOptions) {
|
||||
if (!file.file) return
|
||||
const result = await orderProcessPlanApi.importData(file.file)
|
||||
if (result.fail > 0) {
|
||||
dialog.warning({
|
||||
title: '导入结果',
|
||||
content: `成功 ${result.success} 条,失败 ${result.fail} 条`,
|
||||
positiveText: '确定',
|
||||
})
|
||||
} else {
|
||||
message.success(`导入成功,共 ${result.success} 条`)
|
||||
importModalVisible.value = false
|
||||
}
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.plan-board-page {
|
||||
padding: 0;
|
||||
background: #f5f7fa;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.board-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px 8px 0 0;
|
||||
border-bottom: 1px solid #eef0f3;
|
||||
}
|
||||
|
||||
.board-filter {
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #eef0f3;
|
||||
}
|
||||
|
||||
.board-table-wrap {
|
||||
overflow-x: auto;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.board-table-head,
|
||||
.order-row {
|
||||
display: grid;
|
||||
/* 勾选 | 展开 | 产品工单 | 数量 | 模式 | 齐套率 | 工序轴 | 计划时间 | 生命周期 | 操作 */
|
||||
grid-template-columns:
|
||||
40px
|
||||
40px
|
||||
minmax(220px, 1.6fr)
|
||||
64px
|
||||
108px
|
||||
72px
|
||||
minmax(200px, 1.4fr)
|
||||
168px
|
||||
96px
|
||||
140px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 16px;
|
||||
min-width: 1320px;
|
||||
}
|
||||
|
||||
.board-table-head {
|
||||
height: 40px;
|
||||
background: #fafbfc;
|
||||
border-bottom: 1px solid #e8eaed;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.order-block {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #eef0f3;
|
||||
}
|
||||
|
||||
.order-row {
|
||||
min-height: 64px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.order-row:hover {
|
||||
background: #f8faff;
|
||||
}
|
||||
|
||||
.order-row.expanded {
|
||||
background: rgba(32, 128, 240, 0.04);
|
||||
}
|
||||
|
||||
.col-check,
|
||||
.col-expand {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.col-product {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.col-qty,
|
||||
.col-kit {
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.product-code {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.col-mode {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.col-plan {
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.col-plan .sub-text {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.col-life {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.col-flow {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.life-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.col-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.order-expand {
|
||||
padding: 12px 16px 16px 68px;
|
||||
background: linear-gradient(180deg, rgba(32, 128, 240, 0.03) 0%, #fff 100%);
|
||||
border-top: 1px dashed #e8eaed;
|
||||
}
|
||||
|
||||
.card-flow-scroll,
|
||||
.detail-cards {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.detail-cards {
|
||||
margin-top: 16px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.expand-empty {
|
||||
font-size: 13px;
|
||||
color: #8c8c8c;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.board-empty {
|
||||
padding: 48px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.board-pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
</style>
|
||||
353
src/views/biz/orderProcessPlan/components/ProcessCard.vue
Normal file
353
src/views/biz/orderProcessPlan/components/ProcessCard.vue
Normal file
@ -0,0 +1,353 @@
|
||||
<template>
|
||||
<div class="process-card-wrap">
|
||||
<div class="process-card" :class="themeClass">
|
||||
<div class="card-head">
|
||||
<span class="card-title">{{ process.processName || '-' }}</span>
|
||||
<n-tag size="small" :bordered="false" :type="statusTag.type">{{ statusTag.label }}</n-tag>
|
||||
</div>
|
||||
|
||||
<div class="card-progress">
|
||||
<n-progress
|
||||
type="circle"
|
||||
:percentage="progressPct"
|
||||
:stroke-width="10"
|
||||
:color="progressColor"
|
||||
:rail-color="railColor"
|
||||
style="width: 88px"
|
||||
>
|
||||
<span class="progress-text">{{ progressPct }}%</span>
|
||||
</n-progress>
|
||||
</div>
|
||||
|
||||
<div class="card-stats">
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">完成</span>
|
||||
<span class="stat-value">{{ completedQty }} / {{ totalQty }}</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">合格 / 不合格</span>
|
||||
<span class="stat-value">
|
||||
<em class="ok">{{ qualifiedQty }}</em>
|
||||
<span class="sep">/</span>
|
||||
<em class="bad">{{ scrapQty }}</em>
|
||||
</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">合格率</span>
|
||||
<span class="stat-value highlight">{{ passRate }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-flow">
|
||||
<div class="flow-item">
|
||||
<span>剩余物料</span>
|
||||
<strong>{{ remainQty }}</strong>
|
||||
</div>
|
||||
<div class="flow-item">
|
||||
<span>转下道</span>
|
||||
<strong>{{ process.transferOutQty ?? 0 }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-meta">
|
||||
<div class="meta-line"><span>人员</span><em>{{ personnel }}</em></div>
|
||||
<div class="meta-line"><span>车间</span><em>{{ process.departmentName || '-' }}</em></div>
|
||||
<div class="meta-line"><span>工位</span><em>{{ process.workCenterName || '-' }}</em></div>
|
||||
<div class="meta-line"><span>设备</span><em>-</em></div>
|
||||
<div class="meta-line"><span>计划时间</span><em>{{ planTimeText }}</em></div>
|
||||
<div class="meta-line"><span>实际开始</span><em>{{ process.actualStartTime || '-' }}</em></div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
<n-button
|
||||
v-if="canAssign"
|
||||
size="tiny"
|
||||
type="warning"
|
||||
ghost
|
||||
@click="emit('assign', process)"
|
||||
>派工</n-button>
|
||||
<n-button
|
||||
v-if="canEdit"
|
||||
size="tiny"
|
||||
ghost
|
||||
@click="emit('edit', process)"
|
||||
>编辑</n-button>
|
||||
<n-button size="tiny" quaternary disabled>返工</n-button>
|
||||
<n-button
|
||||
size="tiny"
|
||||
:type="process.processStatus === 4 ? 'success' : 'default'"
|
||||
:ghost="process.processStatus !== 4"
|
||||
disabled
|
||||
>{{ process.processStatus === 4 ? '已完成' : '进行中' }}</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isLast" class="card-chevron">
|
||||
<n-icon size="28" color="#c0c4cc"><ChevronForwardOutline /></n-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { NButton, NIcon, NProgress, NTag } from 'naive-ui'
|
||||
import { ChevronForwardOutline } from '@vicons/ionicons5'
|
||||
import { PROCESS_STATUS_MAP, type ProcessPlanItemVO } from '@/api/orderProcessPlan'
|
||||
|
||||
const props = defineProps<{
|
||||
process: ProcessPlanItemVO
|
||||
isLast?: boolean
|
||||
canEdit?: boolean
|
||||
canAssign?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
edit: [process: ProcessPlanItemVO]
|
||||
assign: [process: ProcessPlanItemVO]
|
||||
}>()
|
||||
|
||||
const totalQty = computed(() => props.process.quantity ?? 0)
|
||||
const completedQty = computed(() => props.process.completedQty ?? 0)
|
||||
const scrapQty = computed(() => props.process.scrapQty ?? 0)
|
||||
const qualifiedQty = computed(() => Math.max(0, completedQty.value - scrapQty.value))
|
||||
|
||||
const progressPct = computed(() => {
|
||||
if (!totalQty.value) return 0
|
||||
return Math.min(100, Math.round((completedQty.value / totalQty.value) * 100))
|
||||
})
|
||||
|
||||
const passRate = computed(() => {
|
||||
if (!completedQty.value) return progressPct.value
|
||||
return Math.round((qualifiedQty.value / completedQty.value) * 100)
|
||||
})
|
||||
|
||||
const remainQty = computed(() => Math.max(0, totalQty.value - completedQty.value))
|
||||
|
||||
const statusTag = computed(() => {
|
||||
const s = props.process.processStatus
|
||||
if (s == null) return { label: '-', type: 'default' as const }
|
||||
const m = PROCESS_STATUS_MAP[s]
|
||||
return m ? { label: m.label, type: m.type } : { label: String(s), type: 'default' as const }
|
||||
})
|
||||
|
||||
const themeClass = computed(() => {
|
||||
const s = props.process.processStatus
|
||||
if (s === 4) return 'theme-done'
|
||||
if (s != null && s >= 2) return 'theme-active'
|
||||
return 'theme-pending'
|
||||
})
|
||||
|
||||
const progressColor = computed(() => {
|
||||
if (themeClass.value === 'theme-done') return '#18a058'
|
||||
if (themeClass.value === 'theme-active') return '#2080f0'
|
||||
return '#d9d9d9'
|
||||
})
|
||||
|
||||
const railColor = computed(() => {
|
||||
if (themeClass.value === 'theme-done') return 'rgba(24,160,88,0.15)'
|
||||
if (themeClass.value === 'theme-active') return 'rgba(32,128,240,0.12)'
|
||||
return '#f0f0f0'
|
||||
})
|
||||
|
||||
const personnel = computed(() => {
|
||||
if (props.process.assignByName) return props.process.assignByName
|
||||
const list = props.process.assignWorkList
|
||||
if (list?.length) return `${list.length} 人派工`
|
||||
return '-'
|
||||
})
|
||||
|
||||
const planTimeText = computed(() => {
|
||||
const start = props.process.planStartTime
|
||||
const end = props.process.planFinishTime
|
||||
if (!start && !end) return '-'
|
||||
if (start && end) return `${start.slice(0, 16)} ~ ${end.slice(11, 16)}`
|
||||
return start || end || '-'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.process-card-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.process-card {
|
||||
width: 220px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e8e8e8;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
transition: box-shadow 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.process-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.theme-done {
|
||||
border-color: rgba(24, 160, 88, 0.35);
|
||||
}
|
||||
|
||||
.theme-done .card-head {
|
||||
background: linear-gradient(135deg, #18a058 0%, #36ad6a 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.theme-done .card-head :deep(.n-tag) {
|
||||
background: rgba(255, 255, 255, 0.25) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.theme-active .card-head {
|
||||
background: linear-gradient(135deg, #2080f0 0%, #4098fc 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.theme-active .card-head :deep(.n-tag) {
|
||||
background: rgba(255, 255, 255, 0.25) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.theme-pending .card-head {
|
||||
background: #f5f5f5;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-progress {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 12px 0 8px;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.card-stats {
|
||||
padding: 0 12px 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-value .ok {
|
||||
color: #18a058;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.stat-value .bad {
|
||||
color: #d03050;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.stat-value .sep {
|
||||
margin: 0 2px;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
|
||||
.stat-value.highlight {
|
||||
color: #18a058;
|
||||
}
|
||||
|
||||
.card-flow {
|
||||
display: flex;
|
||||
margin: 0 12px 8px;
|
||||
padding: 8px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.flow-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
font-size: 11px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.flow-item strong {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
padding: 0 12px 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.meta-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 11px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.meta-line span {
|
||||
color: #8c8c8c;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.meta-line em {
|
||||
font-style: normal;
|
||||
color: #595959;
|
||||
text-align: right;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
padding: 8px 10px 10px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.card-chevron {
|
||||
padding: 0 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
121
src/views/biz/orderProcessPlan/components/ProcessFlowStepper.vue
Normal file
121
src/views/biz/orderProcessPlan/components/ProcessFlowStepper.vue
Normal file
@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="flow-stepper">
|
||||
<template v-for="(step, idx) in steps" :key="step.key">
|
||||
<div
|
||||
class="flow-step"
|
||||
:class="step.state"
|
||||
:title="step.label"
|
||||
>
|
||||
<span class="flow-dot">
|
||||
<n-icon v-if="step.state === 'done'" size="12"><CheckmarkOutline /></n-icon>
|
||||
<span v-else class="flow-num">{{ idx + 1 }}</span>
|
||||
</span>
|
||||
<span class="flow-label">{{ step.shortLabel }}</span>
|
||||
</div>
|
||||
<div v-if="idx < steps.length - 1" class="flow-line" :class="step.state" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { NIcon } from 'naive-ui'
|
||||
import { CheckmarkOutline } from '@vicons/ionicons5'
|
||||
import type { ProcessPlanItemVO } from '@/api/orderProcessPlan'
|
||||
|
||||
const props = defineProps<{
|
||||
list?: ProcessPlanItemVO[]
|
||||
}>()
|
||||
|
||||
const steps = computed(() => {
|
||||
const list = [...(props.list ?? [])].sort((a, b) => (a.operNumber ?? 0) - (b.operNumber ?? 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'
|
||||
return {
|
||||
key: item.planId ?? `${item.processCode}-${idx}`,
|
||||
label,
|
||||
shortLabel: label.length > 5 ? `${label.slice(0, 5)}…` : label,
|
||||
state,
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.flow-stepper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
max-width: 56px;
|
||||
}
|
||||
|
||||
.flow-dot {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
border: 2px solid #d9d9d9;
|
||||
background: #fff;
|
||||
color: #8c8c8c;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.flow-step.done .flow-dot {
|
||||
background: #18a058;
|
||||
border-color: #18a058;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.flow-step.active .flow-dot {
|
||||
background: #2080f0;
|
||||
border-color: #2080f0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.flow-label {
|
||||
font-size: 11px;
|
||||
color: #8c8c8c;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.flow-step.done .flow-label,
|
||||
.flow-step.active .flow-label {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.flow-line {
|
||||
flex: 1;
|
||||
min-width: 12px;
|
||||
max-width: 28px;
|
||||
height: 2px;
|
||||
background: #e8e8e8;
|
||||
margin: 0 2px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.flow-line.done,
|
||||
.flow-line.active {
|
||||
background: #18a058;
|
||||
}
|
||||
|
||||
.flow-num {
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
@ -55,16 +55,25 @@
|
||||
</n-button-group> -->
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<div class="table-toolbar" style="margin-top: -8px">
|
||||
<n-button size="small" quaternary @click="toggleExpandAll">
|
||||
{{ allExpanded ? '全部收起' : '全部展开' }}
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 表格:主行生产订单,展开显示工序明细 -->
|
||||
<n-data-table
|
||||
size="small"
|
||||
remote
|
||||
:border="false"
|
||||
:single-line="false"
|
||||
striped
|
||||
:columns="columns"
|
||||
:columns="orderColumns"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:row-key="orderRowKey"
|
||||
:scroll-x="orderScrollX"
|
||||
v-model:expanded-row-keys="expandedRowKeys"
|
||||
/>
|
||||
<div class="pagination-container" style="display: flex; justify-content: flex-end; margin-top: 12px">
|
||||
<n-pagination
|
||||
@ -367,16 +376,19 @@ import {
|
||||
ref,
|
||||
reactive,
|
||||
h,
|
||||
computed,
|
||||
onMounted
|
||||
} from 'vue'
|
||||
import {
|
||||
NButton,
|
||||
NSpace,
|
||||
NIcon,
|
||||
NTag,
|
||||
NDataTable,
|
||||
NUpload,
|
||||
useMessage,
|
||||
useDialog,
|
||||
//type DataTableColumns,
|
||||
type DataTableColumns,
|
||||
type UploadCustomRequestOptions
|
||||
} from 'naive-ui'
|
||||
import {
|
||||
@ -389,8 +401,13 @@ import {
|
||||
DownloadOutline
|
||||
} from '@vicons/ionicons5'
|
||||
import {
|
||||
orderProcessPlanApi,
|
||||
type OrderProcessPlan
|
||||
orderProcessPlanApi,
|
||||
toOrderProcessPlanPayload,
|
||||
processItemToEntity,
|
||||
ORDER_STATUS_MAP,
|
||||
PROCESS_STATUS_MAP,
|
||||
type OrderProcessPlanVO,
|
||||
type ProcessPlanItemVO,
|
||||
} from '@/api/orderProcessPlan'
|
||||
import { dictDataApi } from '@/api/org'
|
||||
|
||||
@ -415,9 +432,102 @@ const searchForm = reactive<any>({
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<OrderProcessPlan[]>([])
|
||||
const tableData = ref<OrderProcessPlanVO[]>([])
|
||||
const expandedRowKeys = ref<Array<string | number>>([])
|
||||
const loading = ref(false)
|
||||
const selectedIds = ref<number[]>([])
|
||||
const orderScrollX = 2090
|
||||
const processScrollX = 2620
|
||||
|
||||
function orderRowKey(row: OrderProcessPlanVO) {
|
||||
return row.orderItemId ?? `order-${row.mainCode}-${row.orderCode}`
|
||||
}
|
||||
|
||||
function processRowKey(row: ProcessPlanItemVO) {
|
||||
return row.planId ?? `proc-${row.processCode}-${row.operNumber}`
|
||||
}
|
||||
|
||||
const allExpanded = computed(() => {
|
||||
const total = tableData.value.length
|
||||
if (!total) return false
|
||||
return expandedRowKeys.value.length >= total
|
||||
})
|
||||
|
||||
function toggleExpandAll() {
|
||||
if (allExpanded.value) {
|
||||
expandedRowKeys.value = []
|
||||
} else {
|
||||
expandedRowKeys.value = tableData.value.map(orderRowKey)
|
||||
}
|
||||
}
|
||||
|
||||
function renderStatusTag(
|
||||
val: number | null | undefined,
|
||||
map: Record<number, { label: string; type: 'default' | 'info' | 'success' | 'warning' | 'error' }>,
|
||||
) {
|
||||
if (val == null) return '-'
|
||||
const opt = map[val]
|
||||
if (!opt) return String(val)
|
||||
return h(NTag, { size: 'small', type: opt.type, bordered: false }, { default: () => opt.label })
|
||||
}
|
||||
|
||||
function renderYesNo(val: number | null | undefined, options: { label: string; value: unknown }[]) {
|
||||
const opt = options.find(o => o.value === val || String(o.value) === String(val))
|
||||
return opt ? opt.label : (val ?? '-')
|
||||
}
|
||||
|
||||
function buildProcessActionColumn(order: OrderProcessPlanVO): DataTableColumns<ProcessPlanItemVO>[number] {
|
||||
return {
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 140,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
render(row) {
|
||||
const buttons: ReturnType<typeof h>[] = []
|
||||
if (hasPermission('biz:orderItem:edit')) {
|
||||
buttons.push(h(NButton, {
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
ghost: true,
|
||||
onClick: () => handleEdit(row, order),
|
||||
}, { default: () => '编辑' }))
|
||||
}
|
||||
if (hasPermission('biz:orderItem:assign')) {
|
||||
buttons.push(h(NButton, {
|
||||
size: 'small',
|
||||
type: 'success',
|
||||
ghost: true,
|
||||
onClick: () => disphand(row, order),
|
||||
}, { default: () => '派工' }))
|
||||
}
|
||||
return buttons.length > 0 ? h(NSpace, { justify: 'center' }, { default: () => buttons }) : '-'
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function buildProcessColumns(order: OrderProcessPlanVO): DataTableColumns<ProcessPlanItemVO> {
|
||||
return [...processColumns.slice(0, -1), buildProcessActionColumn(order)]
|
||||
}
|
||||
|
||||
function renderProcessExpand(row: OrderProcessPlanVO) {
|
||||
const list = row.processList ?? []
|
||||
if (!list.length) {
|
||||
return h('div', { class: 'process-expand-empty' }, '暂无工序计划')
|
||||
}
|
||||
return h('div', { class: 'process-expand-wrap' }, [
|
||||
h(NDataTable, {
|
||||
class: 'process-plan-table',
|
||||
columns: buildProcessColumns(row),
|
||||
data: list,
|
||||
rowKey: processRowKey,
|
||||
size: 'small',
|
||||
bordered: true,
|
||||
striped: true,
|
||||
scrollX: processScrollX,
|
||||
}),
|
||||
])
|
||||
}
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
@ -458,149 +568,113 @@ const storageEntryOptions = ref<{ label: string; value: any }[]>([])
|
||||
const formRules = {
|
||||
}
|
||||
|
||||
// 表格列
|
||||
const columns = [
|
||||
// { type: 'selection' },
|
||||
{
|
||||
align:'center',
|
||||
minWidth: 240,
|
||||
title: '工序名称',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
align:'center',
|
||||
minWidth: 150,
|
||||
title: '工序编码',
|
||||
key: 'code'
|
||||
},
|
||||
{
|
||||
align:'center',
|
||||
minWidth: 150,
|
||||
title: '工序顺序',
|
||||
key: 'sort'
|
||||
},
|
||||
// {
|
||||
// align:'center',
|
||||
// minWidth: 150,
|
||||
// title: '生产订单id',
|
||||
// key: 'orderItemId'
|
||||
// },
|
||||
{
|
||||
align:'center',
|
||||
minWidth: 150,
|
||||
title: '生产开始时间',
|
||||
key: 'beginTime'
|
||||
},
|
||||
{
|
||||
align:'center',
|
||||
minWidth: 150,
|
||||
title: '生产结束时间',
|
||||
key: 'endTime'
|
||||
},
|
||||
{
|
||||
align:'center',
|
||||
minWidth: 150,
|
||||
title: '是否采购',
|
||||
key: 'procurement',
|
||||
render(row) {
|
||||
const val = row.procurement
|
||||
const opt = procurementOptions.value.find(o => o.value === val || String(o.value) === String(val))
|
||||
return opt ? opt.label : (val ?? '-')
|
||||
}
|
||||
},
|
||||
{
|
||||
align:'center',
|
||||
minWidth: 150,
|
||||
title: '是否委外',
|
||||
key: 'outsource',
|
||||
render(row) {
|
||||
const val = row.outsource
|
||||
const opt = outsourceOptions.value.find(o => o.value === val || String(o.value) === String(val))
|
||||
return opt ? opt.label : (val ?? '-')
|
||||
}
|
||||
},
|
||||
{
|
||||
align:'center',
|
||||
minWidth: 150,
|
||||
title: '生产总数',
|
||||
key: 'quantity'
|
||||
},
|
||||
{
|
||||
align:'center',
|
||||
minWidth: 150,
|
||||
title: '转入数量',
|
||||
key: 'transferNum'
|
||||
},
|
||||
{
|
||||
align:'center',
|
||||
minWidth: 150,
|
||||
title: '转出数量',
|
||||
key: 'transferOutNum'
|
||||
},
|
||||
{
|
||||
align:'center',
|
||||
minWidth: 150,
|
||||
title: '是否质检',
|
||||
key: 'qualityInspection',
|
||||
render(row) {
|
||||
const val = row.qualityInspection
|
||||
const opt = qualityInspectionOptions.value.find(o => o.value === val || String(o.value) === String(val))
|
||||
return opt ? opt.label : (val ?? '-')
|
||||
}
|
||||
},
|
||||
{
|
||||
align:'center',
|
||||
minWidth: 150,
|
||||
title: '是否直接入库',
|
||||
key: 'storageEntry',
|
||||
render(row) {
|
||||
const val = row.storageEntry
|
||||
const opt = storageEntryOptions.value.find(o => o.value === val || String(o.value) === String(val))
|
||||
return opt ? opt.label : (val ?? '-')
|
||||
}
|
||||
// 生产订单主表列(宽度按内容:编码/名称偏宽,数量/状态偏窄)
|
||||
const orderColumns: DataTableColumns<OrderProcessPlanVO> = [
|
||||
{
|
||||
type: 'expand',
|
||||
expandable: (row) => (row.processList?.length ?? 0) > 0,
|
||||
renderExpand: (row) => renderProcessExpand(row),
|
||||
},
|
||||
{
|
||||
align:'center',
|
||||
title: '类型',
|
||||
key: 'rowType',
|
||||
width: 88,
|
||||
align: 'center',
|
||||
render: () => h(NTag, { size: 'small', type: 'info', bordered: false }, { default: () => '生产订单' }),
|
||||
},
|
||||
{ title: '生产令号', key: 'mainCode', width: 140, align: 'center', ellipsis: { tooltip: true } },
|
||||
{ title: '物料编码', key: 'materialCode', width: 130, align: 'center', ellipsis: { tooltip: true } },
|
||||
{ title: '物料名称', key: 'materialName', width: 168, ellipsis: { tooltip: true } },
|
||||
{ title: '生产订单编码', key: 'orderCode', width: 150, align: 'center', ellipsis: { tooltip: true } },
|
||||
{ title: '数量', key: 'quantity', width: 72, align: 'center' },
|
||||
{ title: '工艺路线', key: 'routeCode', width: 108, align: 'center', ellipsis: { tooltip: true } },
|
||||
{ title: '生产车间', key: 'proWorkshop', width: 108, align: 'center', ellipsis: { tooltip: true } },
|
||||
{ title: '计划开工', key: 'beginTime', width: 168, align: 'center', ellipsis: { tooltip: true } },
|
||||
{ title: '计划完工', key: 'endTime', width: 168, align: 'center', ellipsis: { tooltip: true } },
|
||||
{ title: '实际开工', key: 'actualStartTime', width: 168, align: 'center', ellipsis: { tooltip: true }, render: (row) => row.actualStartTime || '-' },
|
||||
{ title: '实际完工', key: 'actualEndTime', width: 168, align: 'center', ellipsis: { tooltip: true }, render: (row) => row.actualEndTime || '-' },
|
||||
{ title: '完成数量', key: 'completedQty', width: 88, align: 'center' },
|
||||
{
|
||||
title: '订单状态',
|
||||
key: 'orderStatus',
|
||||
width: 96,
|
||||
align: 'center',
|
||||
render: (row) => renderStatusTag(row.orderStatus, ORDER_STATUS_MAP),
|
||||
},
|
||||
{ title: '当前工序', key: 'currentProcessName', width: 132, align: 'center', ellipsis: { tooltip: true } },
|
||||
{ title: '当前工序号', key: 'currentOperNumber', width: 96, align: 'center' },
|
||||
]
|
||||
|
||||
// 工序明细列(时间列 168,数量列 72,布尔/状态列 64~96)
|
||||
const processColumns: DataTableColumns<ProcessPlanItemVO> = [
|
||||
{
|
||||
title: '类型',
|
||||
key: 'rowType',
|
||||
width: 68,
|
||||
align: 'center',
|
||||
render: () => h(NTag, { size: 'small', type: 'warning', bordered: false }, { default: () => '工序' }),
|
||||
},
|
||||
{ title: '工序号', key: 'operNumber', width: 72, align: 'center' },
|
||||
{ title: '工序编码', key: 'processCode', width: 96, align: 'center', ellipsis: { tooltip: true } },
|
||||
{ title: '工序名称', key: 'processName', width: 140, ellipsis: { tooltip: true } },
|
||||
{ title: '工序说明', key: 'operDescription', width: 160, ellipsis: { tooltip: true } },
|
||||
{ title: '工作中心', key: 'workCenterName', width: 112, align: 'center', ellipsis: { tooltip: true } },
|
||||
{ title: '部门', key: 'departmentName', width: 112, align: 'center', ellipsis: { tooltip: true } },
|
||||
{ title: '计划开工', key: 'planStartTime', width: 168, align: 'center', ellipsis: { tooltip: true } },
|
||||
{ title: '计划完工', key: 'planFinishTime', width: 168, align: 'center', ellipsis: { tooltip: true } },
|
||||
{ title: '实际开工', key: 'actualStartTime', width: 168, align: 'center', ellipsis: { tooltip: true }, render: (row) => row.actualStartTime || '-' },
|
||||
{ title: '实际完工', key: 'actualFinishTime', width: 168, align: 'center', ellipsis: { tooltip: true }, render: (row) => row.actualFinishTime || '-' },
|
||||
{ title: '数量', key: 'quantity', width: 72, align: 'center' },
|
||||
{ title: '转入', key: 'transferInQty', width: 72, align: 'center' },
|
||||
{ title: '转出', key: 'transferOutQty', width: 72, align: 'center' },
|
||||
{ title: '完成', key: 'completedQty', width: 72, align: 'center' },
|
||||
{ title: '报废', key: 'scrapQty', width: 72, align: 'center' },
|
||||
{ title: '返工', key: 'reworkQty', width: 72, align: 'center' },
|
||||
{
|
||||
title: '采购',
|
||||
key: 'procurement',
|
||||
width: 64,
|
||||
align: 'center',
|
||||
render: (row) => renderYesNo(row.procurement, procurementOptions.value),
|
||||
},
|
||||
{
|
||||
title: '委外',
|
||||
key: 'outsource',
|
||||
width: 64,
|
||||
align: 'center',
|
||||
render: (row) => renderYesNo(row.outsource, outsourceOptions.value),
|
||||
},
|
||||
{
|
||||
title: '质检',
|
||||
key: 'qualityInspection',
|
||||
width: 64,
|
||||
align: 'center',
|
||||
render: (row) => renderYesNo(row.qualityInspection, qualityInspectionOptions.value),
|
||||
},
|
||||
{
|
||||
title: '入库',
|
||||
key: 'storageEntry',
|
||||
width: 64,
|
||||
align: 'center',
|
||||
render: (row) => renderYesNo(row.storageEntry, storageEntryOptions.value),
|
||||
},
|
||||
{
|
||||
title: '工序状态',
|
||||
key: 'processStatus',
|
||||
width: 96,
|
||||
align: 'center',
|
||||
render: (row) => renderStatusTag(row.processStatus, PROCESS_STATUS_MAP),
|
||||
},
|
||||
{ title: '派工人', key: 'assignByName', width: 96, align: 'center', ellipsis: { tooltip: true }, render: (row) => row.assignByName || '-' },
|
||||
{ title: '派工时间', key: 'assignTime', width: 168, align: 'center', ellipsis: { tooltip: true }, render: (row) => row.assignTime || '-' },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 140,
|
||||
width: 136,
|
||||
fixed: 'right',
|
||||
render(row) {
|
||||
|
||||
const buttons:any = []
|
||||
|
||||
if(hasPermission('biz:orderItem:edit')){
|
||||
buttons.push(h(NButton, {
|
||||
size: 'small',
|
||||
type:'primary',
|
||||
ghost:true,
|
||||
onClick: () => { handleEdit(row) } },
|
||||
//{ default: () => [h(NIcon, null, { default: () => h(CreateOutline) }), ' 编辑' ]}
|
||||
{ default: () => '编辑'}
|
||||
))
|
||||
}
|
||||
if(hasPermission('biz:orderItem:assign')){
|
||||
buttons.push(h(NButton, {
|
||||
size: 'small',
|
||||
type:'success',
|
||||
ghost:true,
|
||||
onClick: () => { disphand(row) } },
|
||||
{ default: () => '派工'}
|
||||
))
|
||||
}
|
||||
|
||||
return buttons.length > 0 ? h(NSpace, {justify:'center'}, { default: () => buttons }) : '-'
|
||||
|
||||
// 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) }), ' 删除']
|
||||
// })
|
||||
// ])
|
||||
}
|
||||
}
|
||||
align: 'center',
|
||||
render: () => '-',
|
||||
},
|
||||
]
|
||||
|
||||
// 加载数据
|
||||
@ -665,19 +739,31 @@ function handleAdd() {
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
function handleEdit(row:any) {
|
||||
function parseDateTime(val: string | null | undefined) {
|
||||
if (!val) return null
|
||||
const ts = new Date(val.replace(' ', 'T')).getTime()
|
||||
return Number.isNaN(ts) ? null : ts
|
||||
}
|
||||
|
||||
// 编辑(工序明细 -> 实体字段)
|
||||
function handleEdit(process: ProcessPlanItemVO, order?: OrderProcessPlanVO) {
|
||||
modalTitle.value = '编辑工序计划'
|
||||
Object.assign(formData, row)
|
||||
if (formData.beginTime && typeof formData.beginTime === 'string') {
|
||||
formData.beginTime = new Date(formData.beginTime.replace(' ', 'T')).getTime()
|
||||
}
|
||||
if (formData.endTime && typeof formData.endTime === 'string') {
|
||||
formData.endTime = new Date(formData.endTime.replace(' ', 'T')).getTime()
|
||||
}
|
||||
if (formData.createTime && typeof formData.createTime === 'string') {
|
||||
formData.createTime = new Date(formData.createTime.replace(' ', 'T')).getTime()
|
||||
}
|
||||
const entity = processItemToEntity(process, order?.orderItemId)
|
||||
Object.assign(formData, {
|
||||
...defaultFormData,
|
||||
...entity,
|
||||
sort: entity.sort != null ? String(entity.sort) : '',
|
||||
orderItemId: entity.orderItemId != null ? String(entity.orderItemId) : '',
|
||||
quantity: entity.quantity != null ? String(entity.quantity) : '',
|
||||
transferNum: entity.transferNum != null ? String(entity.transferNum) : '',
|
||||
transferOutNum: entity.transferOutNum != null ? String(entity.transferOutNum) : '',
|
||||
procurement: entity.procurement != null ? entity.procurement : '',
|
||||
outsource: entity.outsource != null ? entity.outsource : '',
|
||||
qualityInspection: entity.qualityInspection != null ? entity.qualityInspection : '',
|
||||
storageEntry: entity.storageEntry != null ? entity.storageEntry : '',
|
||||
beginTime: parseDateTime(entity.beginTime),
|
||||
endTime: parseDateTime(entity.endTime),
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
@ -685,7 +771,7 @@ function handleEdit(row:any) {
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate()
|
||||
try {
|
||||
const submitData = { ...formData }
|
||||
const submitData = toOrderProcessPlanPayload({ ...formData })
|
||||
if (typeof submitData.beginTime === 'number') {
|
||||
submitData.beginTime = new Date(submitData.beginTime).toISOString().slice(0, 19).replace('T', ' ')
|
||||
}
|
||||
@ -767,20 +853,20 @@ function deletenum(index:any) {
|
||||
|
||||
|
||||
|
||||
function disphand(v:any){
|
||||
function disphand(process: ProcessPlanItemVO, order?: OrderProcessPlanVO) {
|
||||
const entity = processItemToEntity(process, order?.orderItemId)
|
||||
dispatchmodal.value = true
|
||||
dispatchformRef.value?.restoreValidation()
|
||||
dispatchform.id = v.id
|
||||
dispatchform.name = v.name
|
||||
dispatchform.beginTime = v.beginTime
|
||||
dispatchform.endTime = v.endTime
|
||||
dispatchform.quantity = v.quantity
|
||||
dispatchform.id = entity.id
|
||||
dispatchform.name = entity.name
|
||||
dispatchform.beginTime = entity.beginTime
|
||||
dispatchform.endTime = entity.endTime
|
||||
dispatchform.quantity = entity.quantity
|
||||
dispatchform.userid = ''
|
||||
dispatchform.list = [{
|
||||
userId:'',
|
||||
quantity:''
|
||||
userId: '',
|
||||
quantity: '',
|
||||
}]
|
||||
//slehand()
|
||||
}
|
||||
|
||||
function dispatchSubmit() {
|
||||
@ -813,16 +899,16 @@ function dispatchSubmit() {
|
||||
}
|
||||
|
||||
|
||||
// 删除
|
||||
function handleDelete(row: OrderProcessPlan) {
|
||||
// 删除(工序计划 id)
|
||||
function handleDelete(process: ProcessPlanItemVO) {
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: '确定要删除该记录吗?',
|
||||
content: '确定要删除该工序计划吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await orderProcessPlanApi.delete([row.id!])
|
||||
await orderProcessPlanApi.delete([process.planId!])
|
||||
message.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
@ -943,4 +1029,31 @@ onMounted(() => {
|
||||
.table-toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.n-data-table-tr--expanded td) {
|
||||
background-color: rgba(32, 128, 240, 0.06) !important;
|
||||
}
|
||||
|
||||
.process-expand-wrap {
|
||||
margin: 4px 8px 8px 36px;
|
||||
padding: 12px 14px 14px;
|
||||
border: 1px solid var(--n-border-color);
|
||||
border-left: 4px solid #18a058;
|
||||
border-radius: 6px;
|
||||
background: var(--n-action-color);
|
||||
}
|
||||
|
||||
:deep(.process-plan-table .n-data-table-th) {
|
||||
background: rgba(24, 160, 88, 0.12) !important;
|
||||
}
|
||||
|
||||
.process-expand-empty {
|
||||
margin: 4px 8px 8px 36px;
|
||||
padding: 12px 14px;
|
||||
font-size: 13px;
|
||||
color: var(--n-text-color-3);
|
||||
border: 1px dashed var(--n-border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--n-action-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
455
src/views/biz/processRoute/index.vue
Normal file
455
src/views/biz/processRoute/index.vue
Normal file
@ -0,0 +1,455 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<n-card>
|
||||
<!-- 搜索表单 -->
|
||||
<div class="search-form">
|
||||
<n-form inline :model="searchForm" label-placement="left">
|
||||
<n-form-item label="主键">
|
||||
<n-input v-model:value="searchForm.id" placeholder="请输入主键" clearable />
|
||||
</n-form-item>
|
||||
<n-form-item label="工艺路线名称">
|
||||
<n-input v-model:value="searchForm.name" placeholder="请输入工艺路线名称" clearable />
|
||||
</n-form-item>
|
||||
<n-form-item label="状态:1启用 0禁用">
|
||||
<n-select v-model:value="searchForm.status" placeholder="请选择状态:1启用 0禁用" clearable style="width: 150px" :options="statusOptions" />
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-space>
|
||||
<n-button type="primary" @click="handleSearch">
|
||||
<template #icon><n-icon><SearchOutline /></n-icon></template>
|
||||
搜索
|
||||
</n-button>
|
||||
<n-button @click="handleReset">
|
||||
<template #icon><n-icon><RefreshOutline /></n-icon></template>
|
||||
重置
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div class="table-toolbar">
|
||||
<n-space>
|
||||
<n-button type="primary" @click="handleAdd">
|
||||
<template #icon><n-icon><AddOutline /></n-icon></template>
|
||||
新增
|
||||
</n-button>
|
||||
<n-button @click="importModalVisible = true">
|
||||
<template #icon><n-icon><CloudUploadOutline /></n-icon></template>
|
||||
导入
|
||||
</n-button>
|
||||
<n-button @click="handleExport">
|
||||
<template #icon><n-icon><DownloadOutline /></n-icon></template>
|
||||
导出{{ selectedIds.length > 0 ? `(${selectedIds.length})` : '' }}
|
||||
</n-button>
|
||||
<n-button type="error" :disabled="selectedIds.length === 0" @click="handleBatchDelete">
|
||||
<template #icon><n-icon><TrashOutline /></n-icon></template>
|
||||
删除
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-key="(row) => row.id"
|
||||
:scroll-x="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">
|
||||
<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="工艺路线编码(金蝶 FNumber)" path="code">
|
||||
<n-input v-model:value="formData.code" placeholder="请输入工艺路线编码(金蝶 FNumber)" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="工艺路线名称" path="name">
|
||||
<n-input v-model:value="formData.name" placeholder="请输入工艺路线名称" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="物料编码" path="materialCode">
|
||||
<n-input v-model:value="formData.materialCode" placeholder="请输入物料编码" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="物料名称" path="materialName">
|
||||
<n-input v-model:value="formData.materialName" placeholder="请输入物料名称" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="版本号" path="version">
|
||||
<n-input v-model:value="formData.version" placeholder="请输入版本号" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="状态:1启用 0禁用" path="status">
|
||||
<n-select v-model:value="formData.status" placeholder="请选择状态:1启用 0禁用" :options="statusOptions" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="来源:1金蝶 9手工" path="source">
|
||||
<n-input v-model:value="formData.source" placeholder="请输入来源:1金蝶 9手工" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="来源系统" path="sourceId">
|
||||
<n-input v-model:value="formData.sourceId" placeholder="请输入来源系统" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="变更时间" path="changeTime">
|
||||
<n-date-picker v-model:value="formData.changeTime" type="datetime" clearable style="width: 100%" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi label="变更人" path="changeBy">
|
||||
<n-input v-model:value="formData.changeBy" placeholder="请输入变更人" />
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="modalVisible = false">取消</n-button>
|
||||
<n-button type="primary" @click="handleSubmit">确定</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<!-- 导入弹窗 -->
|
||||
<n-modal v-model:show="importModalVisible" preset="card" title="导入工艺路线主表" style="width: 500px">
|
||||
<n-space vertical>
|
||||
<n-alert type="info">
|
||||
<template #header>导入说明</template>
|
||||
<ul style="margin: 0; padding-left: 16px; line-height: 1.8">
|
||||
<li>请先下载导入模板,按模板格式填写数据</li>
|
||||
<li>支持 .xlsx 或 .xls 格式</li>
|
||||
</ul>
|
||||
</n-alert>
|
||||
<n-space>
|
||||
<n-button type="primary" @click="handleDownloadTemplate">
|
||||
<template #icon><n-icon><DownloadOutline /></n-icon></template>
|
||||
下载模板
|
||||
</n-button>
|
||||
</n-space>
|
||||
<n-upload :max="1" accept=".xlsx,.xls" :show-file-list="true" :custom-request="handleImportUpload">
|
||||
<n-upload-dragger>
|
||||
<div style="margin-bottom: 12px">
|
||||
<n-icon size="48" :depth="3"><CloudUploadOutline /></n-icon>
|
||||
</div>
|
||||
<n-text style="font-size: 16px">点击或拖拽文件到此处上传</n-text>
|
||||
<n-p depth="3" style="margin: 8px 0 0 0">支持 .xlsx 或 .xls 格式</n-p>
|
||||
</n-upload-dragger>
|
||||
</n-upload>
|
||||
</n-space>
|
||||
<template #footer>
|
||||
<n-button @click="importModalVisible = false">关闭</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, h, 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 { processRouteApi, type ProcessRoute } from '@/api/processRoute'
|
||||
import { dictDataApi } from '@/api/org'
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
id: null as number | null,
|
||||
name: '',
|
||||
status: null as number | null,
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<ProcessRoute[]>([])
|
||||
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: ProcessRoute = {
|
||||
code: '',
|
||||
name: '',
|
||||
materialCode: '',
|
||||
materialName: '',
|
||||
version: '',
|
||||
status: undefined,
|
||||
source: undefined,
|
||||
sourceId: '',
|
||||
changeTime: undefined,
|
||||
changeBy: '',
|
||||
}
|
||||
const formData = reactive<ProcessRoute>({ ...defaultFormData })
|
||||
|
||||
// 字典选项(下拉框/单选框/复选框关联字典时使用)
|
||||
const statusOptions = ref<{ label: string; value: any }[]>([])
|
||||
|
||||
// 表单校验规则
|
||||
const formRules = {
|
||||
code: { required: true, message: '请输入工艺路线编码(金蝶 FNumber)', trigger: 'blur' },
|
||||
}
|
||||
|
||||
// 表格列
|
||||
const columns: DataTableColumns<ProcessRoute> = [
|
||||
{ type: 'selection' },
|
||||
{ title: '主键', key: 'id' },
|
||||
{ title: '工艺路线编码(金蝶 FNumber)', key: 'code' },
|
||||
{ title: '工艺路线名称', key: 'name' },
|
||||
{ title: '物料编码', key: 'materialCode' },
|
||||
{ title: '物料名称', key: 'materialName' },
|
||||
{ title: '版本号', key: 'version' },
|
||||
{ title: '状态:1启用 0禁用', key: 'status',
|
||||
render(row) {
|
||||
const val = row.status
|
||||
const opt = statusOptions.value.find(o => o.value === val || String(o.value) === String(val))
|
||||
return opt ? opt.label : (val ?? '-')
|
||||
}
|
||||
},
|
||||
{ title: '来源:1金蝶 9手工', key: 'source' },
|
||||
{ title: '来源系统', key: 'sourceId' },
|
||||
{ title: '备注', key: 'remark' },
|
||||
{ title: '创建时间', key: 'createTime', width: 180 },
|
||||
{ title: '创建人', key: 'createBy' },
|
||||
{ title: '变更时间', key: 'changeTime' },
|
||||
{ title: '变更人', key: 'changeBy' },
|
||||
{
|
||||
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) }), ' 删除']
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 加载数据
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await processRouteApi.page({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
id: searchForm.id || undefined,
|
||||
name: searchForm.name || undefined,
|
||||
status: searchForm.status || undefined,
|
||||
})
|
||||
tableData.value = res.list
|
||||
pagination.itemCount = res.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchForm.id = null
|
||||
|
||||
searchForm.name = ''
|
||||
|
||||
searchForm.status = null
|
||||
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
// 分页
|
||||
function handlePageChange(page: number) {
|
||||
pagination.page = page
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
pagination.pageSize = pageSize
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 选择
|
||||
function handleCheck(keys: Array<string | number>) {
|
||||
selectedIds.value = keys as number[]
|
||||
}
|
||||
|
||||
// 新增
|
||||
function handleAdd() {
|
||||
modalTitle.value = '新增工艺路线主表'
|
||||
Object.assign(formData, defaultFormData)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
function handleEdit(row: ProcessRoute) {
|
||||
modalTitle.value = '编辑工艺路线主表'
|
||||
Object.assign(formData, row)
|
||||
if (formData.createTime && typeof formData.createTime === 'string') {
|
||||
formData.createTime = new Date(formData.createTime.replace(' ', 'T')).getTime()
|
||||
}
|
||||
if (formData.changeTime && typeof formData.changeTime === 'string') {
|
||||
formData.changeTime = new Date(formData.changeTime.replace(' ', 'T')).getTime()
|
||||
}
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 提交
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate()
|
||||
try {
|
||||
const submitData = { ...formData } as ProcessRoute
|
||||
if (typeof submitData.createTime === 'number') {
|
||||
submitData.createTime = new Date(submitData.createTime).toISOString().slice(0, 19).replace('T', ' ')
|
||||
}
|
||||
if (typeof submitData.changeTime === 'number') {
|
||||
submitData.changeTime = new Date(submitData.changeTime).toISOString().slice(0, 19).replace('T', ' ')
|
||||
}
|
||||
if (submitData.id) {
|
||||
await processRouteApi.update(submitData)
|
||||
message.success('修改成功')
|
||||
} else {
|
||||
await processRouteApi.create(submitData)
|
||||
message.success('新增成功')
|
||||
}
|
||||
modalVisible.value = false
|
||||
loadData()
|
||||
} catch (error) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
function handleDelete(row: ProcessRoute) {
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: '确定要删除该记录吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await processRouteApi.delete([row.id!])
|
||||
message.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
function handleBatchDelete() {
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: `确定要删除选中的 ${selectedIds.value.length} 条记录吗?`,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await processRouteApi.delete(selectedIds.value)
|
||||
message.success('删除成功')
|
||||
selectedIds.value = []
|
||||
loadData()
|
||||
} catch (error) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 导出
|
||||
async function handleExport() {
|
||||
try {
|
||||
const params: Record<string, any> = {}
|
||||
if (selectedIds.value.length > 0) params.ids = selectedIds.value
|
||||
if (searchForm.id != null) params.id = searchForm.id
|
||||
if (searchForm.name) params.name = searchForm.name
|
||||
if (searchForm.status != null) params.status = searchForm.status
|
||||
const blob = await processRouteApi.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 processRouteApi.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 processRouteApi.importData(file.file)
|
||||
if (result.fail > 0) {
|
||||
dialog.warning({
|
||||
title: '导入结果',
|
||||
content: `成功: ${result.success} 条,失败: ${result.fail} 条\n错误信息: ${(result.errors || []).join('\n') || '无'}`,
|
||||
positiveText: '确定'
|
||||
})
|
||||
} else {
|
||||
message.success(`导入成功,共 ${result.success} 条数据`)
|
||||
importModalVisible.value = false
|
||||
}
|
||||
loadData()
|
||||
} catch (error) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
// 加载字典选项
|
||||
async function loadDictOptions() {
|
||||
try {
|
||||
const data = await dictDataApi.listByType('pro_status')
|
||||
statusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue) }))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadDictOptions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
459
src/views/biz/processRouteStep/index.vue
Normal file
459
src/views/biz/processRouteStep/index.vue
Normal file
@ -0,0 +1,459 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<n-card>
|
||||
<!-- 搜索表单 -->
|
||||
<div class="search-form">
|
||||
<n-form inline :model="searchForm" label-placement="left">
|
||||
<n-form-item label="主键">
|
||||
<n-input v-model:value="searchForm.id" placeholder="请输入主键" clearable />
|
||||
</n-form-item>
|
||||
<n-form-item label="状态">
|
||||
<n-select v-model:value="searchForm.status" placeholder="请选择状态" clearable style="width: 150px" :options="statusOptions" />
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-space>
|
||||
<n-button type="primary" @click="handleSearch">
|
||||
<template #icon><n-icon><SearchOutline /></n-icon></template>
|
||||
搜索
|
||||
</n-button>
|
||||
<n-button @click="handleReset">
|
||||
<template #icon><n-icon><RefreshOutline /></n-icon></template>
|
||||
重置
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div class="table-toolbar">
|
||||
<n-space>
|
||||
<n-button type="primary" @click="handleAdd">
|
||||
<template #icon><n-icon><AddOutline /></n-icon></template>
|
||||
新增
|
||||
</n-button>
|
||||
<n-button @click="importModalVisible = true">
|
||||
<template #icon><n-icon><CloudUploadOutline /></n-icon></template>
|
||||
导入
|
||||
</n-button>
|
||||
<n-button @click="handleExport">
|
||||
<template #icon><n-icon><DownloadOutline /></n-icon></template>
|
||||
导出{{ selectedIds.length > 0 ? `(${selectedIds.length})` : '' }}
|
||||
</n-button>
|
||||
<n-button type="error" :disabled="selectedIds.length === 0" @click="handleBatchDelete">
|
||||
<template #icon><n-icon><TrashOutline /></n-icon></template>
|
||||
删除
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-key="(row) => row.id"
|
||||
:scroll-x="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: 600px">
|
||||
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="100px">
|
||||
<n-form-item label="工艺路线id" path="routeId">
|
||||
<n-input v-model:value="formData.routeId" placeholder="请输入工艺路线id" />
|
||||
</n-form-item>
|
||||
<n-form-item label="工序号" path="operNumber">
|
||||
<n-input v-model:value="formData.operNumber" placeholder="请输入工序号" />
|
||||
</n-form-item>
|
||||
<n-form-item label="工序名称" path="processName">
|
||||
<n-input v-model:value="formData.processName" placeholder="请输入工序名称" />
|
||||
</n-form-item>
|
||||
<n-form-item label="工序说明" path="operDescription">
|
||||
<n-input v-model:value="formData.operDescription" type="textarea" placeholder="请输入工序说明" />
|
||||
</n-form-item>
|
||||
<n-form-item label="工作中心" path="workCenterName">
|
||||
<n-input v-model:value="formData.workCenterName" placeholder="请输入工作中心" />
|
||||
</n-form-item>
|
||||
<n-form-item label="生产车间" path="departmentName">
|
||||
<n-input v-model:value="formData.departmentName" placeholder="请输入生产车间" />
|
||||
</n-form-item>
|
||||
<n-form-item label="活动单位" path="activityUnit">
|
||||
<n-input v-model:value="formData.activityUnit" placeholder="请输入活动单位" />
|
||||
</n-form-item>
|
||||
<n-form-item label="工序控制码" path="optCtrlCodeName">
|
||||
<n-input v-model:value="formData.optCtrlCodeName" placeholder="请输入工序控制码" />
|
||||
</n-form-item>
|
||||
<n-form-item label="活动量" path="activityQty">
|
||||
<n-input v-model:value="formData.activityQty" placeholder="请输入活动量" />
|
||||
</n-form-item>
|
||||
<n-form-item label="排序" path="sort">
|
||||
<n-input v-model:value="formData.sort" placeholder="请输入排序" />
|
||||
</n-form-item>
|
||||
<n-form-item label="是否委外" path="isOutsource">
|
||||
<n-input v-model:value="formData.isOutsource" placeholder="请输入是否委外" />
|
||||
</n-form-item>
|
||||
<n-form-item label="状态" path="status">
|
||||
<n-select v-model:value="formData.status" placeholder="请选择状态" :options="statusOptions" />
|
||||
</n-form-item>
|
||||
<n-form-item label="变更时间" path="changeTime">
|
||||
<n-date-picker v-model:value="formData.changeTime" type="datetime" clearable style="width: 100%" />
|
||||
</n-form-item>
|
||||
</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="importModalVisible" preset="card" title="导入工艺路线工序明细" style="width: 500px">
|
||||
<n-space vertical>
|
||||
<n-alert type="info">
|
||||
<template #header>导入说明</template>
|
||||
<ul style="margin: 0; padding-left: 16px; line-height: 1.8">
|
||||
<li>请先下载导入模板,按模板格式填写数据</li>
|
||||
<li>支持 .xlsx 或 .xls 格式</li>
|
||||
</ul>
|
||||
</n-alert>
|
||||
<n-space>
|
||||
<n-button type="primary" @click="handleDownloadTemplate">
|
||||
<template #icon><n-icon><DownloadOutline /></n-icon></template>
|
||||
下载模板
|
||||
</n-button>
|
||||
</n-space>
|
||||
<n-upload :max="1" accept=".xlsx,.xls" :show-file-list="true" :custom-request="handleImportUpload">
|
||||
<n-upload-dragger>
|
||||
<div style="margin-bottom: 12px">
|
||||
<n-icon size="48" :depth="3"><CloudUploadOutline /></n-icon>
|
||||
</div>
|
||||
<n-text style="font-size: 16px">点击或拖拽文件到此处上传</n-text>
|
||||
<n-p depth="3" style="margin: 8px 0 0 0">支持 .xlsx 或 .xls 格式</n-p>
|
||||
</n-upload-dragger>
|
||||
</n-upload>
|
||||
</n-space>
|
||||
<template #footer>
|
||||
<n-button @click="importModalVisible = false">关闭</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, h, 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 { processRouteStepApi, type ProcessRouteStep } from '@/api/processRouteStep'
|
||||
import { dictDataApi } from '@/api/org'
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
id: null as number | null,
|
||||
status: null as number | null,
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<ProcessRouteStep[]>([])
|
||||
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: ProcessRouteStep = {
|
||||
routeId: undefined,
|
||||
operNumber: undefined,
|
||||
processName: '',
|
||||
operDescription: '',
|
||||
workCenterName: '',
|
||||
departmentName: '',
|
||||
activityUnit: '',
|
||||
optCtrlCodeName: '',
|
||||
activityQty: undefined,
|
||||
sort: undefined,
|
||||
isOutsource: undefined,
|
||||
status: undefined,
|
||||
changeTime: undefined,
|
||||
}
|
||||
const formData = reactive<ProcessRouteStep>({ ...defaultFormData })
|
||||
|
||||
// 字典选项(下拉框/单选框/复选框关联字典时使用)
|
||||
const statusOptions = ref<{ label: string; value: any }[]>([])
|
||||
|
||||
// 表单校验规则
|
||||
const formRules = {
|
||||
routeId: { required: true, message: '请输入工艺路线id', trigger: 'blur' },
|
||||
operNumber: { required: true, message: '请输入工序号', trigger: 'blur' },
|
||||
}
|
||||
|
||||
// 表格列
|
||||
const columns: DataTableColumns<ProcessRouteStep> = [
|
||||
{ type: 'selection' },
|
||||
{ title: '主键', key: 'id' },
|
||||
{ title: '工艺路线id', key: 'routeId' },
|
||||
{ title: '工序号', key: 'operNumber' },
|
||||
{ title: '工序名称', key: 'processName' },
|
||||
{ title: '工序说明', key: 'operDescription' },
|
||||
{ title: '工作中心', key: 'workCenterName' },
|
||||
{ title: '生产车间', key: 'departmentName' },
|
||||
{ title: '活动单位', key: 'activityUnit' },
|
||||
{ title: '工序控制码', key: 'optCtrlCodeName' },
|
||||
{ title: '活动量', key: 'activityQty' },
|
||||
{ title: '排序', key: 'sort' },
|
||||
{ title: '是否委外', key: 'isOutsource' },
|
||||
{ title: '状态', key: 'status',
|
||||
render(row) {
|
||||
const val = row.status
|
||||
const opt = statusOptions.value.find(o => o.value === val || String(o.value) === String(val))
|
||||
return opt ? opt.label : (val ?? '-')
|
||||
}
|
||||
},
|
||||
{ title: '创建时间', key: 'createTime', width: 180 },
|
||||
{ title: '变更时间', key: 'changeTime' },
|
||||
{
|
||||
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) }), ' 删除']
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 加载数据
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await processRouteStepApi.page({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
id: searchForm.id || undefined,
|
||||
status: searchForm.status || undefined,
|
||||
})
|
||||
tableData.value = res.list
|
||||
pagination.itemCount = res.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchForm.id = null
|
||||
|
||||
searchForm.status = null
|
||||
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
// 分页
|
||||
function handlePageChange(page: number) {
|
||||
pagination.page = page
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
pagination.pageSize = pageSize
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 选择
|
||||
function handleCheck(keys: Array<string | number>) {
|
||||
selectedIds.value = keys as number[]
|
||||
}
|
||||
|
||||
// 新增
|
||||
function handleAdd() {
|
||||
modalTitle.value = '新增工艺路线工序明细'
|
||||
Object.assign(formData, defaultFormData)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
function handleEdit(row: ProcessRouteStep) {
|
||||
modalTitle.value = '编辑工艺路线工序明细'
|
||||
Object.assign(formData, row)
|
||||
if (formData.createTime && typeof formData.createTime === 'string') {
|
||||
formData.createTime = new Date(formData.createTime.replace(' ', 'T')).getTime()
|
||||
}
|
||||
if (formData.changeTime && typeof formData.changeTime === 'string') {
|
||||
formData.changeTime = new Date(formData.changeTime.replace(' ', 'T')).getTime()
|
||||
}
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 提交
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate()
|
||||
try {
|
||||
const submitData = { ...formData } as ProcessRouteStep
|
||||
if (typeof submitData.createTime === 'number') {
|
||||
submitData.createTime = new Date(submitData.createTime).toISOString().slice(0, 19).replace('T', ' ')
|
||||
}
|
||||
if (typeof submitData.changeTime === 'number') {
|
||||
submitData.changeTime = new Date(submitData.changeTime).toISOString().slice(0, 19).replace('T', ' ')
|
||||
}
|
||||
if (submitData.id) {
|
||||
await processRouteStepApi.update(submitData)
|
||||
message.success('修改成功')
|
||||
} else {
|
||||
await processRouteStepApi.create(submitData)
|
||||
message.success('新增成功')
|
||||
}
|
||||
modalVisible.value = false
|
||||
loadData()
|
||||
} catch (error) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
function handleDelete(row: ProcessRouteStep) {
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: '确定要删除该记录吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await processRouteStepApi.delete([row.id!])
|
||||
message.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
function handleBatchDelete() {
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: `确定要删除选中的 ${selectedIds.value.length} 条记录吗?`,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await processRouteStepApi.delete(selectedIds.value)
|
||||
message.success('删除成功')
|
||||
selectedIds.value = []
|
||||
loadData()
|
||||
} catch (error) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 导出
|
||||
async function handleExport() {
|
||||
try {
|
||||
const params: Record<string, any> = {}
|
||||
if (selectedIds.value.length > 0) params.ids = selectedIds.value
|
||||
if (searchForm.id != null) params.id = searchForm.id
|
||||
if (searchForm.status != null) params.status = searchForm.status
|
||||
const blob = await processRouteStepApi.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 processRouteStepApi.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 processRouteStepApi.importData(file.file)
|
||||
if (result.fail > 0) {
|
||||
dialog.warning({
|
||||
title: '导入结果',
|
||||
content: `成功: ${result.success} 条,失败: ${result.fail} 条\n错误信息: ${(result.errors || []).join('\n') || '无'}`,
|
||||
positiveText: '确定'
|
||||
})
|
||||
} else {
|
||||
message.success(`导入成功,共 ${result.success} 条数据`)
|
||||
importModalVisible.value = false
|
||||
}
|
||||
loadData()
|
||||
} catch (error) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
// 加载字典选项
|
||||
async function loadDictOptions() {
|
||||
try {
|
||||
const data = await dictDataApi.listByType('sys_status')
|
||||
statusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue) }))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadDictOptions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@ -40,7 +40,7 @@
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-key="(row) => row.id"
|
||||
:row-key="(row:any) => row.id"
|
||||
:scroll-x="1200"
|
||||
@update:page="handlePageChange"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
@ -54,9 +54,9 @@
|
||||
</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 { ref, reactive, onMounted } from 'vue'
|
||||
import { NButton, NSpace, NIcon, type DataTableColumns} from 'naive-ui'
|
||||
import { SearchOutline, RefreshOutline } from '@vicons/ionicons5'
|
||||
import { qcResultDetailApi, type QcResultDetail } from '@/api/qcResultDetail'
|
||||
import { dictDataApi } from '@/api/org'
|
||||
|
||||
|
||||
@ -213,7 +213,7 @@
|
||||
:columns="detailColumns"
|
||||
:data="detailTableData"
|
||||
:loading="loading"
|
||||
:row-key="(row) => row.id"
|
||||
:row-key="(row:any) => row.id"
|
||||
:scroll-x="700"
|
||||
max-height="320"
|
||||
:pagination="{ pageSize:4 }"
|
||||
@ -245,15 +245,9 @@ import { qualityTestingApi, type QualityTesting } from '@/api/qualityTesting'
|
||||
import {assingReworkApi} from '@/api/assingRework'
|
||||
import { dictDataApi } from '@/api/org'
|
||||
|
||||
import {Language} from "@xterm/xterm/src/vs/base/common/platform.ts";
|
||||
|
||||
import {fileApi} from '@/api/system1'
|
||||
|
||||
import value = Language.value;
|
||||
import {QcResultDetail} from "@/api/qcResultDetail.ts";
|
||||
import router from "@/router";
|
||||
import dd from 'dingtalk-jsapi'
|
||||
import { env } from 'echarts'
|
||||
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
@ -274,7 +268,7 @@ const pagination = reactive({
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
qcNo: null as number | null,
|
||||
qcNo: null as string | null,
|
||||
status: null as number | null,
|
||||
})
|
||||
|
||||
@ -290,7 +284,7 @@ const infoIsCollapse = ref(false)
|
||||
const detailIsCollapse = ref(true)
|
||||
|
||||
//提交判定明细
|
||||
const submitDetailModalVisible = ref(false)
|
||||
// const submitDetailModalVisible = ref(false)
|
||||
const traceCodeShow = ref(false)
|
||||
//提交明细/部分提交明细标识
|
||||
const submitFlag = ref<Boolean>()
|
||||
@ -334,10 +328,10 @@ const infoDataMapping = {
|
||||
inspectTime: '质检时间',
|
||||
traceType: '追溯类型'
|
||||
}
|
||||
const infoDataMap = new Map(Object.entries(infoDataMapping))
|
||||
|
||||
const mappedInfoList = ref<Array<{ label: string; key: string; value: any }>>([])
|
||||
//详情列表名
|
||||
const detailColumns: DataTableColumns<Object> = [
|
||||
const detailColumns: DataTableColumns<QcResultDetail> = [
|
||||
{
|
||||
title: '判定类型',
|
||||
key: 'resultType',
|
||||
@ -403,7 +397,7 @@ const detailTableData = ref<Object[]>([])
|
||||
|
||||
const formRef = ref()
|
||||
const defaultFormData: QualityTesting = {
|
||||
qcNo: '',
|
||||
qcNo: undefined,
|
||||
sourceType: undefined,
|
||||
sourceId: undefined,
|
||||
orderId: undefined,
|
||||
@ -417,38 +411,38 @@ const defaultFormData: QualityTesting = {
|
||||
}
|
||||
const formData = reactive<QualityTesting>({ ...defaultFormData })
|
||||
|
||||
const defaultFormDetailData:QcResultDetail = {
|
||||
qcId: undefined,
|
||||
resultType: undefined,
|
||||
qty: 0,
|
||||
traceCode: undefined,
|
||||
defectCode: undefined,
|
||||
handleAction: undefined,
|
||||
actionStatus: undefined
|
||||
}
|
||||
// const defaultFormDetailData:QcResultDetail = {
|
||||
// qcId: undefined,
|
||||
// resultType: undefined,
|
||||
// qty: 0,
|
||||
// traceCode: undefined,
|
||||
// defectCode: undefined,
|
||||
// handleAction: undefined,
|
||||
// actionStatus: undefined
|
||||
// }
|
||||
|
||||
|
||||
const formDetailOKData = reactive<QcResultDetail>({ ...defaultFormDetailData, resultType: 1,handleAction:'下一道工序',actionStatus:1 })
|
||||
const formDetailScrapData = reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 2,handleAction:'报废次品',actionStatus:1 })
|
||||
const formDetailReworkData = reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 3,handleAction:'返工',actionStatus:1 })
|
||||
const formDetailConcessionData = reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 4,handleAction:'让步接收',actionStatus:1 })
|
||||
// const formDetailOKData = reactive<QcResultDetail>({ ...defaultFormDetailData, resultType: 1,handleAction:'下一道工序',actionStatus:1 })
|
||||
// const formDetailScrapData = reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 2,handleAction:'报废次品',actionStatus:1 })
|
||||
// const formDetailReworkData = reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 3,handleAction:'返工',actionStatus:1 })
|
||||
// const formDetailConcessionData = reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 4,handleAction:'让步接收',actionStatus:1 })
|
||||
|
||||
const assingWorkDisabled = ref(true)
|
||||
|
||||
const assingWork = ref<{ label: string; value: any }[]>([])
|
||||
|
||||
// 字典选项(下拉框/单选框/复选框关联字典时使用)
|
||||
const statusOptions = ref<{ label: string; value: any }[]>([])
|
||||
const statusOptions = ref<{ label: string; value: any;class:any}[]>([])
|
||||
//质检状态字典
|
||||
const qcStatusOptions = ref<{ label: string; value: any }[]>([])
|
||||
const qcStatusOptions = ref<{ label: string; value: any;class:any }[]>([])
|
||||
//业务来源字典
|
||||
const qcSourceTypeOptions = ref<{ label: string; value: any }[]>([])
|
||||
const qcSourceTypeOptions = ref<{ label: string; value: any;class:any }[]>([])
|
||||
//追溯状态字段
|
||||
const qcTraceTypeOptions = ref<{ label: string; value: any }[]>([])
|
||||
const qcTraceTypeOptions = ref<{ label: string; value: any;class:any }[]>([])
|
||||
//判定类型
|
||||
const qcResultTypeOptions = ref<{ label: string; value: any }[]>([])
|
||||
const qcResultTypeOptions = ref<{ label: string; value: any;class:any }[]>([])
|
||||
//动作执行状态
|
||||
const qcActionStatusOptions = ref<{ label: string; value: any }[]>([])
|
||||
const qcActionStatusOptions = ref<{ label: string; value: any;class:any }[]>([])
|
||||
// 表单校验规则
|
||||
const formRules = {
|
||||
sourceType: [{ required: true, type: 'number', message: '请选择来源类型:1工序汇报/2委外/3手工', trigger: 'change' }],
|
||||
@ -577,7 +571,7 @@ function handleSearch() {
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchForm.id = null
|
||||
searchForm.qcNo = null
|
||||
searchForm.status = null
|
||||
|
||||
handleSearch()
|
||||
@ -699,7 +693,7 @@ async function handleExport() {
|
||||
try {
|
||||
const params: Record<string, any> = {}
|
||||
if (selectedIds.value.length > 0) params.ids = selectedIds.value
|
||||
if (searchForm.id != null) params.id = searchForm.id
|
||||
if (searchForm.qcNo != null) params.id = searchForm.qcNo
|
||||
if (searchForm.status != null) params.status = searchForm.status
|
||||
const blob = await qualityTestingApi.export(params)
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
@ -837,12 +831,12 @@ function formatDisplayValue(key: string, value: any): string {
|
||||
async function loadAssignWorkOrder() {
|
||||
const data = await assingReworkApi.allList()
|
||||
console.log(data)
|
||||
assingWork.value = data.map(d => ({ label: d.assingCode, value: (Number(d.id) || d.id) }))
|
||||
assingWork.value = data.map((d:any) => ({ label: d.assingCode, value: (Number(d.id) || d.id) }))
|
||||
console.log(assingWork)
|
||||
}
|
||||
|
||||
//提交判定明细
|
||||
function submitResultDetail(row) {
|
||||
function submitResultDetail(row:any) {
|
||||
|
||||
if (row.traceType != 1) {
|
||||
traceCodeShow.value = true
|
||||
@ -856,36 +850,14 @@ function submitResultDetail(row) {
|
||||
|
||||
|
||||
|
||||
async function handleSubmitResultDetail() {
|
||||
|
||||
formDetailConcessionData.files = uploadFiles.value
|
||||
|
||||
const submitParam = [
|
||||
formDetailOKData,
|
||||
formDetailScrapData,
|
||||
formDetailReworkData,
|
||||
formDetailConcessionData]
|
||||
console.log(submitParam);
|
||||
|
||||
submitFlag.value? await qualityTestingApi.submit(submitParam) : await qualityTestingApi.submitBF(submitParam)
|
||||
|
||||
message.success('提交成功')
|
||||
submitDetailModalVisible.value = false
|
||||
loadData()
|
||||
}
|
||||
|
||||
|
||||
//提交部分判定明细
|
||||
function submitBFResultDetail(row) {
|
||||
|
||||
function submitBFResultDetail(row:any) {
|
||||
|
||||
if (row.traceType != 1) {
|
||||
traceCodeShow.value = true
|
||||
}
|
||||
submitFlag.value = false
|
||||
|
||||
|
||||
//version-2
|
||||
let path = `/qc/submitDetail/${row.qcNo}/${row.id}/${submitFlag.value}/${traceCodeShow.value}`
|
||||
router.push(path)
|
||||
|
||||
@ -893,13 +865,13 @@ function submitBFResultDetail(row) {
|
||||
|
||||
|
||||
//查询判定明细
|
||||
function queryResultDetail(row) {
|
||||
function queryResultDetail(row:any) {
|
||||
let path = `detail/${row.id}`;
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
//质检撤回
|
||||
function handleCancelQuality(row) {
|
||||
function handleCancelQuality(row:any) {
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: '确定要撤回ID:'+row.id+'质检记录吗?',
|
||||
|
||||
@ -134,7 +134,6 @@
|
||||
multiple
|
||||
directory-dnd
|
||||
:max="5"
|
||||
:action="null"
|
||||
:custom-request="()=>{}"
|
||||
@change="handleUploadChange"
|
||||
v-model:file-list="fileList"
|
||||
@ -173,19 +172,19 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, h, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import {
|
||||
NButton, NSpace, NIcon, NUpload,NTag, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions,
|
||||
NDropdown
|
||||
NButton, NSpace, NIcon, NUpload, useMessage
|
||||
} from 'naive-ui'
|
||||
|
||||
import {useRoute} from "vue-router";
|
||||
|
||||
import {QcResultDetail} from "@/api/qcResultDetail.ts";
|
||||
import {fileApi} from '@/api/system1'
|
||||
import { qualityTestingApi, type QualityTesting } from '@/api/qualityTesting'
|
||||
import { qualityTestingApi} from '@/api/qualityTesting'
|
||||
import router from '@/router';
|
||||
|
||||
|
||||
const message = useMessage()
|
||||
const route = useRoute()
|
||||
const qcId =ref( Number(route.params.id))
|
||||
|
||||
@ -211,6 +211,7 @@ const pagination = reactive({
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
page:pagination.page as number,
|
||||
assingStatus:undefined
|
||||
})
|
||||
|
||||
// 弹窗
|
||||
@ -229,13 +230,13 @@ const reportDetailVisible = ref(false)
|
||||
|
||||
// 字典选项(下拉框/单选框/复选框关联字典时使用)
|
||||
//判定状态字典
|
||||
const qcResultTypeOptions = ref<{ label: string; value: any }[]>([])
|
||||
const qcResultTypeOptions = ref<{ label: string; value: any;class:any }[]>([])
|
||||
//业务来源字典
|
||||
const qcSourceTypeOptions = ref<{ label: string; value: any }[]>([])
|
||||
const qcSourceTypeOptions = ref<{ label: string; value: any;class:any }[]>([])
|
||||
//质检状态字典
|
||||
const qcStatusOptions = ref<{ label: string; value: any }[]>([])
|
||||
const qcStatusOptions = ref<{ label: string; value: any;class:any }[]>([])
|
||||
//派工状态字典
|
||||
const qcAssingStatusOptions = ref<{ label: string; value: any }[]>([])
|
||||
const qcAssingStatusOptions = ref<{ label: string; value: any ;class:any}[]>([])
|
||||
// 表单校验规则
|
||||
const formRules = {
|
||||
}
|
||||
@ -280,7 +281,7 @@ const columns: DataTableColumns<SubmitLog> = [
|
||||
render(row) {
|
||||
return h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'nowrap' } }, [
|
||||
h(NButton, { size: 'small', quaternary: true, onClick: () => handleViewReport(row) }, {
|
||||
default: () => [h(NIcon, null, { default: () => h(null) }), ' 汇报信息']
|
||||
default: () => ['汇报信息']
|
||||
}),
|
||||
// h(NButton, { size: 'small', quaternary: true, onClick: () => handleEdit(row) }, {
|
||||
// default: () => [h(NIcon, null, { default: () => h(CreateOutline) }), ' 编辑']
|
||||
@ -300,7 +301,7 @@ const submitColumns = ref([
|
||||
{ title: "提交时间", key: "submitCreateTime" },
|
||||
{ title: "提交数量", key: "quantity" },
|
||||
{ title: "单据状态", key: "submitStatus",
|
||||
render: (row) => {
|
||||
render: (row:any) => {
|
||||
const val = row.submitStatus
|
||||
const opt = qcResultTypeOptions.value.find(o => o.value === val || String(o.value) === String(val))
|
||||
if (!opt) return val ?? '-'
|
||||
@ -335,7 +336,7 @@ function handleSearch() {
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchForm.assingStatus = null
|
||||
searchForm.assingStatus = undefined
|
||||
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
@ -92,8 +92,8 @@ const statisticTypeOptions = ref<{ label: string; value: any }[]>([])
|
||||
|
||||
//统计属性
|
||||
const searchForm =reactive({
|
||||
type: null as string,
|
||||
timestamp: null as string
|
||||
type:undefined as any,
|
||||
timestamp: undefined as any
|
||||
})
|
||||
|
||||
const stats = reactive({
|
||||
|
||||
@ -396,13 +396,13 @@ const userStore = useUserStore()
|
||||
const hasPermission = (permission: string) => userStore.hasPermission(permission)
|
||||
|
||||
//判定状态字典
|
||||
const qcResultTypeOptions = ref<{ label: string; value: any }[]>([])
|
||||
const qcResultTypeOptions = ref<{ label: string; value: any;class:any }[]>([])
|
||||
//业务来源字典
|
||||
const qcSourceTypeOptions = ref<{ label: string; value: any }[]>([])
|
||||
const qcSourceTypeOptions = ref<{ label: string; value: any;class:any }[]>([])
|
||||
//质检状态字典
|
||||
const qcStatusOptions = ref<{ label: string; value: any }[]>([])
|
||||
const qcStatusOptions = ref<{ label: string; value: any;class:any }[]>([])
|
||||
//派工状态字典
|
||||
const qcAssingStatusOptions = ref<{ label: string; value: any }[]>([])
|
||||
const qcAssingStatusOptions = ref<{ label: string; value: any;class:any }[]>([])
|
||||
|
||||
//汇报详情
|
||||
const infoIsCollapse = ref(false)
|
||||
@ -418,7 +418,7 @@ const submitColumns = ref([
|
||||
{ title: "提交时间", key: "submitCreateTime" },
|
||||
{ title: "提交数量", key: "quantity" },
|
||||
{ title: "单据状态", key: "submitStatus",
|
||||
render: (row) => {
|
||||
render: (row:any) => {
|
||||
const val = row.submitStatus
|
||||
const opt = qcResultTypeOptions.value.find(o => o.value === val || String(o.value) === String(val))
|
||||
if (!opt) return val ?? '-'
|
||||
|
||||
@ -199,7 +199,7 @@ const hasPermission = (permission: string) => userStore.hasPermission(permission
|
||||
|
||||
|
||||
//派工状态字典
|
||||
const qcAssingStatusOptions = ref<{ label: string; value: any }[]>([])
|
||||
const qcAssingStatusOptions = ref<{ label: string; value: any;class:any }[]>([])
|
||||
|
||||
// ==================== 搜索表单 ====================
|
||||
const searchForm = reactive({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user