This commit is contained in:
andy 2026-06-18 08:48:49 +08:00
commit 1382a19d3d
23 changed files with 3287 additions and 355 deletions

View File

@ -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' })
},

View File

@ -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
}
/** 工序计划明细 VOprocessList 项) */
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
View 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' })
}
}

View 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' })
}
}

View File

@ -8,7 +8,7 @@ export interface QcResultDetail {
resultType?: number
qty?: string
qty?: number
traceCode?: string

View File

@ -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' })
},

View File

@ -1,31 +1,31 @@
<!--
站点 Logo 展示优先显示图片 URL 或加载失败时回退为站点名称首字母
用于 layoutloginregister 等页面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 或加载失败时回退为站点名称首字母
用于 layoutloginregister 等页面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',
})

View 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
}

View File

@ -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: '提示',

View File

@ -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

View 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>

View 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>

View 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>

View File

@ -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>

View 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>

View 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>

View File

@ -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'

View File

@ -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+'质检记录吗?',

View File

@ -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))

View File

@ -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()
}

View File

@ -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({

View File

@ -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 ?? '-'

View File

@ -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({