This commit is contained in:
andy 2026-06-11 09:04:23 +08:00
commit ab120a1256
31 changed files with 4138 additions and 723 deletions

View File

@ -0,0 +1,82 @@
import { request } from '@/utils/request'
// 让步接收申请表 类型定义
export interface ConcessionApply {
id?: number
qcId?: number
concessionQty?: number
applyRemark?: string
applyContent?: string
applyFile?: string
applyUserId?: number
deptId?: number
instanceId?: string
approveStatus?: number
createTime?: string
updateTime?: string
}
// 让步接收申请表 API
export const concessionApplyApi = {
// 分页查询
page(params: { page: number; pageSize: number; id?: number }) {
return request({ url: '/biz/concessionApply/page', method: 'get', params })
},
// 获取详情
detail(id: string) {
return request({ url: `/biz/concessionApply/${id}`, method: 'get' })
},
// 新增
create(data: ConcessionApply) {
return request({ url: '/biz/concessionApply', method: 'post', data })
},
// 修改
update(data: ConcessionApply) {
return request({ url: '/biz/concessionApply', method: 'put', data })
},
// 删除
delete(ids: string[]) {
return request({ url: `/biz/concessionApply/${ids.join(',')}`, method: 'delete' })
},
// 导出
export(params?: { ids?: string[]; id?: 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
return request({ url: `/biz/concessionApply/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/concessionApply/import`,
method: 'post',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
})
},
// 下载导入模板
downloadTemplate() {
return request({ url: `/biz/concessionApply/template`, method: 'get', responseType: 'blob' })
}
}

View File

@ -0,0 +1,427 @@
<template>
<div class="page-container">
<n-card>
<!-- 搜索表单 -->
<div class="search-form">
<n-form inline :model="searchForm" label-placement="left">
<n-form-item label="主键id">
<n-input v-model:value="searchForm.id" placeholder="请输入主键id" clearable />
</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="质检编号" path="qcId">
<n-input v-model:value="formData.qcId" placeholder="请输入质检编号" />
</n-form-item>
<n-form-item label="让步接收数量" path="concessionQty">
<n-input v-model:value="formData.concessionQty" placeholder="请输入让步接收数量" />
</n-form-item>
<n-form-item label="申请原因" path="applyRemark">
<n-input v-model:value="formData.applyRemark" type="textarea" placeholder="请输入申请原因" />
</n-form-item>
<n-form-item label="申请内容" path="applyContent">
<n-input v-model:value="formData.applyContent" type="textarea" placeholder="请输入申请内容" />
</n-form-item>
<n-form-item label="申请人id(发去人id)" path="applyUserId">
<n-input v-model:value="formData.applyUserId" placeholder="请输入申请人id(发去人id)" />
</n-form-item>
<n-form-item label="部门id" path="deptId">
<n-input v-model:value="formData.deptId" placeholder="请输入部门id" />
</n-form-item>
<n-form-item label="钉钉单据id" path="instanceId">
<n-input v-model:value="formData.instanceId" placeholder="请输入钉钉单据id" />
</n-form-item>
<n-form-item label="审批状态 1待审批 2审批通过 3 审批驳回" path="approveStatus">
<n-select v-model:value="formData.approveStatus" placeholder="请选择审批状态 1待审批 2审批通过 3 审批驳回" :options="approveStatusOptions" />
</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 { concessionApplyApi, type ConcessionApply } from '@/api/concessionApply'
import { dictDataApi } from '@/api/org'
const message = useMessage()
const dialog = useDialog()
//
const searchForm = reactive({
id: null as number | null,
})
//
const tableData = ref<ConcessionApply[]>([])
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: ConcessionApply = {
qcId: undefined,
concessionQty: undefined,
applyRemark: '',
applyContent: '',
applyFile: '',
applyUserId: undefined,
deptId: undefined,
instanceId: '',
approveStatus: undefined,
}
const formData = reactive<ConcessionApply>({ ...defaultFormData })
// //使
const approveStatusOptions = ref<{ label: string; value: any }[]>([])
//
const formRules = {
}
//
const columns: DataTableColumns<ConcessionApply> = [
{ type: 'selection' },
{ title: '主键id', key: 'id' },
{ title: '质检编号', key: 'qcId' },
{ title: '让步接收数量', key: 'concessionQty' },
{ title: '申请原因', key: 'applyRemark' },
{ title: '申请内容', key: 'applyContent' },
{ title: '申请附件', key: 'applyFile' },
{ title: '申请人id(发去人id)', key: 'applyUserId' },
{ title: '部门id', key: 'deptId' },
{ title: '钉钉单据id', key: 'instanceId' },
{ title: '审批状态 1待审批 2审批通过 3 审批驳回', key: 'approveStatus',
render(row) {
const val = row.approveStatus
const opt = approveStatusOptions.value.find(o => o.value === val || String(o.value) === String(val))
return opt ? opt.label : (val ?? '-')
}
},
{ title: '创建时间', key: 'createTime', width: 180 },
{ title: '更新时间', key: 'updateTime', width: 180 },
{
title: '操作',
key: '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 concessionApplyApi.page({
page: pagination.page,
pageSize: pagination.pageSize,
id: searchForm.id || undefined,
})
tableData.value = res.list
pagination.itemCount = res.total
} finally {
loading.value = false
}
}
//
function handleSearch() {
pagination.page = 1
loadData()
}
//
function handleReset() {
searchForm.id = 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: ConcessionApply) {
modalTitle.value = '编辑让步接收申请表'
Object.assign(formData, row)
if (formData.createTime && typeof formData.createTime === 'string') {
formData.createTime = new Date(formData.createTime.replace(' ', 'T')).getTime()
}
if (formData.updateTime && typeof formData.updateTime === 'string') {
formData.updateTime = new Date(formData.updateTime.replace(' ', 'T')).getTime()
}
modalVisible.value = true
}
//
async function handleSubmit() {
await formRef.value?.validate()
try {
const submitData = { ...formData } as ConcessionApply
if (typeof submitData.createTime === 'number') {
submitData.createTime = new Date(submitData.createTime).toISOString().slice(0, 19).replace('T', ' ')
}
if (typeof submitData.updateTime === 'number') {
submitData.updateTime = new Date(submitData.updateTime).toISOString().slice(0, 19).replace('T', ' ')
}
if (submitData.id) {
await concessionApplyApi.update(submitData)
message.success('修改成功')
} else {
await concessionApplyApi.create(submitData)
message.success('新增成功')
}
modalVisible.value = false
loadData()
} catch (error) {
//
}
}
//
function handleDelete(row: ConcessionApply) {
dialog.warning({
title: '提示',
content: '确定要删除该记录吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
await concessionApplyApi.delete([row.id!])
message.success('删除成功')
loadData()
} catch (error) {
//
}
}
})
}
//
function handleBatchDelete() {
dialog.warning({
title: '提示',
content: `确定要删除选中的 ${selectedIds.value.length} 条记录吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
await concessionApplyApi.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
const blob = await concessionApplyApi.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 concessionApplyApi.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 concessionApplyApi.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')
approveStatusOptions.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,98 @@
import { request } from '@/utils/request'
// 让步接收申请表 类型定义
export interface ConcessionApply {
id?: number
qcId?: number
concessionQty?: number
applyRemark?: string
applyContent?: string
applyFile?: string
applyUserId?: number
deptId?: number
instanceId?: string
approveStatus?: number
createTime?: string
updateTime?: string
}
export interface DetailModel {
businessId:string,
title:string,
deptName:string,
approveResult:string,
approveRecord:[]
}
// 让步接收申请表 API
export const concessionApplyApi = {
// 分页查询
page(params: { page: number; pageSize: number; id?: number }) {
return request({ url: '/biz/concessionApply/page', method: 'get', params })
},
// 获取详情
detail(id: string) {
return request({ url: `/biz/concessionApply/${id}`, method: 'get' })
},
// 新增
create(data: ConcessionApply) {
return request({ url: '/biz/concessionApply', method: 'post', data })
},
// 修改
update(data: ConcessionApply) {
return request({ url: '/biz/concessionApply', method: 'put', data })
},
// 删除
delete(ids: string[]) {
return request({ url: `/biz/concessionApply/${ids.join(',')}`, method: 'delete' })
},
// 导出
export(params?: { ids?: string[]; id?: 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
return request({ url: `/biz/concessionApply/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/concessionApply/import`,
method: 'post',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
})
},
// 下载导入模板
downloadTemplate() {
return request({ url: `/biz/concessionApply/template`, method: 'get', responseType: 'blob' })
},
//撤回审批申请
approveCancel(params?:{id?:number}){
return request({url:`/biz/concessionApply/approveCancel`,method:'get',params})
}
}

View File

@ -45,8 +45,55 @@ export interface OrderItem {
starter?: number starter?: number
orderCode?: string
routeCode?: string
proWorkshop?: string
} }
export interface KingdeePrdMo {
productionOrderNo: string
billNo: string
workShopName: string
materialCode: string
quantity: number | null
specModel: string | null
unit: string | null
materialName: string | null
routingNo: string | null
planStartTime: string | null
planFinishTime: string | null
pickMtrlStatus: string | null
children: KingdeeProcessRoute[]
}
/** 金蝶工艺路线工序(树子节点) */
export interface KingdeeProcessRoute {
/** 工序号 FOperNumber */
operNumber: number | null
/** 工作中心 FWorkCenterId.FName */
workCenterName: string | null
/** 生产车间 FDepartmentId.FName */
departmentName: string | null
/** 工序名称 FProcessProperty */
processName: string | null
/** 工序说明 FOperDescription */
operDescription: string | null
/** 活动单位 FActivity1UnitID.FName */
activityUnit: string | null
/** 工序控制码 FOptCtrlCodeId.FName */
optCtrlCodeName: string | null
/** 活动量 FActivity1Qty */
activityQty: number | null
/** 计划开始时间 yyyy-MM-dd HH:mm:ss */
planStartTime: string | null
/** 计划结束时间 yyyy-MM-dd HH:mm:ss */
planFinishTime: string | null
}
// 生产订单 API // 生产订单 API
export const orderItemApi = { export const orderItemApi = {
// 分页查询 // 分页查询
@ -81,6 +128,10 @@ export const orderItemApi = {
preCreation(data: any) { preCreation(data: any) {
return request({ url: '/biz/orderItem/preCreation', method: 'post', data }) return request({ url: '/biz/orderItem/preCreation', method: 'post', data })
}, },
//根据生产编号查询预生产订单
preCreationInfo(id: number, params:any) {
return request({ url: `/biz/orderItem/preCreation/${id}`, method: 'get', params})
},
//查询项目名称 //查询项目名称
xmoptions(params: any) { xmoptions(params: any) {
@ -92,12 +143,31 @@ export const orderItemApi = {
return request({ url: '/biz/orderItem/options', method: 'get', params }) return request({ url: '/biz/orderItem/options', method: 'get', params })
}, },
holdTemporarilySave(id: number, data: KingdeePrdMo[]){
return request({ url: `/biz/orderItem/kingdee/temporarily/save/${id}`, method: 'post', data })
},
// 同步订单和计划表
synchronizationSave(id: number, data: KingdeePrdMo[]) {
return request({ url: `/biz/orderItem/kingdee/order/create/${id}`, method: 'post', data })
},
// 导出 // 导出
export(params?: { ids?: string[]; code?: string; projectId?: number; beginTime?: string; endTime?: string }) { export(params?: {
ids?: string[]
code?: string
projectName?: string
orderCode?: string
routeCode?: string
proWorkshop?: string
beginTime?: string
endTime?: string
}) {
const p: Record<string, any> = {} const p: Record<string, any> = {}
if (params?.ids?.length) p.ids = params.ids.join(',') if (params?.ids?.length) p.ids = params.ids.join(',')
if (params?.code !== undefined && params?.code !== null) p.code = params.code if (params?.code !== undefined && params?.code !== null) p.code = params.code
if (params?.projectId !== undefined && params?.projectId !== null) p.projectId = params.projectId if (params?.projectName !== undefined && params?.projectName !== null) p.projectName = params.projectName
if (params?.orderCode !== undefined && params?.orderCode !== null) p.orderCode = params.orderCode
if (params?.routeCode !== undefined && params?.routeCode !== null) p.routeCode = params.routeCode
if (params?.proWorkshop !== undefined && params?.proWorkshop !== null) p.proWorkshop = params.proWorkshop
if (params?.beginTime !== undefined && params?.beginTime !== null) p.beginTime = params.beginTime if (params?.beginTime !== undefined && params?.beginTime !== null) p.beginTime = params.beginTime
if (params?.endTime !== undefined && params?.endTime !== null) p.endTime = params.endTime if (params?.endTime !== undefined && params?.endTime !== null) p.endTime = params.endTime
return request({ url: `/biz/orderItem/export`, method: 'get', params: p, responseType: 'blob' }) return request({ url: `/biz/orderItem/export`, method: 'get', params: p, responseType: 'blob' })

View File

@ -69,18 +69,57 @@ export interface KingdeePrdMo {
/** 金蝶工艺路线工序(树子节点) */ /** 金蝶工艺路线工序(树子节点) */
export interface KingdeeProcessRoute { export interface KingdeeProcessRoute {
/** 工序号 FOperNumber */
operNumber: number | null operNumber: number | null
/** 工作中心 FWorkCenterId.FName */
workCenterName: string | null workCenterName: string | null
/** 生产车间 FDepartmentId.FName */
departmentName: string | null departmentName: string | null
processProperty: string | null /** 工序名称 FProcessProperty */
processName: string | null
/** 工序说明 FOperDescription */
operDescription: string | null operDescription: string | null
/** 活动单位 FActivity1UnitID.FName */
activityUnit: string | null activityUnit: string | null
/** 工序控制码 FOptCtrlCodeId.FName */
optCtrlCodeName: string | null optCtrlCodeName: string | null
/** 活动量 FActivity1Qty */
activityQty: number | null activityQty: number | null
/** 计划开始时间 yyyy-MM-dd HH:mm:ss */
planStartTime: string | null planStartTime: string | null
/** 计划结束时间 yyyy-MM-dd HH:mm:ss */
planFinishTime: string | null planFinishTime: string | null
} }
/** 解析工序名称,兼容旧字段及后端误映射 */
export function resolveProcessName(route: Partial<KingdeeProcessRoute> & { processProperty?: string | null }) {
const legacyName = typeof route.processProperty === 'string' ? route.processProperty.trim() : ''
const processName = route.processName?.trim() || legacyName || ''
const operDescription = route.operDescription?.trim() || ''
if (processName) return processName
// 兼容FProcessProperty 被写入 operDescription 而 processName 为空
if (operDescription) return operDescription
return ''
}
/** 归一化金蝶工序字段,保证 processName / operDescription 各归其位 */
export function normalizeKingdeeProcessRoute(
route: KingdeeProcessRoute & { processProperty?: string | null }
): KingdeeProcessRoute {
const legacyName = typeof route.processProperty === 'string' ? route.processProperty.trim() : ''
const processName = route.processName?.trim() || legacyName || ''
const operDescription = route.operDescription?.trim() || ''
if (processName) {
return { ...route, processName, operDescription: operDescription || null }
}
if (operDescription) {
return { ...route, processName: operDescription, operDescription: null }
}
return { ...route, processName: null, operDescription: null }
}
// 项目表 API // 项目表 API
export const orderProjectApi = { export const orderProjectApi = {
// 分页查询 // 分页查询
@ -136,8 +175,8 @@ export const orderProjectApi = {
}, },
// 查询金蝶生产订单 (支持优先读取本地暂存) // 查询金蝶生产订单 (支持优先读取本地暂存)
getKingdeeOrder(id: number) { getKingdeeOrder(id: number, params:any) {
return request<KingdeePrdMo[]>({ url: `/biz/orderProject/kingdee/order/${id}`, method: 'get' }) return request<KingdeePrdMo[]>({ url: `/biz/orderProject/kingdee/order/${id}`, method: 'get', params })
}, },
// 暂存金蝶生产订单数据(草稿) // 暂存金蝶生产订单数据(草稿)

View File

@ -59,7 +59,7 @@ export function workload(params?:any) {
export function inspectio(data:any) { export function inspectio(data:any) {
return request({ return request({
url: `/mes/dispatch/qc`, url: `/mes/dispatch/qc`,
method: 'put', method: 'post',
data data
}) })
} }

View File

@ -24,6 +24,12 @@ export interface QcResultDetail {
updateTime?: string updateTime?: string
files?:any[],
title?:string,
images?:any[],
attachments?:any[]
} }
// 质检结果明细表 API // 质检结果明细表 API

View File

@ -68,6 +68,10 @@ export const qualityTestingApi = {
submit(data: QcResultDetail[]) { submit(data: QcResultDetail[]) {
return request({ url: '/mes/qc/judge', method: 'put', data }) return request({ url: '/mes/qc/judge', method: 'put', data })
}, },
submitBF(data: QcResultDetail[]) {
return request({ url: '/mes/qc/partial', method: 'post', data })
},
//质检撤回 //质检撤回
cancel(id: number) { cancel(id: number) {
return request({ url: `/mes/dispatch/revoke/${id}`, method: 'put' }) return request({ url: `/mes/dispatch/revoke/${id}`, method: 'put' })

View File

@ -71,5 +71,11 @@ export const submitLogApi = {
//汇报统计 //汇报统计
statistics(params?: {time?:string; type?:string}){ statistics(params?: {time?:string; type?:string}){
return request({url:'/mes/report/statistics',method:'get',params}) return request({url:'/mes/report/statistics',method:'get',params})
},
//按派工单id查询汇报
getByDispatch(dispatchId:number){
return request({url:`/mes/report/bydispatch/${dispatchId}`,method:"get"})
} }
} }

View File

@ -126,6 +126,14 @@ export const userApi = {
method: 'get', method: 'get',
responseType: 'blob' responseType: 'blob'
}) })
},
//查询全部用户
getPathList() {
return request({
url:"sys/user/list",
method:'get'
})
} }
} }

View File

@ -1,4 +1,5 @@
import { request } from '@/utils/request' import { request } from '@/utils/request'
import { F } from 'vue-router/dist/router-CWoNjPRp.mjs'
// 分页结果 // 分页结果
export interface PageResult<T> { export interface PageResult<T> {
@ -371,6 +372,34 @@ export interface SysFile {
remark?: string remark?: string
} }
export interface DingTalkImage{
id?:number
mainId?:number
fileName?:string
fileSuffix?:string
fileSize?:number
localFilePath?:string
fileUrl?:string
sortNum?:number
createTime?:string
}
export interface DingTalkAttachment{
id?:number
mainId?:number
fileName?:string
fileSuffix?:string
fileSize?:number
localFilePath?:string
ddSpaceId?:string
ddFileId?:string
uploadDdStatus?:string
sortNum?:number
createTime?:string
}
export const fileApi = { export const fileApi = {
page(params: { page: number; pageSize: number; originalName?: string; fileType?: string }): Promise<PageResult<SysFile>> { page(params: { page: number; pageSize: number; originalName?: string; fileType?: string }): Promise<PageResult<SysFile>> {
return request({ url: '/sys/file/page', method: 'get', params }) return request({ url: '/sys/file/page', method: 'get', params })
@ -402,8 +431,53 @@ export const fileApi = {
return request({ return request({
url: '/sys/file/upload', url: '/sys/file/upload',
method: 'post', method: 'post',
data: formData, data: formData
headers: { 'Content-Type': 'multipart/form-data' } // headers: { 'Content-Type': 'multipart/form-data' }
})
},
uploadDingImage(file: File):Promise<DingTalkImage> {
const formData = new FormData()
formData.append("file",file)
return request({
url:"/sys/file/uploadDingImage",
method:'post',
data:formData
})
},
uploadDingAttachment(file: File):Promise<DingTalkAttachment> {
const formData = new FormData()
formData.append("file",file)
return request({
url:"/sys/file/uploadDingAttachment",
method:"post",
data:formData
})
},
uploadAndDingding(file: File, path?: string, groupId?: number | null): Promise<SysFile>{
const formData = new FormData()
formData.append('file', file)
if (path) {
formData.append('path', path)
}
if (groupId !== undefined && groupId !== null && groupId > 0) {
formData.append('groupId', groupId.toString())
}
return request({
url: '/sys/file/uploadAndDingding',
method: 'post',
data: formData
})
},
getApprovalSpaceId() {
return request({
url:'sys/file/getApprovalSpaceId',
method:'get',
}) })
}, },
@ -418,6 +492,8 @@ export const fileApi = {
}) })
}, },
getDownloadUrl(id: number): string { getDownloadUrl(id: number): string {
return `/api/sys/file/download/${id}` return `/api/sys/file/download/${id}`
}, },

View File

@ -0,0 +1,36 @@
<!--
站点 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'
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',
})
const { siteLogo, showSiteLogoImg, fallbackLetter, handleLogoError } = useSiteLogo(
() => props.src
)
</script>

View File

@ -0,0 +1,30 @@
import { computed, ref, watch } from 'vue'
import { useSiteStore } from '@/stores/site'
/**
* Logo退
*/
export function useSiteLogo(src?: () => string | undefined) {
const siteStore = useSiteStore()
const siteName = computed(() => siteStore.siteName || 'MES系统')
const siteLogo = computed(() => src?.() ?? siteStore.siteLogo)
const logoLoadFailed = ref(false)
const showSiteLogoImg = computed(() => !!siteLogo.value && !logoLoadFailed.value)
const fallbackLetter = computed(() => siteName.value.charAt(0) || 'M')
watch(siteLogo, () => {
logoLoadFailed.value = false
})
function handleLogoError() {
logoLoadFailed.value = true
}
return {
siteName,
siteLogo,
showSiteLogoImg,
fallbackLetter,
handleLogoError,
}
}

View File

@ -17,8 +17,11 @@
> >
<!-- Logo --> <!-- Logo -->
<div class="logo" :class="{ 'logo-collapsed': collapsed, 'logo-primary': themeStore.headerUsePrimaryColor }" :style="themeStore.headerUsePrimaryColor ? { background: themeStore.primaryColor, borderBottomColor: themeStore.primaryColor } : {}"> <div class="logo" :class="{ 'logo-collapsed': collapsed, 'logo-primary': themeStore.headerUsePrimaryColor }" :style="themeStore.headerUsePrimaryColor ? { background: themeStore.primaryColor, borderBottomColor: themeStore.primaryColor } : {}">
<img v-if="siteLogo" :src="siteLogo" class="logo-img" alt="Logo" /> <SiteLogoMark
<div v-else class="logo-icon" :style="{ background: themeStore.headerUsePrimaryColor ? '#fff' : themeStore.primaryColor, color: themeStore.headerUsePrimaryColor ? themeStore.primaryColor : '#fff' }">{{ siteName.charAt(0) }}</div> img-class="logo-img"
icon-class="logo-icon"
:icon-style="logoIconStyle"
/>
<transition name="fade"> <transition name="fade">
<span v-if="!collapsed" class="logo-text">{{ siteName }}</span> <span v-if="!collapsed" class="logo-text">{{ siteName }}</span>
<!-- <span v-if="!collapsed" class="logo-text">伊特智造MES</span> --> <!-- <span v-if="!collapsed" class="logo-text">伊特智造MES</span> -->
@ -43,8 +46,11 @@
<n-layout-header bordered class="layout-header" :class="[`theme-${layoutConfig.theme}`, { 'header-primary': themeStore.headerUsePrimaryColor }]" :style="headerStyle"> <n-layout-header bordered class="layout-header" :class="[`theme-${layoutConfig.theme}`, { 'header-primary': themeStore.headerUsePrimaryColor }]" :style="headerStyle">
<!-- 顶部菜单模式下的Logo --> <!-- 顶部菜单模式下的Logo -->
<div v-if="layoutConfig.siderPosition === 'top'" class="header-logo"> <div v-if="layoutConfig.siderPosition === 'top'" class="header-logo">
<img v-if="siteLogo" :src="siteLogo" class="logo-img" alt="Logo" /> <SiteLogoMark
<div v-else class="logo-icon" :style="{ background: themeStore.headerUsePrimaryColor ? '#fff' : themeStore.primaryColor, color: themeStore.headerUsePrimaryColor ? themeStore.primaryColor : '#fff' }">{{ siteName.charAt(0) }}</div> img-class="logo-img"
icon-class="logo-icon"
:icon-style="logoIconStyle"
/>
<span class="logo-text">{{ siteName }}</span> <span class="logo-text">{{ siteName }}</span>
</div> </div>
@ -373,6 +379,7 @@ import ProfileModal from '@/components/ProfileModal.vue'
import PasswordModal from '@/components/PasswordModal.vue' import PasswordModal from '@/components/PasswordModal.vue'
import MessageNotification from '@/components/MessageNotification.vue' import MessageNotification from '@/components/MessageNotification.vue'
import TabBar from '@/components/TabBar.vue' import TabBar from '@/components/TabBar.vue'
import SiteLogoMark from '@/components/SiteLogoMark.vue'
import { noticeApi, chatApi, type SysNotice, type ChatMessage } from '@/api/message' import { noticeApi, chatApi, type SysNotice, type ChatMessage } from '@/api/message'
import { iconMap as externalIconMap } from '@/utils/icons' import { iconMap as externalIconMap } from '@/utils/icons'
@ -387,7 +394,10 @@ const themeStore = useThemeStore()
// //
const siteName = computed(() => siteStore.siteName || 'CSY Admin') const siteName = computed(() => siteStore.siteName || 'CSY Admin')
const siteLogo = computed(() => siteStore.siteLogo) const logoIconStyle = computed(() => ({
background: themeStore.headerUsePrimaryColor ? '#fff' : themeStore.primaryColor,
color: themeStore.headerUsePrimaryColor ? themeStore.primaryColor : '#fff',
}))
// message // message
window.$message = message window.$message = message

View File

@ -154,6 +154,18 @@ const routes: RouteRecordRaw[] = [
component: () => import('@/views/biz/order/index.vue'), component: () => import('@/views/biz/order/index.vue'),
meta: { title: '订单管理', icon: 'ListOutline' } meta: { title: '订单管理', icon: 'ListOutline' }
}, },
{
path: 'biz/orderProject',
name: 'orderProject',
component: () => import('@/views/biz/orderProject/index.vue'),
meta: { title: '项目管理', icon: 'ListOutline' }
},
{
path: 'biz/orderProject/gantt',
name: 'OrderProjectGantt',
component: () => import('@/views/biz/orderProject/components/GanttSchedule.vue'),
meta: { title: '甘特图排产', icon: 'CalendarOutline', activeMenu: '/biz/orderProject' }
},
{ {
path: 'biz/code', path: 'biz/code',
name: 'code', name: 'code',

View File

@ -7,7 +7,7 @@ import { configGroupApi } from '@/api/org'
*/ */
export const useSiteStore = defineStore('site', () => { export const useSiteStore = defineStore('site', () => {
// 站点名称 // 站点名称
const siteName = ref('CSY Admin') const siteName = ref('MES系统')
// 站点描述 // 站点描述
const siteDescription = ref('伊特智造MES') const siteDescription = ref('伊特智造MES')
// 站点 Logo // 站点 Logo
@ -33,7 +33,7 @@ export const useSiteStore = defineStore('site', () => {
try { try {
const config = await configGroupApi.getPublicConfig() const config = await configGroupApi.getPublicConfig()
if (config.system) { if (config.system) {
siteName.value = config.system.siteName || 'CSY Admin' siteName.value = config.system.siteName || 'MES系统'
siteDescription.value = config.system.siteDescription || '伊特智造MES' siteDescription.value = config.system.siteDescription || '伊特智造MES'
siteLogo.value = config.system.siteLogo || '' siteLogo.value = config.system.siteLogo || ''
copyright.value = config.system.copyright || '' copyright.value = config.system.copyright || ''

View File

@ -125,6 +125,12 @@ service.interceptors.request.use(
if (userStore.token) { if (userStore.token) {
config.headers['Authorization'] = userStore.token config.headers['Authorization'] = userStore.token
} }
if (config.data instanceof FormData) {
// 文件上传:不设置 JSON 请求头、不序列化,交给浏览器自动处理 multipart
return config
}
config.headers["Content-Type"] = 'application/json;charset=utf-8' config.headers["Content-Type"] = 'application/json;charset=utf-8'
config.data = config.data instanceof Object ? JSON.stringify(config.data) : config.data config.data = config.data instanceof Object ? JSON.stringify(config.data) : config.data
return config return config

View File

@ -0,0 +1,904 @@
<template>
<div class="page-container">
<n-card>
<!-- 搜索表单 -->
<div class="search-form">
<n-form inline :model="searchForm" label-placement="left">
<n-form-item label="主键id">
<n-input v-model:value="searchForm.id" placeholder="请输入主键id" clearable />
</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"
size="small"
remote
:border="false"
:single-line="false"
striped
/>
<div class="pagination-container" style="display: flex; justify-content: flex-end; margin-top: 12px">
<n-pagination
v-model:page="pagination.page"
v-model:page-size="pagination.pageSize"
:item-count="pagination.itemCount"
:page-sizes="[10, 20, 50, 100]"
show-size-picker
show-quick-jumper
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
>
<template #prefix>
{{ pagination.itemCount }}
</template>
</n-pagination>
</div>
</n-card>
<!-- 新增/编辑弹窗 -->
<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="质检编号" path="qcId">
<n-input v-model:value="formData.qcId" placeholder="请输入质检编号" />
</n-form-item>
<n-form-item label="让步接收数量" path="concessionQty">
<n-input v-model:value="formData.concessionQty" placeholder="请输入让步接收数量" />
</n-form-item>
<n-form-item label="申请原因" path="applyRemark">
<n-input v-model:value="formData.applyRemark" type="textarea" placeholder="请输入申请原因" />
</n-form-item>
<n-form-item label="申请内容" path="applyContent">
<n-input v-model:value="formData.applyContent" type="textarea" placeholder="请输入申请内容" />
</n-form-item>
<n-form-item label="申请人id(发去人id)" path="applyUserId">
<n-input v-model:value="formData.applyUserId" placeholder="请输入申请人id(发去人id)" />
</n-form-item>
<n-form-item label="部门id" path="deptId">
<n-input v-model:value="formData.deptId" placeholder="请输入部门id" />
</n-form-item>
<n-form-item label="钉钉单据id" path="instanceId">
<n-input v-model:value="formData.instanceId" placeholder="请输入钉钉单据id" />
</n-form-item>
<n-form-item label="审批状态 1待审批 2审批通过 3 审批驳回" path="approveStatus">
<n-select v-model:value="formData.approveStatus" placeholder="请选择审批状态 1待审批 2审批通过 3 审批驳回" :options="approveStatusOptions" />
</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>
<!-- 审批记录弹窗 -->
<n-modal v-model:show="recordModalVisible" ref="exportWrapRef" preset="card" title="审批记录" style="width: 800px">
<n-card bordered style="width:100%;max-width:800px;padding:24px;position:relative;">
<div class="header-bar" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<span style="font-size:20sp;color:#666">审批编号: {{ detailModel?.businessId }}</span>
<div>
<n-icon style="font-size:22px;margin:0 6px;cursor:pointer" @click="downloadOutline()" ><DownloadOutline /></n-icon>
</div>
</div>
<div style="padding:10px 0;">
<div style="font-size:30px;font-weight:500;margin-bottom:12px;">{{ detailModel?.title }}</div>
<div style="font-size:36sp;color:#777;margin-bottom:12px;">{{ detailModel?.deptName }}</div>
<div :style="{fontSize:'30sp', color:getApproveResultColor(detailModel?.approveResult)}">{{ detailModel?.approveResult }}</div>
</div>
<div class="seal-wrap" style="position:absolute;right:40px;top:50%;transform:translateY(-50%);">
<div v-if="detailModel?.approveResult==='已通过'" class="seal" :class="{'pass-seal':detailModel?.approveResult==='已通过'}">已通过</div>
<div v-if="detailModel?.approveResult==='已驳回'" class="seal" :class="{'pass-seal_reject':detailModel?.approveResult==='已驳回'}">已驳回</div>
<div v-if="detailModel?.approveResult==='已撤销'" class="seal" :class="{'pass-seal_cancel':detailModel?.approveResult==='已撤销'}">已撤销</div>
</div>
</n-card>
<n-card bordered style="width:100%;max-width:800px;padding:24px;margin-top: 10px;position:relative;">
<div style="padding:10px;">
<!-- 循环审批记录 -->
<div
v-for="(item,index) in detailModel?.approveRecord"
:key="index"
style="display:flex;gap:12px;position:relative;padding-bottom:24px;"
>
<!-- 左侧圆点 + 竖连接线 -->
<div style="display:flex;flex-direction:column;align-items:center;">
<!-- 圆点图标 -->
<div style="width:20px;height:20px;border-radius:50%;background:#36d399;display:flex;align-items:center;justify-content:center;">
<!-- 可替换CashIcon图标 -->
<span style="color:#fff;font-size:12px;">$</span>
</div>
<!-- 中间竖线最后一条去掉竖线 -->
<div v-if="index !== detailModel.approveRecord.length -1" style="width:2px;flex:1;background:#d0d5dd;margin-top:4px;"></div>
</div>
<!-- 右侧标题 + 时间 + 备注 -->
<div style="flex:1;">
<!-- 用户名+操作结果 -->
<div style="font-size:18px;margin-bottom:4px;">
{{ item.username ?? '未知人员' }}
<span :style="{
color: item.operationResult === '已同意' ? '#00b42a' : item.operationResult === '已拒绝' ? '#f53f3f' : item.operationResult === '已撤销' ? '#ff7d00' : '#666',
marginLeft:'8px'
}">
{{ item.operationResult }}
</span>
</div>
<!-- 时间 -->
<div style="color:#888;font-size:14px;margin-bottom:6px;">{{ item.date }}</div>
<!-- 审批备注 -->
<div v-if="item.remark" style="padding:6px 10px;background:#f7f8fa;border-radius:6px;color:#333;">
{{ item.remark }}
</div>
<div v-if="item.attachments && item.attachments.length" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px;">
<!-- 图片类附件png/jpg/jpeg/gif -->
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px;">
<div
v-for="file in item.attachments.filter(f=>['png','jpg','jpeg','gif'].includes(f.file_type.toLowerCase()))"
:key="file.file_id"
@click="previewImg(file)"
style="width:80px;height:80px;border-radius:6px;overflow:hidden;border:1px solid #eee;cursor:pointer;"
>
</div>
</div>
</div>
</div>
</div>
</div>
</n-card>
</n-modal>
<n-image-group
v-model:show="showRef"
v-model:current="currentIndex"
:src-list="previewSrcList"
/>
<n-modal v-model:show="fileModalShow" title="附件列表" style="width: 400px;">
<n-space vertical :size="12">
<div v-for="(item, idx) in fileList" :key="idx" style="display:flex;align-items:center;justify-content:space-between;">
<span>{{ item.fileName }}</span>
<n-button
size="small"
type="primary"
@click="window.open(item, '_blank')"
>
下载
</n-button>
</div>
</n-space>
</n-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, h, onMounted } from 'vue'
import { NButton, NSpace, NIcon, NUpload, useMessage,NTag, useDialog, type DataTableColumns, type UploadCustomRequestOptions, NDropdown, NImage } from 'naive-ui'
import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline,DocumentTextOutline,
CashOutline as CashIcon,
EllipsisHorizontalCircle
} from '@vicons/ionicons5'
import { concessionApplyApi, DetailModel, type ConcessionApply } from '@/api/concessionApply'
import { dictDataApi } from '@/api/org'
import html2canvas from 'html2canvas'
import { jsPDF } from 'jspdf'
import * as dd from 'dingtalk-jsapi';
const message = useMessage()
const dialog = useDialog()
//
const searchForm = reactive({
id: null as number | null,
})
//
const tableData = ref<ConcessionApply[]>([])
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: ConcessionApply = {
qcId: undefined,
concessionQty: undefined,
applyRemark: '',
applyContent: '',
applyFile: '',
applyUserId: undefined,
deptId: undefined,
instanceId: '',
approveStatus: undefined,
}
const formData = reactive<ConcessionApply>({ ...defaultFormData })
//
const recordModalVisible = ref(false)
const detailModel = ref<DetailModel>()
// //使
const approveStatusOptions = ref<{ label: string; value: any }[]>([])
//
const formRules = {
}
const showRef = ref(false)
const currentIndex = ref(0)
const previewSrcList = ref([])
const fileModalShow = ref(false)
const fileList = ref([])
//
const columns: DataTableColumns<ConcessionApply> = [
{ type: 'selection' },
{ title: '物料编号', key: 'materialCode',align:"center",minWidth:"150"},
{ title: '物料名称', key: 'materialName',align:"center",minWidth:"150"},
{ title: '让步接收数量', key: 'concessionQty',align:"center",minWidth:"150" },
{ title: '申请原因', key: 'applyRemark',align:"center",minWidth:"150" },
{ title: '申请内容', key: 'applyContent',align:"center",minWidth:"200" },
{
title: '申请图片',
key: 'applyImages',
align: "center",
minWidth: "100",
render(row) {
//
const imageUrls = row.applyImages?.split(",").filter(url => url.trim() !== "") || [];
if (imageUrls.length === 0) return "-";
// 访URL
const fullUrls = imageUrls.map(path => {
if (path.startsWith("http")) return path;
return `${window.location.origin}${path}`;
});
// 3. + +n
return h('div', {
style: 'display: flex; align-items: center; justify-content: center; gap: 6px;'
}, [
//
h('img', {
src: fullUrls[0],
alt: `申请图片`,
style: 'width: 60px; height: 60px; object-fit: cover; border-radius: 4px; cursor: pointer;',
onClick: () => {
//
previewSrcList.value = fullUrls;
currentIndex.value = 0;
showRef.value = true;
}
}),
//
fullUrls.length > 1 ? h('span', {
style: 'font-size: 12px; color: #666; background: #f0f0f0; padding: 2px 6px; border-radius: 10px;'
}, `+${fullUrls.length - 1}`) : null
]);
}
},
{
title: '附件',
key: 'applyAttachments',
align: "center",
minWidth: "180",
render(row) {
const files = row.applyAttachments?.split(",").filter(url => url.trim() !== "") || [];
if (files.length === 0) return "-"
return h('div', {style: 'display:flex;flex-direction:column;gap:4px;'},
files.map((item, idx) =>
h('div', {key: idx, style: 'display:flex;align-items:center;gap:6px;'}, [
h('span', {style: 'font-size:12px;'}, item.fileName),
h('a', {
href: item,
target: '_blank',
style: 'color:#409eff;font-size:12px;cursor:pointer;'
}, '查看')
])
)
)
}
},
{ title: '申请人', key: 'applyUserName',align:"center",minWidth:"150" },
{ title: '部门名称', key: 'deptName',align:"center",minWidth:"150" },
// { title: '', key: 'instanceId',align:"center",minWidth:"150" },
{ 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);
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 })
return row.approveResult
}
},
{ title: '审批人', key: 'approveUserName',align:"center",minWidth:"150" },
{ title: '创建时间', key: 'createTime', width: 180 },
{ title: '更新时间', key: 'updateTime', width: 180 },
{
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) }), ' 删除']
}),
h(NDropdown,{
trigger: 'click',
options:[
{label:'审批记录',key:'approveRecord'},
{type:'divider',show: row.approveStatus === '待审批'},
{label:"撤回审批",key:'approveCancel',show: row.approveStatus === '待审批'}
],
onSelect:(key:string)=>{
switch(key) {
case 'approveRecord':
handleViewApproveRecord(row)
break;
case 'approveCancel':
handleCancel(row)
break
}
}
},{
default:()=>h(NButton,{size:'small',quaternary:true},{
default:()=>[h(NIcon,null,{default:()=>h(EllipsisHorizontalCircle)})]
})
}
)
])
}
}
]
//
async function loadData() {
loading.value = true
try {
const res = await concessionApplyApi.page({
page: pagination.page,
pageSize: pagination.pageSize,
id: searchForm.id || undefined,
})
tableData.value = res.list
pagination.itemCount = res.total
} finally {
loading.value = false
}
}
//
function handleSearch() {
pagination.page = 1
loadData()
}
//
function handleReset() {
searchForm.id = 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: ConcessionApply) {
modalTitle.value = '编辑让步接收申请表'
Object.assign(formData, row)
if (formData.createTime && typeof formData.createTime === 'string') {
formData.createTime = new Date(formData.createTime.replace(' ', 'T')).getTime()
}
if (formData.updateTime && typeof formData.updateTime === 'string') {
formData.updateTime = new Date(formData.updateTime.replace(' ', 'T')).getTime()
}
modalVisible.value = true
}
//
async function handleSubmit() {
await formRef.value?.validate()
try {
const submitData = { ...formData } as ConcessionApply
if (typeof submitData.createTime === 'number') {
submitData.createTime = new Date(submitData.createTime).toISOString().slice(0, 19).replace('T', ' ')
}
if (typeof submitData.updateTime === 'number') {
submitData.updateTime = new Date(submitData.updateTime).toISOString().slice(0, 19).replace('T', ' ')
}
if (submitData.id) {
await concessionApplyApi.update(submitData)
message.success('修改成功')
} else {
await concessionApplyApi.create(submitData)
message.success('新增成功')
}
modalVisible.value = false
loadData()
} catch (error) {
//
}
}
//
function handleDelete(row: ConcessionApply) {
dialog.warning({
title: '提示',
content: '确定要删除该记录吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
await concessionApplyApi.delete([row.id!])
message.success('删除成功')
loadData()
} catch (error) {
//
}
}
})
}
//
function handleViewApproveRecord(row:any) {
recordModalVisible.value = true
detailModel.value = {...row}
console.log(detailModel.value);
}
//
function handleBatchDelete() {
dialog.warning({
title: '提示',
content: `确定要删除选中的 ${selectedIds.value.length} 条记录吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
await concessionApplyApi.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
const blob = await concessionApplyApi.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 concessionApplyApi.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 concessionApplyApi.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('assets_state')
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) {
switch(status){
case '已通过':
return "green"
case '已驳回':
return "red"
case '已撤销':
return "gray"
}
}
const getRemark=(item) =>{
console.log(item.remark);
}
async function downloadOutline() {
const model = detailModel.value
if (!model) {
alert("暂无审批数据")
return
}
// 1. HTML
let recordHtml = ""
const recordList = model.approveRecord || []
recordList.forEach((item, index) => {
const remark = item.remark ?? ""
//
if (index === 0) {
recordHtml += `
<tr>
<td style="background: #f5f5f5;" rowspan="${recordList.length}">审批人</td>
<td colspan="3">
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<div>
${remark ? remark + "<br>" : ""}
${item.username ?? "未知人员"} ${item.operationResult ?? ""}
</div>
<span style="color: #888; white-space: nowrap;">${item.date ?? ""}</span>
</div>
</td>
</tr>
`
} else {
// rowspan
recordHtml += `
<tr>
<td colspan="3">
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<div>
${remark ? remark + "<br>" : ""}
${item.username ?? "未知人员"} ${item.operationResult ?? ""}
</div>
<span style="color: #888; white-space: nowrap;">${item.date ?? ""}</span>
</div>
</td>
</tr>
`
}
})
// 2.
let sealText = ""
let sealBorder = "#00b42a"
let sealColor = "#00b42a"
if (model.approveResult === "已通过") {
sealText = "已通过"
} else if (model.approveResult === "已驳回") {
sealText = "已驳回"
sealBorder = "#f53f3f"
sealColor = "#f53f3f"
} else if (model.approveResult === "已撤销") {
sealText = "已撤销"
sealBorder = "#ff7d00"
sealColor = "#ff7d00"
}
const createUser = recordList[0]?.username ?? ""
const nowTime = new Date().toLocaleString()
// 3. HTML
const html = `
<div style="width: 750px; margin: 0 auto; font-family: SimSun, serif; position: relative; background: #fff;">
<div style="text-align: center; font-size: 22px; font-weight: bold; margin-bottom: 20px;">通用审批</div>
<div style="display: flex; justify-content: space-between; font-size: 14px; margin-bottom: 20px; align-items: center;">
<span>${model.title ?? ""}</span>
<span>创建时间${model.createTime ?? ""}</span>
</div>
<table border="1" cellpadding="8" cellspacing="0" style="width: 100%; border-collapse: collapse; font-size: 14px;">
<tr>
<td style="width: 120px; background: #f5f5f5;">审批编号</td>
<td colspan="3">${model.businessId ?? ""}</td>
</tr>
<tr>
<td style="background: #f5f5f5;">创建人</td>
<td>${createUser}</td>
<td style="width: 120px; background: #f5f5f5;">创建人部门</td>
<td>${model.deptName ?? ""}</td>
</tr>
<tr>
<td style="background: #f5f5f5;">申请内容</td>
<td colspan="3">${model.content ?? ""}</td>
</tr>
<tr>
<td style="background: #f5f5f5;">审批详情</td>
<td colspan="3">${model.approveResult ?? ""}</td>
</tr>
<tr>
<td style="background: #f5f5f5;">附件</td>
<td colspan="3"></td>
</tr>
${recordHtml}
</table>
<div style="display: flex; justify-content: space-between; font-size: 12px; margin-top: 20px;">
<span>打印时间${nowTime}</span>
<span>打印人${createUser}</span>
</div>
${sealText ? `
<div style="position: absolute; top: 100px; right: 20px; width: 100px; height: 100px; border: 3px solid ${sealBorder}; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: ${sealColor}; font-size: 20px; font-weight: bold; transform: rotate(15deg);">
${sealText}
</div>
` : ""}
</div>
`
// ========== PDF ==========
const tempDiv = document.createElement('div')
tempDiv.innerHTML = html
tempDiv.style.position = 'absolute'
tempDiv.style.left = '-9999px'
tempDiv.style.top = '0'
document.body.appendChild(tempDiv)
try {
const canvas = await html2canvas(tempDiv, {
scale: 2,
useCORS: true,
logging: false,
backgroundColor: '#ffffff',
allowTaint: true
})
const imgData = canvas.toDataURL('image/jpeg', 1.0)
const pdf = new jsPDF('p', 'mm', 'a4')
const a4W = 210
const imgH = (canvas.height * a4W) / canvas.width
pdf.addImage(imgData, 'JPEG', 0, 0, a4W, imgH)
pdf.save(`通用审批_${model.businessId || "审批记录"}.pdf`)
} catch (err) {
console.error("导出失败:", err)
alert("导出PDF失败")
} finally {
document.body.removeChild(tempDiv)
}
}
async function handleCancel(row) {
dialog.warning({
title: '提示',
content: `确定要撤回ID:${row.id}审批申请吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try{
await concessionApplyApi.approveCancel({id:row.id});
message.success('撤回成功')
loadData()
}catch(error){}
}
})
}
onMounted(() => {
loadData()
loadDictOptions()
})
</script>
<style scoped>
.search-form {
margin-bottom: 16px;
}
.table-toolbar {
margin-bottom: 16px;
}
.pass-seal{
width:150px;
height:150px;
border:4px solid #36c972;
border-radius:50%;
color:#36c972;
font-size:40px;
font-weight:bold;
display:flex;
align-items:center;
justify-content:center;
transform:rotate(25deg);
}
.pass-seal_reject{
width:150px;
height:150px;
border:4px solid #da0d0d;
border-radius:50%;
color: #da0d0d;
font-size:40px;
font-weight:bold;
display:flex;
align-items:center;
justify-content:center;
transform:rotate(25deg);
}
.pass-seal_cancel{
width:150px;
height:150px;
border:4px solid #3a3a3a;
border-radius:50%;
color:#3a3a3a;
font-size:40px;
font-weight:bold;
display:flex;
align-items:center;
justify-content:center;
transform:rotate(25deg);
}
</style>

View File

@ -0,0 +1,462 @@
<template>
<div class="kingdee-toolbar">
<n-space>
<n-button
size="small"
:disabled="kingdeeLoading || !kingdeeTableData.length"
@click="kingdeeToggleExpandAll"
>
{{ kingdeeAllExpanded ? '全部收起' : '全部展开' }}
</n-button>
<n-button
size="small"
type="primary"
:disabled="kingdeeLoading || !kingdeeTableData.length"
@click="handleGanttSchedule"
>
<template #icon><n-icon><CalendarOutline /></n-icon></template>
甘特图排产
</n-button>
</n-space>
</div>
<n-data-table
v-model:expanded-row-keys="kingdeeExpandedKeys"
:columns="kingdeeMoColumns"
:data="kingdeeTableData"
:loading="kingdeeLoading"
:row-key="kingdeeMoRowKey"
:scroll-x="kingdeeMoScrollX"
:max-height="kingdeeTableMaxHeight"
:row-class-name="kingdeeMoRowClassName"
size="small"
style="margin-bottom:15px"
/>
<div style="margin-bottom: 1px;">
<!-- <template #footer> -->
<n-space justify="end">
<n-button type="error" :disabled="kingdeeLoading" @click="reCrawlReorderForm">
重新读取数据
</n-button>
<n-button type="primary" :disabled="kingdeeLoading" @click="handleSaveDraft">
保存草稿
</n-button>
<n-button type="info" :disabled="kingdeeLoading" @click="handleSyncOrderAndPlan">
同步订单计划
</n-button>
<n-button @click="$emit('close-modal')">关闭</n-button>
</n-space>
<!-- </template> -->
</div>
</template>
<script setup lang="ts">
import { ref, h, computed, defineProps, onMounted, watch, defineEmits} from 'vue'
import { NButton, NSpace, NIcon, NTag, NDatePicker, NDataTable, useMessage, useDialog, type DataTableColumns } from 'naive-ui'
import { type KingdeePrdMo, type KingdeeProcessRoute } from '@/api/orderProject'
import { useRouter } from 'vue-router'
import { CalendarOutline } from '@vicons/ionicons5'
const emit = defineEmits(['close-modal','holdTemporarilyCallback', "synchronizationCallback","reCrawlCallback"])
const dialog = useDialog()
const message = useMessage()
const router = useRouter()
const props = defineProps({
kingdeeTableData: {
type: Array,
default: () => []
},
selectedId:{
type: Number,
default: null
}
})
onMounted(() => {
kingdeeTableData.value = props.kingdeeTableData as KingdeeMoTableRow[]
})
watch(()=>props.kingdeeTableData,()=>{
kingdeeTableData.value = props.kingdeeTableData as KingdeeMoTableRow[]
},{ deep: true } )
const kingdeeModalVisible = ref(false)
const kingdeeLoading = ref(false)
/** 主表行:工序仅存 processRoutes避免 children 触发树形重复行 */
type KingdeeMoTableRow = Omit<KingdeePrdMo, 'children'> & { processRoutes: KingdeeProcessRoute[] }
const kingdeeTableData = ref<KingdeeMoTableRow[]>([])
const kingdeeExpandedKeys = ref<Array<string | number>>([])
// const kingdeeCurrentProjectId = ref<number | null>(null) // ID
const kingdeeTableMaxHeight = 'min(78vh, 720px)'
const kingdeeMoScrollX = 1820
const kingdeeProcessScrollX = 1360
const kingdeeMoColumns: DataTableColumns<KingdeeMoTableRow> = [
{
type: 'expand',
expandable: () => true,
renderExpand: (row) => renderKingdeeExpand(row),
},
{
title: '类型', key: 'rowType', width: 96, align: 'center',
render() {
return h(NTag, { size: 'small', type: 'info', bordered: false }, { default: () => '生产订单' })
}
},
{ title: '单据编号', key: 'billNo', width: 120, align: 'center', ellipsis: { tooltip: true } },
{ title: '生产令号', key: 'productionOrderNo', width: 130, align: 'center', ellipsis: { tooltip: true } },
{ title: '工艺路线', key: 'routingNo', width: 110, align: 'center', ellipsis: { tooltip: true } },
{ title: '物料编码', key: 'materialCode', width: 180, ellipsis: { tooltip: true } },
{ title: '物料名称', key: 'materialName', width: 160, ellipsis: { tooltip: true } },
{
title: '数量', key: 'quantity', width: 90, align: 'center',
render(row) {
return formatKingdeeQty(row.quantity)
}
},
{ title: '生产车间', key: 'workShopName', width: 100, align: 'center', ellipsis: { tooltip: true } },
{ title: '规格型号', key: 'specModel', width: 110, align: 'center', ellipsis: { tooltip: true } },
{ title: '单位', key: 'unit', width: 60, align: 'center' },
{
title: '计划开工', key: 'planStartTime', width: 180, align: 'center',
render(row) {
return renderKingdeePlanPickerMo(row, 'planStartTime')
}
},
{
title: '计划完工', key: 'planFinishTime', width: 180, align: 'center',
render(row) {
return renderKingdeePlanPickerMo(row, 'planFinishTime')
}
},
{
title: '领料状态', key: 'pickMtrlStatus', width: 90, align: 'center',
render(row) {
const val = row.pickMtrlStatus
return val != null ? (pickMtrlStatusMap[String(val)] ?? val) : '-'
}
},
]
const kingdeeProcessColumns: DataTableColumns<KingdeeProcessRoute> = [
{
title: '类型', key: 'rowType', width: 72, align: 'center',
render() {
return h(NTag, { size: 'small', type: 'warning', bordered: false }, { default: () => '工序' })
}
},
{
title: '工序号', key: 'operNumber', width: 80, align: 'center',
render(row) {
return row.operNumber != null ? String(row.operNumber) : '-'
}
},
{ title: '工序名称', key: 'processProperty', width: 100, align: 'center' },
{ title: '工序说明', key: 'operDescription', width: 140, ellipsis: { tooltip: true } },
{ title: '工作中心', key: 'workCenterName', width: 110, align: 'center', ellipsis: { tooltip: true } },
{ title: '生产车间', key: 'departmentName', width: 110, align: 'center', ellipsis: { tooltip: true } },
{
title: '计划开始时间', key: 'planStartTime', width: 180, align: 'center',
render(row) {
return renderKingdeePlanPicker(row, 'planStartTime')
}
},
{
title: '计划结束时间', key: 'planFinishTime', width: 180, align: 'center',
render(row) {
return renderKingdeePlanPicker(row, 'planFinishTime')
}
},
{
title: '活动量', key: 'activityQty', width: 90, align: 'center',
render(row) {
return formatKingdeeQty(row.activityQty)
}
},
{ title: '活动单位', key: 'activityUnit', width: 80, align: 'center' },
{ title: '控制码', key: 'optCtrlCodeName', width: 120, align: 'center', ellipsis: { tooltip: true } },
]
const kingdeeAllExpanded = computed(() => {
const total = kingdeeTableData.value.length
if (!total) return false
return kingdeeExpandedKeys.value.length >= total
})
const pickMtrlStatusMap: Record<string, string> = {
'1': '未领料',
'2': '待定',
'3': '已领料',
}
function kingdeeMoRowClassName() {
return 'kingdee-mo-row'
}
function formatDateTime(ts: number) {
const d = new Date(ts)
const pad = (n: number) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
}
function parseKingdeePlanTime(val: string | number | null | undefined): number | null {
if (val == null || val === '') return null
if (typeof val === 'number') return val
const ts = new Date(val.replace(' ', 'T')).getTime()
return Number.isNaN(ts) ? null : ts
}
function renderKingdeePlanPicker(row: KingdeeProcessRoute, field: 'planStartTime' | 'planFinishTime') {
return h(NDatePicker, {
value: parseKingdeePlanTime(row[field]),
type: 'datetime',
clearable: true,
size: 'small',
style: { width: '100%' },
onUpdateValue: (val: number | null) => {
row[field] = val != null ? formatDateTime(val) : null
},
})
}
function renderKingdeePlanPickerMo(row: KingdeeMoTableRow, field: 'planStartTime' | 'planFinishTime') {
return h(NDatePicker, {
value: parseKingdeePlanTime(row[field]),
type: 'datetime',
clearable: true,
size: 'small',
style: { width: '100%' },
onUpdateValue: (val: number | null) => {
row[field] = val != null ? formatDateTime(val) : null
},
})
}
function formatKingdeeQty(val: number | null | undefined) {
return val != null ? Number(val).toFixed(3) : '-'
}
function kingdeeMoRowKey(row: KingdeeMoTableRow) {
return `mo-${row.productionOrderNo}-${row.billNo}`
}
function kingdeeToggleExpandAll() {
if (kingdeeAllExpanded.value) {
kingdeeExpandedKeys.value = []
} else {
kingdeeExpandedKeys.value = kingdeeTableData.value.map(kingdeeMoRowKey)
}
}
function renderKingdeeExpand(row: KingdeeMoTableRow) {
const routes = getKingdeeRoutes(row)
if (!routes.length) {
return h('div', { class: 'kingdee-expand-empty' }, '暂无工艺路线工序')
}
return h('div', { class: 'kingdee-expand-wrap' }, [
h(NDataTable, {
class: 'kingdee-process-table',
columns: kingdeeProcessColumns,
data: routes,
rowKey: kingdeeProcessRowKey,
size: 'small',
bordered: true,
striped: true,
scrollX: kingdeeProcessScrollX,
}),
])
}
function getKingdeeRoutes(mo: KingdeeMoTableRow) {
return mo.processRoutes
}
function kingdeeProcessRowKey(row: KingdeeProcessRoute) {
return `route-${row.operNumber}-${row.workCenterName}-${row.operDescription}`
}
//
function handleGanttSchedule() {
// ID sessionStorage使
sessionStorage.setItem('gantt_schedule_data', JSON.stringify(kingdeeTableData.value))
if (props.selectedId) {
sessionStorage.setItem('gantt_project_id', String(props.selectedId))
}
//
router.push('/biz/orderProject/gantt')
}
//
async function handleSyncOrderAndPlan() {
if (!props.selectedId) return
kingdeeLoading.value = true
try {
// processRoutes children
const draftData = kingdeeTableData.value.map(mo => {
const { processRoutes, ...rest } = mo
return { ...rest, children: processRoutes } as KingdeePrdMo
})
emit('synchronizationCallback', props.selectedId, draftData, (res) => {
if("success" === res){
message.success('同步订单计划成功')
emit('close-modal')
}
})
kingdeeModalVisible.value = false
} catch (error) {
//
} finally {
kingdeeLoading.value = false
}
}
const reCrawlReorderForm = () => {
// // 稿
if (!props.selectedId) return
dialog.warning({
title: '提示',
content: '重启读取会导致当前页面数据全部重置, 确定需要重新读取吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
kingdeeLoading.value = true
try {
emit('reCrawlCallback', props.selectedId, true)
message.success('重启读取成功')
} catch (error) {
//
} finally {
kingdeeLoading.value = false
}
}
})
}
const handleSaveDraft = () => {
// // 稿
if (!props.selectedId) return
kingdeeLoading.value = true
try {
const draftData = kingdeeTableData.value.map(mo => {
const { processRoutes, ...rest } = mo
return { ...rest, children: processRoutes } as KingdeePrdMo
})
emit('holdTemporarilyCallback', props.selectedId, draftData)
message.success('草稿保存成功')
} catch (error) {
//
} finally {
kingdeeLoading.value = false
}
}
// // 稿
// async function handleSaveDraft() {
// if (!kingdeeCurrentProjectId) return
// kingdeeLoading.value = true
// try {
// // processRoutes children
// const draftData = kingdeeTableData.value.map(mo => {
// const { processRoutes, ...rest } = mo
// return { ...rest, children: processRoutes } as KingdeePrdMo
// })
// emit('holdTemporarilyCallback', { id:kingdeeCurrentProjectId, data:draftData })
// // await orderProjectApi.saveKingdeeOrderDraft(kingdeeCurrentProjectId.value, draftData)
// // message.success('稿')
// } catch (error) {
// //
// } finally {
// kingdeeLoading.value = false
// }
// }
</script>
<style scoped>
.search-form {
margin-bottom: 16px;
}
.table-toolbar {
margin-bottom: 16px;
}
.kingdee-toolbar {
margin-bottom: 12px;
}
:deep(.kingdee-modal .n-card) {
max-height: 94vh;
display: flex;
flex-direction: column;
}
:deep(.kingdee-modal .n-card__content) {
flex: 1;
min-height: 0;
overflow: hidden;
}
:deep(.kingdee-mo-row td) {
background-color: rgba(32, 128, 240, 0.06) !important;
font-weight: 500;
}
:deep(.kingdee-mo-row:hover td) {
background-color: rgba(32, 128, 240, 0.1) !important;
}
.kingdee-expand-wrap {
margin: 4px 8px 8px 36px;
padding: 12px 14px 14px;
border: 1px solid var(--n-border-color);
border-left: 4px solid #18a058;
border-radius: 6px;
background: var(--n-action-color);
}
:deep(.kingdee-process-table .n-data-table-th) {
background: rgba(24, 160, 88, 0.12) !important;
}
:deep(.kingdee-process-table .n-data-table-td) {
background-color: var(--n-color) !important;
}
:deep(.kingdee-process-table .n-data-table-tr--striped .n-data-table-td) {
background-color: var(--n-action-color) !important;
}
.kingdee-expand-empty {
margin: 4px 8px 8px 36px;
padding: 12px 14px;
font-size: 13px;
color: var(--n-text-color-3);
border: 1px dashed var(--n-border-color);
border-radius: 6px;
background: var(--n-action-color);
}
</style>

View File

@ -34,7 +34,7 @@
<!-- 工具栏 --> <!-- 工具栏 -->
<div class="table-toolbar"> <div class="table-toolbar">
<n-space> <n-space>
<n-button <!--<n-button
size="small" size="small"
type="primary" type="primary"
@click="handleAdd" @click="handleAdd"
@ -42,15 +42,15 @@
<template #icon><n-icon><AddOutline /></n-icon></template> <template #icon><n-icon><AddOutline /></n-icon></template>
新增/补单 新增/补单
</n-button> </n-button>
<!-- <n-button @click="importModalVisible = true"> <n-button @click="importModalVisible = true">
<template #icon><n-icon><CloudUploadOutline /></n-icon></template> <template #icon><n-icon><CloudUploadOutline /></n-icon></template>
导入 导入
</n-button> </n-button>
<n-button @click="handleExport"> <n-button @click="handleExport">
<template #icon><n-icon><DownloadOutline /></n-icon></template> <template #icon><n-icon><DownloadOutline /></n-icon></template>
导出{{ selectedIds.length > 0 ? `(${selectedIds.length})` : '' }} 导出{{ selectedIds.length > 0 ? `(${selectedIds.length})` : '' }}
</n-button> --> </n-button>
<!-- <n-button type="error" :disabled="selectedIds.length === 0" @click="handleBatchDelete"> <n-button type="error" :disabled="selectedIds.length === 0" @click="handleBatchDelete">
<template #icon><n-icon><TrashOutline /></n-icon></template> <template #icon><n-icon><TrashOutline /></n-icon></template>
删除 删除
</n-button> --> </n-button> -->
@ -124,16 +124,11 @@
</n-form-item> </n-form-item>
</n-gi> --> </n-gi> -->
<n-gi> <n-gi>
<n-form-item label="项目名称" path="projectCode"> <n-form-item label="项目名称" path="project">
<!-- <n-input v-if="formData.id" v-model:value="formData.projectName" disabled placeholder="请选择项目名称" />
-->
<n-input v-if="formData.id" v-model:value="formData.projectName" disabled placeholder="请选择项目" /> <n-input v-if="formData.id" v-model:value="formData.projectName" disabled placeholder="请选择项目" />
<!--@search="xmhand"-->
<n-select <n-select
v-else v-else
v-model:value="formData.projectCode" v-model:value="formData.projectId"
filterable filterable
placeholder="请选择项目" placeholder="请选择项目"
:options="xmlist" :options="xmlist"
@ -182,6 +177,21 @@
<n-input v-else v-model:value="formData.quantity" placeholder="请输入生产数量" disabled /> <n-input v-else v-model:value="formData.quantity" placeholder="请输入生产数量" disabled />
</n-form-item> </n-form-item>
</n-gi> </n-gi>
<n-gi>
<n-form-item label="订单编号" path="orderCode">
<n-input v-model:value="formData.orderCode" placeholder="订单编号" :disabled="!formData.id" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="工艺路线编码" path="routeCode">
<n-input v-model:value="formData.routeCode" placeholder="工艺路线编码" :disabled="!formData.id" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="生产车间" path="proWorkshop">
<n-input v-model:value="formData.proWorkshop" placeholder="生产车间" :disabled="!formData.id" />
</n-form-item>
</n-gi>
<n-gi> <n-gi>
<n-form-item label="是否半成品" path="sfProduct"> <n-form-item label="是否半成品" path="sfProduct">
@ -341,77 +351,39 @@
</n-space> </n-space>
</template> </template>
</n-modal> </n-modal>
<!-- 导入弹窗 --> <n-modal
<!-- <n-modal v-model:show="importModalVisible" preset="card" title="导入生产订单" style="width: 500px"> v-model:show="kingdeeModalVisible"
<n-space vertical> preset="card"
<n-alert type="info"> :title="kingdeeModalTitle"
<template #header>导入说明</template> class="kingdee-modal"
<ul style="margin: 0; padding-left: 16px; line-height: 1.8"> :style="{ width: '96vw', maxWidth: '1920px' }"
<li>请先下载导入模板按模板格式填写数据</li> :content-style="{ padding: '16px 24px', minHeight: '72vh' }"
<li>支持 .xlsx .xls 格式</li> :mask-closable="false"
</ul> :closable="true"
</n-alert> :close-on-esc="false"
<n-space> >
<n-button type="primary" @click="handleDownloadTemplate"> <KingdeeVue
<template #icon><n-icon><DownloadOutline /></n-icon></template> :kingdeeTableData="kingdeeTableData"
下载模板 :selectedId="kingdeeOrderOtemId"
</n-button> @close-modal="kingdeeModalVisible = false"
</n-space> @holdTemporarilyCallback="holdTemporarilySave"
<n-upload :max="1" accept=".xlsx,.xls" :show-file-list="true" :custom-request="handleImportUpload"> @synchronizationCallback="synchronizationSave"
<n-upload-dragger> @reCrawlCallback="reCrawlReorderForm"/>
<div style="margin-bottom: 12px"> </n-modal>
<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> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { import { ref, reactive, h, onMounted } from 'vue'
ref, import { NButton, NSpace, NIcon, useMessage, useDialog, type UploadCustomRequestOptions} from 'naive-ui'
reactive, import { orderItemApi, type OrderItem } from '@/api/orderItem'
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 {
orderItemApi,
type OrderItem
} from '@/api/orderItem'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { VueDraggable } from 'vue-draggable-plus' import { VueDraggable } from 'vue-draggable-plus'
// import Gxitem from './gxitem.vue' import KingdeeVue from '@/views/biz/kingdee/index.vue'
import { type KingdeePrdMo, type KingdeeProcessRoute } from '@/api/orderProject'
import { userApi } from '@/api/system' import { userApi } from '@/api/system'
const message = useMessage() const message = useMessage()
@ -431,6 +403,9 @@ const hasPermission = (permission: string) => userStore.hasPermission(permission
const searchForm = reactive<any>({ const searchForm = reactive<any>({
code: '', code: '',
projectName: '', projectName: '',
orderCode: '',
routeCode: '',
proWorkshop: '',
beginTime: null as number | null, beginTime: null as number | null,
endTime: null as number | null, endTime: null as number | null,
}) })
@ -477,6 +452,9 @@ const defaultFormData = {
assingWorkOperationId: '', assingWorkOperationId: '',
assingWorkOperationTime: null, assingWorkOperationTime: null,
starter: '', starter: '',
orderCode: '',
routeCode: '',
proWorkshop: '',
} }
const formData = reactive<any>({ ...defaultFormData }) const formData = reactive<any>({ ...defaultFormData })
@ -512,6 +490,24 @@ const columns = [
title: '物料编码', title: '物料编码',
key: 'code' key: 'code'
}, },
{
align:'center',
minWidth: 150,
title: '订单编号',
key: 'orderCode',
},
{
align:'center',
minWidth: 150,
title: '工艺路线编码',
key: 'routeCode',
},
{
align:'center',
minWidth: 150,
title: '生产车间',
key: 'proWorkshop',
},
{ {
align:'center', align:'center',
minWidth: 150, minWidth: 150,
@ -602,7 +598,7 @@ const columns = [
align:'center', align:'center',
title: '操作', title: '操作',
key: 'actions', key: 'actions',
width: 140, width: 190,
fixed: 'right', fixed: 'right',
render(row:any) { render(row:any) {
@ -614,7 +610,6 @@ const columns = [
type:'primary', type:'primary',
ghost:true, ghost:true,
onClick: () => { handleEdit(row) } }, onClick: () => { handleEdit(row) } },
//{ default: () => [h(NIcon, null, { default: () => h(CreateOutline) }), ' ' ]}
{ default: () => '编辑'} { default: () => '编辑'}
)) ))
} }
@ -627,6 +622,15 @@ const columns = [
{ default: () => '派工'} { default: () => '派工'}
)) ))
} }
if(hasPermission('biz:orderItem:reorderForm')){
buttons.push(h(NButton, {
size: 'small',
type:'info',
ghost:true,
onClick: () => handleReorderForm(row, false) }, //, disabled: row.starter !==1
{ default: () => '补单'}
))
}
return buttons.length > 0 ? h(NSpace, {justify:'center'}, { default: () => buttons }) : '-' return buttons.length > 0 ? h(NSpace, {justify:'center'}, { default: () => buttons }) : '-'
@ -664,7 +668,10 @@ async function loadData() {
page: pagination.page, page: pagination.page,
pageSize: pagination.pageSize, pageSize: pagination.pageSize,
code: searchForm.code, code: searchForm.code,
projectId: searchForm.projectId, projectName: searchForm.projectName,
orderCode: searchForm.orderCode,
routeCode: searchForm.routeCode,
proWorkshop: searchForm.proWorkshop,
beginTime: searchForm.beginTime, beginTime: searchForm.beginTime,
endTime: searchForm.endTime endTime: searchForm.endTime
}) })
@ -684,13 +691,12 @@ function handleSearch() {
// //
function handleReset() { function handleReset() {
searchForm.code = '' searchForm.code = ''
searchForm.projectName = ''
searchForm.projectId = '' searchForm.orderCode = ''
searchForm.routeCode = ''
searchForm.proWorkshop = ''
searchForm.beginTime = null searchForm.beginTime = null
searchForm.endTime = null searchForm.endTime = null
handleSearch() handleSearch()
} }
@ -718,14 +724,20 @@ let planList = ref<any>([])
if(formData.code){ if(formData.code){
planList.value.splice(0) planList.value.splice(0)
orderItemApi.preCreation({ orderItemApi.preCreation({
//productionCode:formData.productionCode
code:formData.code, code:formData.code,
projectCode:formData.projectCode projectCode:formData.projectCode
}).then((rps:any) => { }).then((rps:any) => {
planList.value = rps.planList?rps.planList:[] planList.value = rps.planList ? rps.planList : []
formData.orderCode = rps.orderCode ?? ''
formData.routeCode = rps.routeCode ?? ''
formData.proWorkshop = rps.proWorkshop ?? ''
if (rps.quantity != null) formData.quantity = rps.quantity
}) })
}else{ }else{
planList.value.splice(0) planList.value.splice(0)
formData.orderCode = ''
formData.routeCode = ''
formData.proWorkshop = ''
} }
} }
@ -754,35 +766,22 @@ let xmlist = ref<any>([])
let xmloading = ref(false) let xmloading = ref(false)
function xmhand() { async function xmhand() {
xmloading.value = true xmloading.value = true
orderItemApi.xmoptions({ xmlist.value = await orderItemApi.xmoptions({});
// page: 1,
// pageSize: 100,
// username: v,
}).then((rps:any) =>{
if(rps && rps.length > 0){
xmlist.value = rps.map((n:any) => {
return {
label:n.label,
value:n.value+''
}
})
}
xmloading.value = false xmloading.value = false
}).catch(() => {
xmloading.value = false
})
} }
let wllist = ref<any>([]) let wllist = ref<any>([])
function xmselupdate(v:any,opt:any) { function xmselupdate(v:any,opt:any) {
// console.log(v,'sle')
// console.log(opt,'opt')
planList.value.splice(0) planList.value.splice(0)
wllist.value.splice(0) wllist.value.splice(0)
formData.code = '' formData.code = ''
formData.orderCode = ''
formData.routeCode = ''
formData.proWorkshop = ''
formData.projectName = opt.label formData.projectName = opt.label
formData.projectCode = opt?.additionParams?.projectCode || ''
if(v){ if(v){
wlhand(opt.value) wlhand(opt.value)
} }
@ -791,29 +790,91 @@ function xmselupdate(v:any,opt:any) {
// //
let wlloading = ref(false) let wlloading = ref(false)
function wlhand(v:any) { async function wlhand(v:any) {
wlloading.value = true wlloading.value = true
orderItemApi.wloptions({ wllist.value = await orderItemApi.wloptions({projectId:v });
projectId:v
// page: 1,
// pageSize: 100,
// username: v
}).then((rps:any) =>{
if(rps && rps.length > 0){
wllist.value = rps.map((n:any) => {
return {
label:n.label,
value:n.value+''
}
})
}
console.log(yglist.value,'12')
wlloading.value = false wlloading.value = false
}).catch(() => {
wlloading.value = false
})
} }
const kingdeeOrderOtemId = ref<number | null>(null) // ID
const kingdeeModalVisible = ref(false)
const kingdeeModalTitle = ref('金蝶生产订单')
const kingdeeLoading = ref(false)
type KingdeeMoTableRow = Omit<KingdeePrdMo, 'children'> & { processRoutes: KingdeeProcessRoute[] }
const kingdeeTableData = ref<KingdeeMoTableRow[]>([])
// const kingdeeExpandedKeys = ref<Array<string | number>>([])
async function reCrawlReorderForm(id:number,reCrawl:Boolean){
try {
const data = await orderItemApi.preCreationInfo(id, {reCrawl:reCrawl})
kingdeeTableData.value = (Array.isArray(data) ? data : []).map((mo) => {
const { children, ...rest } = mo
const routes = [...(children ?? [])].sort((a, b) => (a.operNumber ?? 0) - (b.operNumber ?? 0)).map((r) => ({
...r,
planStartTime: r.planStartTime ?? null,
planFinishTime: r.planFinishTime ?? null,
}))
return { ...rest, processRoutes: routes }
})
} finally {
kingdeeLoading.value = false
}
}
async function handleReorderForm(row:OrderItem, reCrawl:Boolean){
if (!row.id) {
message.warning('缺少物料订单ID无法查询')
return
}
kingdeeOrderOtemId.value = row.id
kingdeeModalTitle.value = `金蝶生产订单${row.code ? ` - ${row.code}` : ''}`
kingdeeModalVisible.value = true
kingdeeLoading.value = true
reCrawlReorderForm(row.id, false)
kingdeeLoading.value = false
}
// 稿
async function holdTemporarilySave(id: number, data: KingdeePrdMo[]) {
try {
// processRoutes children
await orderItemApi.holdTemporarilySave(id, data)
} catch (error) {
//
}
}
// const synchronizationSave = async (id: number, data: KingdeePrdMo[], callback:Function) => {
// try {
// await orderItemApi.synchronizationSave(id, data) .then((rps:any) =>{
// }).catch((res) => {
// callback(res)
// })
// //
// } catch (err) {
// callback(err)
// }
// }
//
async function synchronizationSave(id: number, data: KingdeePrdMo[], callback:Function) {
try {
// processRoutes children
await orderItemApi.synchronizationSave(id, data)
.then((rps:any) =>{
callback(rps)
}).catch((res) => {
callback(res)
})
} catch (error) {
//
}
}
// //
function handleEdit(row: OrderItem) { function handleEdit(row: OrderItem) {
modalTitle.value = '编辑生产订单' modalTitle.value = '编辑生产订单'
@ -1022,7 +1083,10 @@ async function handleExport() {
const params: Record<string, any> = {} const params: Record<string, any> = {}
if (selectedIds.value.length > 0) params.ids = selectedIds.value if (selectedIds.value.length > 0) params.ids = selectedIds.value
if (searchForm.code) params.code = searchForm.code if (searchForm.code) params.code = searchForm.code
if (searchForm.projectId != null) params.projectId = searchForm.projectId if (searchForm.projectName) params.projectName = searchForm.projectName
if (searchForm.orderCode) params.orderCode = searchForm.orderCode
if (searchForm.routeCode) params.routeCode = searchForm.routeCode
if (searchForm.proWorkshop) params.proWorkshop = searchForm.proWorkshop
if (searchForm.beginTime != null) params.beginTime = searchForm.beginTime if (searchForm.beginTime != null) params.beginTime = searchForm.beginTime
if (searchForm.endTime != null) params.endTime = searchForm.endTime if (searchForm.endTime != null) params.endTime = searchForm.endTime
const blob = await orderItemApi.export(params) const blob = await orderItemApi.export(params)

View File

@ -1,25 +1,43 @@
<template> <template>
<div class="gantt-schedule-page"> <div class="gantt-schedule-page">
<div class="gantt-header"> <div class="gantt-header">
<n-space justify="space-between" align="center"> <div class="gantt-header-left">
<n-h2 style="margin: 0">甘特图排产</n-h2> <h1 class="page-title">甘特图排产</h1>
<p class="page-desc">拖拽工序条块调整计划时间完成后请保存排产草稿</p>
</div>
<n-space> <n-space>
<n-button type="primary" :loading="saving" @click="handleSaveDraft">保存排产草稿</n-button> <n-button type="primary" :loading="saving" @click="handleSaveDraft">保存排产草稿</n-button>
<n-button @click="handleBack">返回</n-button> <n-button @click="handleBack">返回</n-button>
</n-space> </n-space>
</n-space>
</div> </div>
<div class="gantt-container" ref="ganttContainer"></div>
<div class="gantt-meta">
<span>生产订单 {{ scheduleStats.moCount }} </span>
<span class="meta-divider">|</span>
<span>工序 {{ scheduleStats.routeCount }} </span>
<span class="meta-divider">|</span>
<span>今日 {{ todayLabel }}</span>
<span class="meta-divider">|</span>
<span class="meta-legend"><i class="legend-today-line" />今日时间轴</span>
</div>
<div class="gantt-panel">
<div class="gantt-panel-head">
<span class="panel-title">排产明细</span>
<span class="panel-tip">主产品行展示物料编码与名称子行展示工序号与工序名称</span>
</div>
<div class="gantt-container" ref="ganttContainer" />
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, toRaw } from 'vue' import { ref, onMounted, onUnmounted, toRaw, computed } from 'vue'
import { useMessage } from 'naive-ui' import { useMessage } from 'naive-ui'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { gantt } from 'dhtmlx-gantt' import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css' import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
import { orderProjectApi, type KingdeePrdMo } from '@/api/orderProject' import { orderProjectApi, normalizeKingdeeProcessRoute, resolveProcessName, type KingdeePrdMo } from '@/api/orderProject'
const router = useRouter() const router = useRouter()
const ganttContainer = ref<HTMLElement | null>(null) const ganttContainer = ref<HTMLElement | null>(null)
@ -30,6 +48,42 @@ const tableData = ref<any[]>([])
const projectId = ref<number | null>(null) const projectId = ref<number | null>(null)
const saving = ref(false) const saving = ref(false)
const PROCESS_COLORS = [
'#409eff', '#67c23a', '#e6a23c', '#909399', '#f56c6c',
'#36cfc9', '#597ef7', '#9254de', '#73d13d',
]
const WEEKDAY_LABELS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
function getRouteColorIndex(index: number) {
return index % PROCESS_COLORS.length
}
function getRouteColor(index: number) {
return PROCESS_COLORS[getRouteColorIndex(index)]
}
function formatDayScale(date: Date) {
return `${date.getDate()}${WEEKDAY_LABELS[date.getDay()]}`
}
const scheduleStats = computed(() => {
let routeCount = 0
tableData.value.forEach((mo) => {
routeCount += mo.processRoutes?.length ?? 0
})
return {
moCount: tableData.value.length,
routeCount,
}
})
const todayLabel = computed(() => {
const d = new Date()
const pad = (n: number) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
})
function handleBack() { function handleBack() {
router.back() router.back()
} }
@ -42,7 +96,6 @@ async function handleSaveDraft() {
saving.value = true saving.value = true
try { try {
// processRoutes children
const draftData = tableData.value.map(mo => { const draftData = tableData.value.map(mo => {
const { processRoutes, ...rest } = mo const { processRoutes, ...rest } = mo
return { ...rest, children: processRoutes } as KingdeePrdMo return { ...rest, children: processRoutes } as KingdeePrdMo
@ -50,11 +103,7 @@ async function handleSaveDraft() {
await orderProjectApi.saveKingdeeOrderDraft(projectId.value, draftData) await orderProjectApi.saveKingdeeOrderDraft(projectId.value, draftData)
message.success('排产草稿保存成功') message.success('排产草稿保存成功')
// session
sessionStorage.setItem('gantt_schedule_data', JSON.stringify(tableData.value)) sessionStorage.setItem('gantt_schedule_data', JSON.stringify(tableData.value))
} catch (error) {
//
} finally { } finally {
saving.value = false saving.value = false
} }
@ -62,10 +111,8 @@ async function handleSaveDraft() {
onMounted(async () => { onMounted(async () => {
try { try {
initGantt() initGantt()
const stateStr = sessionStorage.getItem('gantt_schedule_data') const stateStr = sessionStorage.getItem('gantt_schedule_data')
const idStr = sessionStorage.getItem('gantt_project_id') const idStr = sessionStorage.getItem('gantt_project_id')
@ -75,12 +122,15 @@ onMounted(async () => {
if (stateStr) { if (stateStr) {
const parsedData = JSON.parse(stateStr) const parsedData = JSON.parse(stateStr)
tableData.value = parsedData tableData.value = (Array.isArray(parsedData) ? parsedData : []).map((mo: any) => ({
...mo,
processRoutes: (mo.processRoutes ?? []).map(normalizeKingdeeProcessRoute),
}))
renderGanttData() renderGanttData()
} else { } else {
message.warning('未获取到排产数据') message.warning('未获取到排产数据')
} }
} catch (error) { } catch {
message.error('加载排产数据失败') message.error('加载排产数据失败')
} }
}) })
@ -90,122 +140,197 @@ function formatGanttDate(date: Date) {
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
} }
function formatGridDate(value: Date | string | undefined) {
if (!value) return ''
const date = value instanceof Date ? value : new Date(String(value).replace(' ', 'T'))
if (isNaN(date.getTime())) return ''
const pad = (n: number) => String(n).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
}
function formatMoMaterialLabel(mo: any) {
const code = mo.materialCode?.trim() || ''
const name = mo.materialName?.trim() || ''
if (code && name) return `${code} ${name}`
return code || name
}
function initGantt() { function initGantt() {
if (!ganttContainer.value) return if (!ganttContainer.value) return
//
ganttEventIds.forEach(id => gantt.detachEvent(id)) ganttEventIds.forEach(id => gantt.detachEvent(id))
ganttEventIds = [] ganttEventIds = []
gantt.clearAll() gantt.clearAll()
// gantt.config.date_format = '%Y-%m-%d %H:%i:%s'
gantt.config.date_format = "%Y-%m-%d %H:%i:%s" gantt.config.scale_height = 60
gantt.config.scale_height = 50 gantt.config.row_height = 40
gantt.config.bar_height = 26
gantt.config.scales = [ gantt.config.scales = [
{ unit: "month", step: 1, format: "%Y年 %m月" }, { unit: 'month', step: 1, format: '%Y年 %m月' },
{ unit: "day", step: 1, format: "%d日" } { unit: 'day', step: 1, format: formatDayScale },
] ]
gantt.config.columns = [ gantt.config.columns = [
{ name: "text", label: "单号 / 序号", tree: true, width: 200, resize: true }, {
{ name: "materialCode", label: "物料编码", align: "center", width: 140, resize: true }, name: 'text',
{ name: "name", label: "名称", align: "center", width: 160, resize: true }, label: '工序号 / 物料编码',
{ name: "start_date", label: "计划开始", align: "center", width: 130 }, tree: true,
{ name: "end_date", label: "计划结束", align: "center", width: 130 }, width: 200,
resize: true,
template(task: any) {
if (task._type === 'route') return task.operNumber ?? ''
return task.materialCode ?? ''
},
},
{
name: 'routeProcessName',
label: '工序名称 / 物料名称',
align: 'center',
width: 168,
resize: true,
template(task: any) {
if (task._type === 'route') {
return task.routeProcessName ?? resolveProcessName(task._raw ?? {}) ?? ''
}
return task.materialName ?? ''
},
},
{
name: 'start_date',
label: '计划开始',
align: 'center',
width: 118,
template(task: any) {
return formatGridDate(task.start_date)
},
},
{
name: 'end_date',
label: '计划结束',
align: 'center',
width: 118,
template(task: any) {
return formatGridDate(task.end_date)
},
},
] ]
//??
gantt.config.layout = { gantt.config.layout = {
css: "gantt_container", css: 'gantt_container',
cols: [ cols: [
{ {
width: 300, width: 620,
minWidth: 200, minWidth: 420,
maxWidth: 600, maxWidth: 680,
rows: [ rows: [
{ view: "grid", scrollX: "gridScroll", scrollable: true, { view: 'grid', scrollX: 'gridScroll', scrollable: true, scrollY: 'scrollVer' },
scrollY: "scrollVer" { view: 'scrollbar', id: 'gridScroll', group: 'horizontal' },
}, ],
{ view: "scrollbar", id: "gridScroll", group: "horizontal" } /*!*/
]
}, },
{ resizer: true, width: 1 }, { resizer: true, width: 1 },
{ {
rows: [ rows: [
{ view: "timeline", scrollX: "scrollHor", scrollY: "scrollVer" }, { view: 'timeline', scrollX: 'scrollHor', scrollY: 'scrollVer' },
{ view: "scrollbar", id: "scrollHor", group: "horizontal" } /*!*/ { view: 'scrollbar', id: 'scrollHor', group: 'horizontal' },
] ],
}, },
{ view: "scrollbar", id: "scrollVer" } { view: 'scrollbar', id: 'scrollVer' },
] ],
} }
//??
gantt.config.drag_progress = false
// gantt.config.drag_links = false
gantt.config.drag_progress = false // // 使
gantt.config.drag_links = false // 线 gantt.config.work_time = false
gantt.config.work_time = true // gantt.config.correct_work_time = false
// gantt.config.auto_scheduling gantt.config.skip_off_time = false
gantt.config.duration_unit = 'day'
gantt.i18n.setLocale("cn") gantt.config.smart_rendering = true
// marker gantt.i18n.setLocale('cn')
gantt.plugins({ gantt.plugins({ marker: true })
marker: true
}) gantt.templates.task_text = function(_start, _end, task) {
if (task._type === 'route') {
// return task.routeProcessName || resolveProcessName(task._raw ?? {}) || ''
gantt.templates.task_text = function(start, end, task) { }
if (task._type === 'route') { return task.materialName || task.materialLabel || ''
return task.name; // }
gantt.templates.task_class = function(_start, _end, task) {
if (task._type === 'mo') return 'gantt-task-mo'
if (task._type === 'route') {
return `gantt-task-route gantt-route-c-${task.colorIndex ?? 0}`
}
return ''
}
gantt.templates.grid_row_class = function(_start, _end, task) {
if (task._type === 'mo') return 'gantt-row-mo'
if (task._type === 'route') {
return `gantt-row-route gantt-route-c-${task.colorIndex ?? 0}`
}
return ''
}
gantt.templates.timeline_cell_class = function(_task, date) {
const day = date.getDay()
if (day === 0 || day === 6) return 'gantt-weekend'
return ''
} }
return task.text;
};
gantt.init(ganttContainer.value) gantt.init(ganttContainer.value)
}
// /** 今日时间轴标记(数据刷新后需重新添加) */
const today = new Date() function addTodayMarker() {
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 12, 0, 0)
gantt.addMarker({ gantt.addMarker({
start_date: today, start_date: today,
css: "today-marker", css: 'today-marker',
text: "今日", text: '今日',
title: "今日: " + formatGanttDate(today) title: `今日: ${formatGanttDate(now)}`,
}) })
gantt.showDate(today)
}
function getRouteProcessName(route: any) {
return resolveProcessName(route)
} }
function renderGanttData() { function renderGanttData() {
gantt.clearAll() gantt.clearAll()
//
const tasks: any[] = [] const tasks: any[] = []
const rawData = toRaw(tableData.value) || [] const rawData = toRaw(tableData.value) || []
const processColors = ['#3498db', '#1abc9c', '#9b59b6', '#e67e22', '#e74c3c', '#2ecc71', '#f1c40f', '#34495e', '#7f8c8d']
rawData.forEach((mo: any, moIndex: number) => { rawData.forEach((mo: any, moIndex: number) => {
const moId = `mo_${moIndex}_${mo.productionOrderNo}_${mo.billNo}` const moId = `mo_${moIndex}_${mo.productionOrderNo}_${mo.billNo}`
// 使
let minDate: Date | null = mo.planStartTime ? new Date(mo.planStartTime.replace(' ', 'T')) : null let minDate: Date | null = mo.planStartTime ? new Date(mo.planStartTime.replace(' ', 'T')) : null
let maxDate: Date | null = mo.planFinishTime ? new Date(mo.planFinishTime.replace(' ', 'T')) : null let maxDate: Date | null = mo.planFinishTime ? new Date(mo.planFinishTime.replace(' ', 'T')) : null
if (!minDate || isNaN(minDate.getTime())) minDate = new Date() if (!minDate || isNaN(minDate.getTime())) minDate = new Date()
if (!maxDate || isNaN(maxDate.getTime())) maxDate = new Date(minDate.getTime() + 86400000) if (!maxDate || isNaN(maxDate.getTime())) maxDate = new Date(minDate.getTime() + 86400000)
if (maxDate <= minDate) maxDate = new Date(minDate.getTime() + 86400000) if (maxDate <= minDate) maxDate = new Date(minDate.getTime() + 86400000)
const materialLabel = formatMoMaterialLabel(mo)
tasks.push({ tasks.push({
id: moId, id: moId,
text: mo.materialCode || '', //mo.productionOrderNo || mo.billNo || '', ?? text: materialLabel,
materialLabel,
materialCode: mo.materialCode || '', materialCode: mo.materialCode || '',
name: mo.materialName || '产品', materialName: mo.materialName || '',
operNumber: '',
processName: '',
start_date: formatGanttDate(minDate), start_date: formatGanttDate(minDate),
end_date: formatGanttDate(maxDate), end_date: formatGanttDate(maxDate),
open: false, // open: true,
type: gantt.config.types.project, type: gantt.config.types.project,
_type: 'mo', _type: 'mo',
progress:0.6, //?? progress: 0,
_raw: mo _raw: mo,
}) })
if (mo.processRoutes && mo.processRoutes.length > 0) { if (mo.processRoutes?.length > 0) {
mo.processRoutes.forEach((route: any, index: number) => { mo.processRoutes.forEach((route: any, index: number) => {
const routeId = `route_${moId}_${index}` const routeId = `route_${moId}_${index}`
let rStart = route.planStartTime ? new Date(route.planStartTime.replace(' ', 'T')) : null let rStart = route.planStartTime ? new Date(route.planStartTime.replace(' ', 'T')) : null
@ -213,45 +338,47 @@ function renderGanttData() {
if (!rStart || isNaN(rStart.getTime())) rStart = minDate if (!rStart || isNaN(rStart.getTime())) rStart = minDate
if (!rEnd || isNaN(rEnd.getTime())) rEnd = maxDate if (!rEnd || isNaN(rEnd.getTime())) rEnd = maxDate
if (rEnd <= rStart) rEnd = new Date(rStart!.getTime() + 3600000) // 1 if (rEnd <= rStart) rEnd = new Date(rStart!.getTime() + 3600000)
const operNumber = route.operNumber != null ? String(route.operNumber) : String(index + 1)
const routeProcessName = getRouteProcessName(route)
const colorIndex = getRouteColorIndex(index)
const routeColor = getRouteColor(index)
tasks.push({ tasks.push({
id: routeId, id: routeId,
text:mo.materialCode || '', //route.operNumber != null ? String(route.operNumber) : String(index + 1), ?? text: operNumber,
materialCode: '', operNumber,
name: route.processProperty || '工序', routeProcessName,
color: processColors[index % processColors.length], colorIndex,
routeColor,
color: routeColor,
start_date: formatGanttDate(rStart!), start_date: formatGanttDate(rStart!),
end_date: formatGanttDate(rEnd), end_date: formatGanttDate(rEnd),
parent: moId, parent: moId,
_type: 'route', _type: 'route',
progress:0.6, //?? progress: 0,
_raw: route _raw: route,
}) })
}) })
} }
}) })
gantt.parse({ data: tasks, links: [] }) gantt.parse({ data: tasks, links: [] })
addTodayMarker()
// const evDragId = gantt.attachEvent('onBeforeTaskDrag', (id) => {
const evDragId = gantt.attachEvent("onBeforeTaskDrag", (id, mode, e) => {
const task = gantt.getTask(id) const task = gantt.getTask(id)
if (task.type === gantt.config.types.project) { return task.type !== gantt.config.types.project
return false
}
return true
}) })
ganttEventIds.push(evDragId) ganttEventIds.push(evDragId)
// const evId = gantt.attachEvent('onAfterTaskDrag', (id, mode) => {
const evId = gantt.attachEvent("onAfterTaskDrag", (id, mode, e) => {
const task = gantt.getTask(id) const task = gantt.getTask(id)
if (mode === 'move' || mode === 'resize') { if (mode === 'move' || mode === 'resize') {
const start = gantt.date.date_to_str(gantt.config.date_format)(task.start_date) const start = gantt.date.date_to_str(gantt.config.date_format)(task.start_date)
const end = gantt.date.date_to_str(gantt.config.date_format)(task.end_date) const end = gantt.date.date_to_str(gantt.config.date_format)(task.end_date)
//
if (task._type === 'mo') { if (task._type === 'mo') {
task._raw.planStartTime = start task._raw.planStartTime = start
task._raw.planFinishTime = end task._raw.planFinishTime = end
@ -259,33 +386,35 @@ function renderGanttData() {
task._raw.planStartTime = start task._raw.planStartTime = start
task._raw.planFinishTime = end task._raw.planFinishTime = end
//
const parentId = task.parent const parentId = task.parent
if (parentId) { if (parentId) {
const parentTask = gantt.getTask(parentId) const parentTask = gantt.getTask(parentId)
if (parentTask) {
const children = gantt.getChildren(parentId) const children = gantt.getChildren(parentId)
let minDate: Date | null = null let minD: Date | null = null
let maxDate: Date | null = null let maxD: Date | null = null
children.forEach((childId: string | number) => { children.forEach((childId: string | number) => {
const childTask = gantt.getTask(childId) const childTask = gantt.getTask(childId)
if (!minDate || childTask.start_date < minDate) minDate = childTask.start_date const cs = childTask.start_date
if (!maxDate || childTask.end_date > maxDate) maxDate = childTask.end_date const ce = childTask.end_date
if (cs && (!minD || cs < minD)) minD = cs
if (ce && (!maxD || ce > maxD)) maxD = ce
}) })
if (minDate && maxDate) { if (minD && maxD) {
parentTask.start_date = new Date(minDate) parentTask.start_date = new Date(minD)
parentTask.end_date = new Date(maxDate) parentTask.end_date = new Date(maxD)
parentTask._raw.planStartTime = gantt.date.date_to_str(gantt.config.date_format)(minDate) parentTask._raw.planStartTime = gantt.date.date_to_str(gantt.config.date_format)(minD)
parentTask._raw.planFinishTime = gantt.date.date_to_str(gantt.config.date_format)(maxDate) parentTask._raw.planFinishTime = gantt.date.date_to_str(gantt.config.date_format)(maxD)
gantt.updateTask(parentId) gantt.updateTask(parentId)
} }
} }
} }
}
message.success(`任务 [${task.text}] 计划时间已更新`) const label = task._type === 'route'
? (task.routeProcessName || task.operNumber)
: (task.materialLabel || task.materialName)
message.success(`任务 [${label}] 计划时间已更新`)
} }
}) })
ganttEventIds.push(evId) ganttEventIds.push(evId)
@ -299,36 +428,249 @@ onUnmounted(() => {
<style scoped> <style scoped>
.gantt-schedule-page { .gantt-schedule-page {
--gs-font: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', sans-serif;
--gs-title: #333333;
--gs-text: #606266;
--gs-border: #e4e7ed;
--gs-bg: #f5f7fa;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: calc(100vh - 120px); height: calc(100vh - 120px);
background-color: var(--n-color);
padding: 16px; padding: 16px;
border-radius: 8px;
box-sizing: border-box; box-sizing: border-box;
font-family: var(--gs-font);
font-weight: 400;
color: var(--gs-text);
background: var(--gs-bg);
} }
.gantt-header { .gantt-header {
margin-bottom: 16px; display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-shrink: 0; flex-shrink: 0;
margin-bottom: 12px;
padding: 14px 16px;
background: #fff;
border: 1px solid var(--gs-border);
border-radius: 4px;
}
.gantt-header-left {
min-width: 0;
}
.page-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--gs-title);
line-height: 1.4;
}
.page-desc {
margin: 4px 0 0;
font-size: 13px;
font-weight: 400;
color: var(--gs-text);
line-height: 1.5;
}
.gantt-meta {
flex-shrink: 0;
margin-bottom: 12px;
padding: 0 4px;
font-size: 13px;
font-weight: 400;
color: var(--gs-text);
}
.meta-divider {
margin: 0 10px;
color: #dcdfe6;
}
.meta-legend {
display: inline-flex;
align-items: center;
gap: 6px;
}
.legend-today-line {
display: inline-block;
width: 2px;
height: 14px;
background: #f56c6c;
border-radius: 1px;
}
.gantt-panel {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
background: #fff;
border: 1px solid var(--gs-border);
border-radius: 4px;
overflow: hidden;
}
.gantt-panel-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 16px;
background: #fafafa;
border-bottom: 1px solid var(--gs-border);
flex-shrink: 0;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: var(--gs-title);
}
.panel-tip {
font-size: 12px;
font-weight: 400;
color: #909399;
} }
.gantt-container { .gantt-container {
flex: 1; flex: 1;
width: 100%; width: 100%;
min-height: 0; /* 必须设置min-height以允许flex子元素内部滚动 */ min-height: 0;
border: 1px solid var(--n-border-color);
border-radius: 4px;
overflow: hidden; overflow: hidden;
} }
:deep(.today-marker) { /* dhtmlx-gantt仅表头标题加粗其余正常字重 */
background-color: #ff5252; .gantt-container :deep(.gantt_container) {
font-family: var(--gs-font);
font-weight: 400;
border: none !important;
background: #fff;
} }
:deep(.today-marker .gantt_marker_content) {
background-color: #ff5252; .gantt-container :deep(.gantt_grid_scale),
color: white; .gantt-container :deep(.gantt_task_scale) {
background: #fafafa;
border-bottom: 1px solid var(--gs-border) !important;
}
.gantt-container :deep(.gantt_task_scale .gantt_scale_cell) {
font-size: 12px;
font-weight: 400;
color: var(--gs-text);
border-color: var(--gs-border) !important;
line-height: 1.3;
white-space: normal;
}
.gantt-container :deep(.gantt_grid_scale .gantt_grid_head_cell) {
font-size: 13px;
font-weight: 600;
color: var(--gs-title);
border-color: var(--gs-border) !important;
}
/* 工序号文字与进度条同色(无圆点) */
.gantt-container :deep(.gantt_row.gantt-row-route.gantt-route-c-0 .gantt_cell:first-child) { color: #409eff; }
.gantt-container :deep(.gantt_row.gantt-row-route.gantt-route-c-1 .gantt_cell:first-child) { color: #67c23a; }
.gantt-container :deep(.gantt_row.gantt-row-route.gantt-route-c-2 .gantt_cell:first-child) { color: #e6a23c; }
.gantt-container :deep(.gantt_row.gantt-row-route.gantt-route-c-3 .gantt_cell:first-child) { color: #909399; }
.gantt-container :deep(.gantt_row.gantt-row-route.gantt-route-c-4 .gantt_cell:first-child) { color: #f56c6c; }
.gantt-container :deep(.gantt_row.gantt-row-route.gantt-route-c-5 .gantt_cell:first-child) { color: #36cfc9; }
.gantt-container :deep(.gantt_row.gantt-row-route.gantt-route-c-6 .gantt_cell:first-child) { color: #597ef7; }
.gantt-container :deep(.gantt_row.gantt-row-route.gantt-route-c-7 .gantt_cell:first-child) { color: #9254de; }
.gantt-container :deep(.gantt_row.gantt-row-route.gantt-route-c-8 .gantt_cell:first-child) { color: #73d13d; }
.gantt-container :deep(.gantt_task_line.gantt-route-c-0) { background-color: #409eff !important; border-color: #409eff !important; }
.gantt-container :deep(.gantt_task_line.gantt-route-c-1) { background-color: #67c23a !important; border-color: #67c23a !important; }
.gantt-container :deep(.gantt_task_line.gantt-route-c-2) { background-color: #e6a23c !important; border-color: #e6a23c !important; }
.gantt-container :deep(.gantt_task_line.gantt-route-c-3) { background-color: #909399 !important; border-color: #909399 !important; }
.gantt-container :deep(.gantt_task_line.gantt-route-c-4) { background-color: #f56c6c !important; border-color: #f56c6c !important; }
.gantt-container :deep(.gantt_task_line.gantt-route-c-5) { background-color: #36cfc9 !important; border-color: #36cfc9 !important; }
.gantt-container :deep(.gantt_task_line.gantt-route-c-6) { background-color: #597ef7 !important; border-color: #597ef7 !important; }
.gantt-container :deep(.gantt_task_line.gantt-route-c-7) { background-color: #9254de !important; border-color: #9254de !important; }
.gantt-container :deep(.gantt_task_line.gantt-route-c-8) { background-color: #73d13d !important; border-color: #73d13d !important; }
.gantt-container :deep(.gantt_task_line.gantt-task-route .gantt_task_progress) {
background-color: rgba(0, 0, 0, 0.18) !important;
opacity: 1;
}
.gantt-container :deep(.gantt_task_line.gantt-task-route) {
border: none !important;
border-radius: 3px;
}
.gantt-container :deep(.gantt_grid_data .gantt_cell) {
font-size: 13px;
font-weight: 400;
color: var(--gs-text);
border-color: #f0f2f5 !important;
}
.gantt-container :deep(.gantt_row.gantt-row-mo) {
background: #fafafa;
}
.gantt-container :deep(.gantt_row.gantt-row-mo .gantt_cell) {
font-weight: 400;
color: var(--gs-title);
}
.gantt-container :deep(.gantt_row:hover) {
background: #f5f7fa !important;
}
.gantt-container :deep(.gantt_task_row.gantt_selected),
.gantt-container :deep(.gantt_row.gantt_selected) {
background: #ecf5ff !important;
}
.gantt-container :deep(.gantt_task_line.gantt-task-route .gantt_task_content) {
font-size: 12px;
font-weight: 400;
color: #fff;
padding: 0 6px;
}
.gantt-container :deep(.gantt_task_line.gantt_project) {
display: none;
}
.gantt-container :deep(.gantt_weekend) {
background: #fafafa;
}
.gantt-container :deep(.gantt_marker.today-marker) {
background: #f56c6c;
width: 2px;
z-index: 4;
opacity: 1;
}
.gantt-container :deep(.today-marker .gantt_marker_content) {
background: #f56c6c;
color: #fff;
font-size: 12px;
font-weight: 600;
border-radius: 2px; border-radius: 2px;
padding: 0 4px; padding: 2px 8px;
box-shadow: 0 1px 4px rgba(245, 108, 108, 0.35);
}
.gantt-container :deep(.gantt_layout_content) {
border-color: var(--gs-border) !important;
}
.gantt-container :deep(.gantt_hor_scroll),
.gantt-container :deep(.gantt_ver_scroll) {
background: #fafafa;
} }
</style> </style>

View File

@ -148,48 +148,13 @@
:closable="false" :closable="false"
:close-on-esc="false" :close-on-esc="false"
> >
<div class="kingdee-toolbar"> <KingdeeVue
<n-space> :kingdeeTableData="kingdeeTableData"
<n-button :selectedId="kingdeeCurrentProjectId"
size="small" @close-modal="kingdeeModalVisible = false"
:disabled="kingdeeLoading || !kingdeeTableData.length" @holdTemporarilyCallback="handleSaveDraft"
@click="kingdeeToggleExpandAll" @synchronizationCallback="handleSyncOrderAndPlan"
> @reCrawlCallback="reCrawlReorderForm"/>
{{ kingdeeAllExpanded ? '全部收起' : '全部展开' }}
</n-button>
<n-button
size="small"
type="primary"
:disabled="kingdeeLoading || !kingdeeTableData.length"
@click="handleGanttSchedule"
>
<template #icon><n-icon><CalendarOutline /></n-icon></template>
甘特图排产
</n-button>
</n-space>
</div>
<n-data-table
v-model:expanded-row-keys="kingdeeExpandedKeys"
:columns="kingdeeMoColumns"
:data="kingdeeTableData"
:loading="kingdeeLoading"
:row-key="kingdeeMoRowKey"
:scroll-x="kingdeeMoScrollX"
:max-height="kingdeeTableMaxHeight"
:row-class-name="kingdeeMoRowClassName"
size="small"
/>
<template #footer>
<n-space justify="end">
<n-button type="info" :disabled="kingdeeLoading" @click="handleSyncOrderAndPlan">
同步订单计划
</n-button>
<n-button type="primary" :disabled="kingdeeLoading" @click="handleSaveDraft">
保存草稿
</n-button>
<n-button @click="kingdeeModalVisible = false">关闭</n-button>
</n-space>
</template>
</n-modal> </n-modal>
<!-- 导入弹窗 --> <!-- 导入弹窗 -->
@ -229,12 +194,15 @@
import { ref, reactive, h, computed, onMounted, watch } from 'vue' import { ref, reactive, h, computed, onMounted, watch } from 'vue'
import { NButton, NSpace, NIcon, NTag, NDatePicker, NDataTable, NUpload, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions } from 'naive-ui' import { NButton, NSpace, NIcon, NTag, NDatePicker, NDataTable, NUpload, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions } from 'naive-ui'
import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline, EyeOutline, CalendarOutline } from '@vicons/ionicons5' import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline, EyeOutline, CalendarOutline } from '@vicons/ionicons5'
import { orderProjectApi, type OrderProject, type KingdeePrdMo, type KingdeeProcessRoute } from '@/api/orderProject' import { orderProjectApi, type OrderProject, type KingdeePrdMo, type KingdeeProcessRoute, normalizeKingdeeProcessRoute } from '@/api/orderProject'
import { dictDataApi } from '@/api/org' import { dictDataApi } from '@/api/org'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import KingdeeVue from '@/views/biz/kingdee/index.vue'
const message = useMessage() const message = useMessage()
const dialog = useDialog() const dialog = useDialog()
const userStore = useUserStore() const userStore = useUserStore()
@ -451,7 +419,7 @@ const columns: DataTableColumns<OrderProject> = [
const buttons = [] const buttons = []
if (hasPermission('biz:orderProject:getKingdeeOrder')) { if (hasPermission('biz:orderProject:getKingdeeOrder')) {
buttons.push( buttons.push(
h(NButton, { size: 'small', quaternary: true, type: 'info', onClick: () => handleKingdeeQuery(row) }, { h(NButton, { size: 'small', quaternary: true, type: 'info', onClick: () => handleKingdeeQuery(row, false) }, {
default: () => [h(NIcon, null, { default: () => h(EyeOutline) }), ' 预生产查询'] default: () => [h(NIcon, null, { default: () => h(EyeOutline) }), ' 预生产查询']
}) })
) )
@ -534,7 +502,7 @@ const kingdeeProcessColumns: DataTableColumns<KingdeeProcessRoute> = [
return row.operNumber != null ? String(row.operNumber) : '-' return row.operNumber != null ? String(row.operNumber) : '-'
} }
}, },
{ title: '工序名称', key: 'processProperty', width: 100, align: 'center' }, { title: '工序名称', key: 'processName', width: 100, align: 'center' },
{ title: '工序说明', key: 'operDescription', width: 140, ellipsis: { tooltip: true } }, { title: '工序说明', key: 'operDescription', width: 140, ellipsis: { tooltip: true } },
{ title: '工作中心', key: 'workCenterName', width: 110, align: 'center', ellipsis: { tooltip: true } }, { title: '工作中心', key: 'workCenterName', width: 110, align: 'center', ellipsis: { tooltip: true } },
{ title: '生产车间', key: 'departmentName', width: 110, align: 'center', ellipsis: { tooltip: true } }, { title: '生产车间', key: 'departmentName', width: 110, align: 'center', ellipsis: { tooltip: true } },
@ -557,7 +525,7 @@ const kingdeeProcessColumns: DataTableColumns<KingdeeProcessRoute> = [
} }
}, },
{ title: '活动单位', key: 'activityUnit', width: 80, align: 'center' }, { title: '活动单位', key: 'activityUnit', width: 80, align: 'center' },
{ title: '控制码', key: 'optCtrlCodeName', width: 120, align: 'center', ellipsis: { tooltip: true } }, { title: '工序控制码', key: 'optCtrlCodeName', width: 120, align: 'center', ellipsis: { tooltip: true } },
] ]
// //
@ -691,23 +659,14 @@ function handleGanttSchedule() {
router.push('/biz/orderProject/gantt') router.push('/biz/orderProject/gantt')
} }
// async function reCrawlReorderForm(id:number,reCrawl:Boolean){
async function handleKingdeeQuery(row: OrderProject) {
if (!row.id) {
message.warning('缺少项目ID无法查询')
return
}
kingdeeCurrentProjectId.value = row.id
kingdeeModalTitle.value = `金蝶生产订单${row.code ? ` - ${row.code}` : ''}`
kingdeeModalVisible.value = true
kingdeeLoading.value = true
kingdeeTableData.value = []
kingdeeExpandedKeys.value = []
try { try {
const data = await orderProjectApi.getKingdeeOrder(row.id) const data = await orderProjectApi.getKingdeeOrder(id,{reCrawl:reCrawl})
kingdeeTableData.value = (Array.isArray(data) ? data : []).map((mo) => { kingdeeTableData.value = (Array.isArray(data) ? data : []).map((mo) => {
const { children, ...rest } = mo const { children, ...rest } = mo
const routes = [...(children ?? [])].sort((a, b) => (a.operNumber ?? 0) - (b.operNumber ?? 0)).map((r) => ({ const routes = [...(children ?? [])]
.sort((a, b) => (a.operNumber ?? 0) - (b.operNumber ?? 0))
.map((r) => normalizeKingdeeProcessRoute({
...r, ...r,
planStartTime: r.planStartTime ?? null, planStartTime: r.planStartTime ?? null,
planFinishTime: r.planFinishTime ?? null, planFinishTime: r.planFinishTime ?? null,
@ -721,50 +680,115 @@ async function handleKingdeeQuery(row: OrderProject) {
kingdeeLoading.value = false kingdeeLoading.value = false
} }
} }
//
async function handleKingdeeQuery(row: OrderProject,reCrawl:Boolean) {
if (!row.id) {
message.warning('缺少项目ID无法查询')
return
}
kingdeeCurrentProjectId.value = row.id
kingdeeModalTitle.value = `金蝶生产订单${row.code ? ` - ${row.code}` : ''}`
kingdeeModalVisible.value = true
kingdeeLoading.value = true
kingdeeTableData.value = []
kingdeeExpandedKeys.value = []
reCrawlReorderForm(row.id,reCrawl)
// try {
// const data = await orderProjectApi.getKingdeeOrder(row.id)
// kingdeeTableData.value = (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 }
// })
// if (kingdeeTableData.value.length === 0) {
// message.info('')
// }
// } finally {
// kingdeeLoading.value = false
// }
}
// 稿 // 稿
async function handleSaveDraft() { // async function holdTemporarilyCallback(id: number, data: KingdeePrdMo[]) {
if (!kingdeeCurrentProjectId.value) return // await orderProjectApi.saveKingdeeOrderDraft(id, data)
// }
kingdeeLoading.value = true // 稿
async function handleSaveDraft(id: number, data: KingdeePrdMo[]) {
try { try {
// processRoutes children // processRoutes children
const draftData = kingdeeTableData.value.map(mo => { await orderProjectApi.saveKingdeeOrderDraft(id, data)
const { processRoutes, ...rest } = mo
return { ...rest, children: processRoutes } as KingdeePrdMo
})
await orderProjectApi.saveKingdeeOrderDraft(kingdeeCurrentProjectId.value, draftData)
message.success('草稿保存成功')
} catch (error) { } catch (error) {
// //
} finally {
kingdeeLoading.value = false
} }
} }
// //
async function handleSyncOrderAndPlan() { async function handleSyncOrderAndPlan(id: number, data: KingdeePrdMo[], callback:Function) {
if (!kingdeeCurrentProjectId.value) return
kingdeeLoading.value = true
try { try {
// processRoutes children // processRoutes children
const draftData = kingdeeTableData.value.map(mo => { await orderProjectApi.saveKingdeeOrderAndPlan(id, data)
const { processRoutes, ...rest } = mo .then((rps:any) =>{
return { ...rest, children: processRoutes } as KingdeePrdMo callback(rps)
}).catch((res) => {
callback(res)
}) })
await orderProjectApi.saveKingdeeOrderAndPlan(kingdeeCurrentProjectId.value, draftData)
message.success('同步订单计划成功')
kingdeeModalVisible.value = false
} catch (error) { } catch (error) {
// //
} finally {
kingdeeLoading.value = false
} }
} }
// // 稿
// async function handleSaveDraft() {
// if (!kingdeeCurrentProjectId.value) return
// kingdeeLoading.value = true
// try {
// // processRoutes children
// const draftData = kingdeeTableData.value.map(mo => {
// const { processRoutes, ...rest } = mo
// return { ...rest, children: processRoutes } as KingdeePrdMo
// })
// await orderProjectApi.saveKingdeeOrderDraft(kingdeeCurrentProjectId.value, draftData)
// message.success('稿')
// } catch (error) {
// //
// } finally {
// kingdeeLoading.value = false
// }
// }
//
// async function handleSyncOrderAndPlan() {
// if (!kingdeeCurrentProjectId.value) return
// kingdeeLoading.value = true
// try {
// // processRoutes children
// const draftData = kingdeeTableData.value.map(mo => {
// const { processRoutes, ...rest } = mo
// return { ...rest, children: processRoutes } as KingdeePrdMo
// })
// await orderProjectApi.saveKingdeeOrderAndPlan(kingdeeCurrentProjectId.value, draftData)
// message.success('')
// kingdeeModalVisible.value = false
// } catch (error) {
// //
// } finally {
// kingdeeLoading.value = false
// }
// }
// //
function handleDelete(row: OrderProject) { function handleDelete(row: OrderProject) {
dialog.warning({ dialog.warning({

View File

@ -180,8 +180,16 @@
<!--质检单详情面板--> <!--质检单详情面板-->
<n-modal v-model:show="detailVisible" preset="card" :title="detailTitle" style="width: 800px"> <n-modal v-model:show="detailVisible" preset="card" :title="detailTitle" style="width: 800px">
<n-card title="主表信息" hoverable style="margin-bottom: 12px" > <n-card hoverable style="margin-bottom: 12px" >
<n-descriptions :column="2" bordered> <template #header>
<div style="display:flex;justify-content:space-between;align-items:center;width:100%">
<span>主表信息</span>
<n-button text @click="infoIsCollapse = !infoIsCollapse">
<n-icon :component="infoIsCollapse ? ArrowDownOutline : ArrowUpOutline" />
</n-button>
</div>
</template>
<n-descriptions :column="3" bordered v-show="!infoIsCollapse">
<n-descriptions-item <n-descriptions-item
v-for="item in mappedInfoList" v-for="item in mappedInfoList"
:key="item.key" :key="item.key"
@ -191,13 +199,24 @@
</n-descriptions-item> </n-descriptions-item>
</n-descriptions> </n-descriptions>
</n-card> </n-card>
<n-card title="明细列表" hoverable style="margin-bottom: 12px"> <n-card hoverable style="margin-bottom: 12px">
<template #header>
<div style="display:flex;justify-content:space-between;align-items:center;width:100%">
<span>明细列表</span>
<n-button text @click="detailIsCollapse = !detailIsCollapse">
<n-icon :component="detailIsCollapse ? ArrowDownOutline : ArrowUpOutline" />
</n-button>
</div>
</template>
<n-data-table <n-data-table
v-show="!detailIsCollapse"
:columns="detailColumns" :columns="detailColumns"
:data="detailTableData" :data="detailTableData"
:loading="loading" :loading="loading"
:row-key="(row) => row.id" :row-key="(row) => row.id"
:scroll-x="1200" :scroll-x="700"
max-height="320"
:pagination="{ pageSize:4 }"
/> />
</n-card> </n-card>
<!-- <n-card title="追溯码" hoverable>--> <!-- <n-card title="追溯码" hoverable>-->
@ -205,110 +224,7 @@
<!-- </n-card>--> <!-- </n-card>-->
</n-modal> </n-modal>
<!--提交判定明细面板-->
<n-modal v-model:show="submitDetailModalVisible" preset="card" title="提交判定明细" style="width: 800px">
<n-collapse default-expanded-names="['ok', 'scrap', 'rework','concession']">
<n-collapse-item title="合格" name="ok">
<n-form ref="formRef" :model="formDetailOKData" :rules="formRules" label-placement="left" label-width="100px">
<n-grid x-gap="12" :cols="2">
<n-gi>
<n-form-item label="判定数量" path="qty">
<n-input-number v-model:value="formDetailOKData.qty" placeholder="请输入判定数量" :min="0" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="批次号或SN码" path="traceCode" v-show="traceCodeShow">
<n-input v-model:value="formDetailOKData.traceCode" placeholder="请输入批次号或SN码(启用追溯时必填)" />
</n-form-item>
</n-gi>
</n-grid>
<n-form-item label="备注" >
<n-input
v-model:value="formDetailOKData.remark"
type="textarea"
placeholder="输入备注原因"
/>
</n-form-item>
</n-form>
</n-collapse-item>
<n-collapse-item title="报废" name="scrap">
<n-form ref="formRef" :model="formDetailScrapData" :rules="formRules" label-placement="left" label-width="100px">
<n-grid x-gap="12" :cols="2">
<n-gi>
<n-form-item label="判定数量" path="qty">
<n-input-number v-model:value="formDetailScrapData.qty" placeholder="请输入判定数量" :min="0" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="批次号或SN码" path="traceCode" v-show="traceCodeShow">
<n-input v-model:value="formDetailScrapData.traceCode" placeholder="请输入批次号或SN码(启用追溯时必填)" />
</n-form-item>
</n-gi>
</n-grid>
<n-form-item label="备注" >
<n-input
v-model:value="formDetailScrapData.remark"
type="textarea"
placeholder="输入备注原因"
/>
</n-form-item>
</n-form>
</n-collapse-item>
<n-collapse-item title="返工" name="rework">
<n-form ref="formRef" :model="formDetailReworkData" :rules="formRules" label-placement="left" label-width="100px">
<n-grid x-gap="12" :cols="2">
<n-gi>
<n-form-item label="判定数量" path="qty">
<n-input-number v-model:value="formDetailReworkData.qty" placeholder="请输入判定数量" :min="0" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="批次号或SN码" path="traceCode" v-show="traceCodeShow">
<n-input v-model:value="formDetailReworkData.traceCode" placeholder="请输入批次号或SN码(启用追溯时必填)" />
</n-form-item>
</n-gi>
</n-grid>
<n-form-item label="备注" >
<n-input
v-model:value="formDetailReworkData.remark"
type="textarea"
placeholder="输入备注原因"
/>
</n-form-item>
</n-form>
</n-collapse-item>
<n-collapse-item title="让步接收" name="concession">
<n-form ref="formRef" :model="formDetailConcessionData" :rules="formRules" label-placement="left" label-width="100px">
<n-grid x-gap="12" :cols="2">
<n-gi>
<n-form-item label="判定数量" path="qty">
<n-input-number v-model:value="formDetailConcessionData.qty" placeholder="请输入判定数量" :min="0" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="批次号或SN码" path="traceCode" v-show="traceCodeShow">
<n-input v-model:value="formDetailConcessionData.traceCode" placeholder="请输入批次号或SN码(启用追溯时必填)" />
</n-form-item>
</n-gi>
</n-grid>
<n-form-item label="备注" >
<n-input
v-model:value="formDetailConcessionData.remark"
type="textarea"
placeholder="输入备注原因"
/>
</n-form-item>
</n-form>
</n-collapse-item>
</n-collapse>
<template #footer>
<n-space justify="end">
<n-button @click="submitDetailModalVisible = false">取消</n-button>
<n-button type="primary" @click="handleSubmitResultDetail">确定</n-button>
</n-space>
</template>
</n-modal>
</div> </div>
@ -317,12 +233,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, h, onMounted } from 'vue' import { ref, reactive, h, onMounted } from 'vue'
import { import {
NButton, NSpace, NIcon, NUpload, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions, NButton, NSpace, NIcon, NUpload,NTag, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions,
NDropdown NDropdown
} from 'naive-ui' } from 'naive-ui'
import { import {
SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline, SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline,
EllipsisHorizontalOutline EllipsisHorizontalOutline, ArrowDownOutline, ArrowUpOutline,ArchiveOutline as ArchiveIcon
} from '@vicons/ionicons5' } from '@vicons/ionicons5'
import { qualityTestingApi, type QualityTesting } from '@/api/qualityTesting' import { qualityTestingApi, type QualityTesting } from '@/api/qualityTesting'
import {assingReworkApi} from '@/api/assingRework' import {assingReworkApi} from '@/api/assingRework'
@ -330,9 +247,13 @@ import { dictDataApi } from '@/api/org'
import {Language} from "@xterm/xterm/src/vs/base/common/platform.ts"; import {Language} from "@xterm/xterm/src/vs/base/common/platform.ts";
import {fileApi} from '@/api/system1'
import value = Language.value; import value = Language.value;
import {QcResultDetail} from "@/api/qcResultDetail.ts"; import {QcResultDetail} from "@/api/qcResultDetail.ts";
import router from "@/router"; import router from "@/router";
import dd from 'dingtalk-jsapi'
import { env } from 'echarts'
const message = useMessage() const message = useMessage()
const dialog = useDialog() const dialog = useDialog()
@ -365,24 +286,53 @@ const importModalVisible = ref(false)
const detailVisible = ref(false) const detailVisible = ref(false)
const detailTitle = ref('') const detailTitle = ref('')
const infoIsCollapse = ref(false)
const detailIsCollapse = ref(true)
// //
const submitDetailModalVisible = ref(false) const submitDetailModalVisible = ref(false)
const traceCodeShow = ref(false) const traceCodeShow = ref(false)
///
const submitFlag = ref<Boolean>()
const customOrderKeys = [
'materialCode',
'materialName',
'materialQty',
'processCode',
'processName',
'processQty',
'assingCode',
'workerName',
'assingQty',
'qcNo',
'sourceType',
'totalQty',
'status',
'inspector',
'inspectTime',
'traceType'
]
const infoDataMapping = { const infoDataMapping = {
id: '质检单编号',
materialCode: '物料编码',
materialName: '物料名称',
materialQty: '物料数量',
processCode:'工序编号',
processName:'工序名称',
processQty:'生产总数',
assingCode:'派工编号',
workerName:'派工人',
assingQty:'派工数量',
qcNo: '质检单编号', qcNo: '质检单编号',
sourceType: '来源类型', sourceType: '来源类型',
sourceId: '来源编号',
orderId: '订单编号',
processId: '工序编号',
itemId: '物料编号',
totalQty: '报检总数', totalQty: '报检总数',
status: '质检状态', status: '质检状态',
inspectorId: '质检员编号', inspector: '质检员',
inspectTime: '质检时间', inspectTime: '质检时间',
traceType: '追溯类型', traceType: '追溯类型'
} }
const infoDataMap = new Map(Object.entries(infoDataMapping)) const infoDataMap = new Map(Object.entries(infoDataMapping))
const mappedInfoList = ref<Array<{ label: string; key: string; value: any }>>([]) const mappedInfoList = ref<Array<{ label: string; key: string; value: any }>>([])
@ -391,22 +341,27 @@ const detailColumns: DataTableColumns<Object> = [
{ {
title: '判定类型', title: '判定类型',
key: 'resultType', key: 'resultType',
align: 'center',
width: 80, width: 80,
render: (row) => { render: (row) => {
const val = row.resultType const val = row.resultType
const opt = qcResultTypeOptions.value.find(o => o.value === val || String(o.value) === String(val)) const opt = qcResultTypeOptions.value.find(o => o.value === val || String(o.value) === String(val))
return opt ? opt.label : (val ?? '-') if (!opt) return val ?? '-'
return h(NTag, { type: opt.class, size: 'small' }, { default: () => opt.label })
} }
}, },
{ {
title: '判定数量', title: '判定数量',
key: 'qty', key: 'qty',
align: 'center',
width: 80 width: 80
}, },
{ {
title: '批次号', title: '批次号',
key: 'traceCode', key: 'traceCode',
width: 80, width: 80,
align: 'center',
render: (row) => { render: (row) => {
const val = row.traceCode const val = row.traceCode
return val ?? '-' return val ?? '-'
@ -416,6 +371,7 @@ const detailColumns: DataTableColumns<Object> = [
title: '不良代码', title: '不良代码',
key: 'defectCode', key: 'defectCode',
width: 80, width: 80,
align: 'center',
render: (row) => { render: (row) => {
const val = row.defectCode const val = row.defectCode
return val ?? '-' return val ?? '-'
@ -425,16 +381,19 @@ const detailColumns: DataTableColumns<Object> = [
title: '后续动作', title: '后续动作',
key: 'handleAction', key: 'handleAction',
width: 80, width: 80,
align: 'center'
}, },
{ {
title: '动作执行状态', title: '动作执行状态',
key: 'actionStatus', key: 'actionStatus',
width: 80, width: 80,
align: 'center',
render: (row) => { render: (row) => {
const val = row.actionStatus const val = row.actionStatus
const opt = qcActionStatusOptions.value.find(o => o.value === val || String(o.value) === String(val)) const opt = qcActionStatusOptions.value.find(o => o.value === val || String(o.value) === String(val))
return opt ? opt.label : (val ?? '-') if (!opt) return val ?? '-'
return h(NTag, { type: opt.class, size: 'small' }, { default: () => opt.label })
} }
} }
] ]
@ -467,6 +426,8 @@ const defaultFormDetailData:QcResultDetail = {
handleAction: undefined, handleAction: undefined,
actionStatus: undefined actionStatus: undefined
} }
const formDetailOKData = reactive<QcResultDetail>({ ...defaultFormDetailData, resultType: 1,handleAction:'下一道工序',actionStatus:1 }) const formDetailOKData = reactive<QcResultDetail>({ ...defaultFormDetailData, resultType: 1,handleAction:'下一道工序',actionStatus:1 })
const formDetailScrapData = reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 2,handleAction:'报废次品',actionStatus:1 }) const formDetailScrapData = reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 2,handleAction:'报废次品',actionStatus:1 })
const formDetailReworkData = reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 3,handleAction:'返工',actionStatus:1 }) const formDetailReworkData = reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 3,handleAction:'返工',actionStatus:1 })
@ -497,37 +458,43 @@ const formRules = {
// //
const columns: DataTableColumns<QualityTesting> = [ const columns: DataTableColumns<QualityTesting> = [
{ type: 'selection' }, { type: 'selection' },
{ title: '质检编号', key: 'qcNo' }, { title: '质检编号', key: 'qcNo',align: 'center' },
{ title: '来源类型', key: 'sourceType', { title: '来源类型', key: 'sourceType',align: 'center',
render(row) { render(row) {
const val = row.sourceType const val = row.sourceType
const opt = qcSourceTypeOptions.value.find(o => o.value === val || String(o.value) === String(val)) const opt = qcSourceTypeOptions.value.find(o => o.value === val || String(o.value) === String(val))
return opt ? opt.label : (val ?? '-') if (!opt) return val ?? '-'
return h(NTag, { type: opt.class, size: 'small' }, { default: () => opt.label })
} }
}, },
// { title: 'ID(/)', key: 'sourceId' }, // { title: 'ID(/)', key: 'sourceId' },
// { title: 'ID', key: 'orderId' }, // { title: 'ID', key: 'orderId' },
// { title: 'ID', key: 'processId' }, // { title: 'ID', key: 'processId' },
// { title: 'ID', key: 'itemId' }, // { title: 'ID', key: 'itemId' },
{ title: '报检总数', key: 'totalQty' }, { title: '报检总数', key: 'totalQty',align: 'center' },
{ title: '质检状态', key: 'status', { title: '已检验数量', key: 'inspectQty',align: 'center' },
{ title: '质检状态', key: 'status',align: 'center',
render(row) { render(row) {
const val = row.status const val = row.status
const opt = qcStatusOptions.value.find(o => o.value === val || String(o.value) === String(val)) const opt = qcStatusOptions.value.find(o => o.value === val || String(o.value) === String(val))
return opt ? opt.label : (val ?? '-') if (!opt) return val ?? '-'
return h(NTag, { type: opt.class, size: 'small' }, { default: () => opt.label })
} }
}, },
{ title: '检验员', key: 'inspector' }, { title: '检验员', key: 'inspector',align: 'center' },
{ title: '检验完成时间', key: 'inspectTime' }, { title: '检验完成时间', key: 'inspectTime',align: 'center' },
{ title: '追溯类型', key: 'traceType', { title: '追溯类型', key: 'traceType',align: 'center',
render(row) { render(row) {
const val = row.traceType const val = row.traceType
const opt = qcTraceTypeOptions.value.find(o => o.value === val || String(o.value) === String(val)) const opt = qcTraceTypeOptions.value.find(o => o.value === val || String(o.value) === String(val))
return opt ? opt.label : (val ?? '-') if (!opt) return val ?? '-'
return h(NTag, { type: opt.class, size: 'small' }, { default: () => opt.label })
} }
}, },
{ title: '创建时间', key: 'createTime', width: 180 }, { title: '创建时间', key: 'createTime',align: 'center', width: 180 },
{ title: '更新时间', key: 'updateTime', width: 180 }, { title: '更新时间', key: 'updateTime',align: 'center', width: 180 },
{ {
title: '操作', title: '操作',
key: 'actions', key: 'actions',
@ -546,8 +513,10 @@ const columns: DataTableColumns<QualityTesting> = [
options: [ options: [
{ label: '查看详情', key: 'view' }, { label: '查看详情', key: 'view' },
{ type: 'divider' }, // 线 { type: 'divider' }, // 线
{ label: '提交判定明细', key: 'submitResultDetail' }, { label: '提交判定明细', key: 'submitResultDetail',show:row.status == 1 },
{ type: 'divider' }, // 线 { type: 'divider',show:row.status == 1 }, // 线
{ label: '提交部分判定明细', key: 'submitBFResultDetail',show:(row.status == 1 || row.status == 2) },
{ type: 'divider',show:(row.status == 1 || row.status == 2) }, // 线
{ label: '查询判定明细', key: 'queryResultDetail' }, { label: '查询判定明细', key: 'queryResultDetail' },
{ type: 'divider' }, // 线 { type: 'divider' }, // 线
{ label: '质检撤回', key: 'cancelQuality' }, { label: '质检撤回', key: 'cancelQuality' },
@ -561,6 +530,9 @@ const columns: DataTableColumns<QualityTesting> = [
case 'submitResultDetail': case 'submitResultDetail':
submitResultDetail(row) submitResultDetail(row)
break break
case 'submitBFResultDetail':
submitBFResultDetail(row)
break
case 'queryResultDetail': case 'queryResultDetail':
queryResultDetail(row) queryResultDetail(row)
break break
@ -781,28 +753,28 @@ async function handleImportUpload({ file }: UploadCustomRequestOptions) {
async function loadDictOptions() { async function loadDictOptions() {
try { try {
const data = await dictDataApi.listByType('sys_status') const data = await dictDataApi.listByType('sys_status')
statusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue) })) statusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class: d.listClass }))
} catch {} } catch {}
try { try {
const data = await dictDataApi.listByType('qc_status') const data = await dictDataApi.listByType('qc_status')
qcStatusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue) })) qcStatusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class: d.listClass }))
}catch {} }catch {}
try { try {
const data = await dictDataApi.listByType('source_type') const data = await dictDataApi.listByType('source_type')
qcSourceTypeOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue) })) qcSourceTypeOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class: d.listClass }))
}catch {} }catch {}
try { try {
const data = await dictDataApi.listByType('qc_trace_type') const data = await dictDataApi.listByType('qc_trace_type')
qcTraceTypeOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue) })) qcTraceTypeOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class: d.listClass }))
}catch {} }catch {}
try { try {
const data = await dictDataApi.listByType('qc_result_type') const data = await dictDataApi.listByType('qc_result_type')
qcResultTypeOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue) })) qcResultTypeOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class: d.listClass }))
}catch {} }catch {}
try { try {
const data = await dictDataApi.listByType('qc_action_status') const data = await dictDataApi.listByType('qc_action_status')
qcActionStatusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue) })) qcActionStatusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class: d.listClass }))
}catch {} }catch {}
} }
@ -812,18 +784,28 @@ async function handleDetail(row: QualityTesting) {
detailVisible.value = true detailVisible.value = true
const data = await qualityTestingApi.detail(row.id!) const data = await qualityTestingApi.detail(row.id!)
// // //
mappedInfoList.value = Object.entries(data.info) // mappedInfoList.value = Object.entries(data.info)
.filter(([key]) => infoDataMap.has(key)) // // .filter(([key]) => infoDataMap.has(key)) //
.map(([key, value]) => ({ // .map(([key, value]) => ({
label: infoDataMap.get(key) || key, // // label: infoDataMap.get(key) || key, //
key, // // key, //
value // // value //
})) // }))
setMappedInfoList(data.info)
detailTableData.value = data.detail detailTableData.value = data.detail
} }
//
function setMappedInfoList(rawData: Record<string, any>) {
mappedInfoList.value = customOrderKeys.map(key => ({
key,
label: infoDataMapping[key as keyof typeof infoDataMapping],
value: rawData[key]
}))
}
function formatDisplayValue(key: string, value: any): string { function formatDisplayValue(key: string, value: any): string {
if (value === null || value === undefined) return '-' if (value === null || value === undefined) return '-'
@ -860,34 +842,56 @@ async function loadAssignWorkOrder() {
} }
// //
async function submitResultDetail(row) { function submitResultDetail(row) {
Object.assign(formDetailOKData, reactive<QcResultDetail>({ ...defaultFormDetailData, resultType: 1,handleAction:'下一道工序',actionStatus:1 }))
Object.assign(formDetailScrapData, reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 2,handleAction:'报废次品',actionStatus:1 }))
Object.assign(formDetailReworkData, reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 3,handleAction:'返工',actionStatus:1 }))
Object.assign(formDetailConcessionData, reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 4,handleAction:'让步接收',actionStatus:1 }))
if (row.traceType != 1) { if (row.traceType != 1) {
traceCodeShow.value = true traceCodeShow.value = true
} }
submitDetailModalVisible.value = true submitFlag.value = true
formDetailOKData.qcId = row.id
formDetailScrapData.qcId = row.id
formDetailReworkData.qcId = row.id let path = `/qc/submitDetail/${row.qcNo}/${row.id}/${submitFlag.value}/${traceCodeShow.value}`
formDetailConcessionData.qcId = row.id router.push(path)
} }
async function handleSubmitResultDetail() { async function handleSubmitResultDetail() {
try {
const submitParam = [formDetailOKData,formDetailScrapData,formDetailReworkData,formDetailConcessionData] formDetailConcessionData.files = uploadFiles.value
await qualityTestingApi.submit(submitParam)
const submitParam = [
formDetailOKData,
formDetailScrapData,
formDetailReworkData,
formDetailConcessionData]
console.log(submitParam);
submitFlag.value? await qualityTestingApi.submit(submitParam) : await qualityTestingApi.submitBF(submitParam)
message.success('提交成功') message.success('提交成功')
}catch ( error) {
message.error('提交失败'+error)
}
submitDetailModalVisible.value = false submitDetailModalVisible.value = false
loadData() loadData()
} }
//
function submitBFResultDetail(row) {
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)
}
// //
function queryResultDetail(row) { function queryResultDetail(row) {
let path = `detail/${row.id}`; let path = `detail/${row.id}`;
@ -913,6 +917,9 @@ function handleCancelQuality(row) {
}) })
} }
onMounted(() => { onMounted(() => {
loadData() loadData()
loadDictOptions() loadDictOptions()

View File

@ -0,0 +1,378 @@
<template>
<div class="page-container">
<n-card :title="title" style="margin-bottom: 16px">
<n-tabs type="card" animated>
<n-tab-pane name="合格" tab="合格">
<n-form ref="formRef" :model="formDetailOKData" label-placement="left" label-width="100px">
<n-grid x-gap="12" :cols="2">
<n-gi>
<n-form-item label="判定数量" path="qty">
<n-input-number v-model:value="formDetailOKData.qty" placeholder="请输入判定数量" :min="0" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="批次号或SN码" path="traceCode" v-show="traceCodeShow">
<n-input v-model:value="formDetailOKData.traceCode" placeholder="请输入批次号或SN码(启用追溯时必填)" />
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="8" :cols="3">
<n-gi>
<n-form-item label="备注" >
<n-input
v-model:value="formDetailOKData.remark"
type="textarea"
placeholder="输入备注原因"
/>
</n-form-item>
</n-gi>
</n-grid>
</n-form>
</n-tab-pane>
<n-tab-pane name="报废" tab="报废">
<n-form ref="formRef" :model="formDetailScrapData" label-placement="left" label-width="100px">
<n-grid x-gap="12" :cols="2">
<n-gi>
<n-form-item label="判定数量" path="qty">
<n-input-number v-model:value="formDetailScrapData.qty" placeholder="请输入判定数量" :min="0" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="批次号或SN码" path="traceCode" v-show="traceCodeShow">
<n-input v-model:value="formDetailScrapData.traceCode" placeholder="请输入批次号或SN码(启用追溯时必填)" />
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="8" :cols="3">
<n-gi>
<n-form-item label="备注" >
<n-input
v-model:value="formDetailScrapData.remark"
type="textarea"
placeholder="输入备注原因"
/>
</n-form-item>
</n-gi>
</n-grid>
</n-form>
</n-tab-pane>
<n-tab-pane name="返工" tab="返工">
<n-form ref="formRef" :model="formDetailReworkData" label-placement="left" label-width="100px">
<n-grid x-gap="12" :cols="2">
<n-gi>
<n-form-item label="判定数量" path="qty">
<n-input-number v-model:value="formDetailReworkData.qty" placeholder="请输入判定数量" :min="0" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="批次号或SN码" path="traceCode" v-show="traceCodeShow">
<n-input v-model:value="formDetailReworkData.traceCode" placeholder="请输入批次号或SN码(启用追溯时必填)" />
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="8" :cols="3">
<n-gi>
<n-form-item label="备注" >
<n-input
v-model:value="formDetailReworkData.remark"
type="textarea"
placeholder="输入备注原因"
/>
</n-form-item>
</n-gi>
</n-grid>
</n-form>
</n-tab-pane>
<n-tab-pane name="让步接收" tab="让步接收">
<n-form ref="formRef" :model="formDetailConcessionData" label-placement="left" label-width="100px">
<n-grid x-gap="12" :cols="2">
<n-gi>
<n-form-item label="判定数量" path="qty">
<n-input-number v-model:value="formDetailConcessionData.qty" placeholder="请输入判定数量" :min="0" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="批次号或SN码" path="traceCode" v-show="traceCodeShow">
<n-input v-model:value="formDetailConcessionData.traceCode" placeholder="请输入批次号或SN码(启用追溯时必填)" />
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="8" :cols="3">
<n-gi>
<n-form-item label="备注" >
<n-input
v-model:value="formDetailConcessionData.remark"
type="textarea"
placeholder="输入备注原因"
/>
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="8" :cols="3">
<n-gi>
<n-form-item label="图片">
<n-upload
action="null"
:max="5"
:custom-request="()=>{}"
@change="handleUploadImageChange"
v-model:file-list="imageList"
list-type="image-card"
>
点击上传
</n-upload>
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="8" :cols="3">
<n-gi>
<n-form-item label="附件">
<n-upload
multiple
directory-dnd
:max="5"
:action="null"
:custom-request="()=>{}"
@change="handleUploadChange"
v-model:file-list="fileList"
accept=".pdf,.doc,.docx,.xls,.xlsx"
:max-size="10*1024*1024"
>
<n-upload-dragger>
<div style="margin-bottom: 12px">
<n-icon size="48" :depth="3">
<ArchiveIcon />
</n-icon>
</div>
<n-text style="font-size: 16px">
点击或者拖动文件到该区域来上传
</n-text>
<n-p depth="3" style="margin: 8px 0 0 0">
仅支持 PDFDOCDOCXXLSXLSXJPGPNGGIF单文件最大10MB
</n-p>
</n-upload-dragger>
</n-upload>
</n-form-item>
</n-gi>
</n-grid>
</n-form>
</n-tab-pane>
</n-tabs>
<n-space justify="center" style="width: 100%; margin-top: 16px;">
<n-button size="small" @click="handleReset">重置</n-button>
<n-button type="primary" size="small" @click="handleSubmitResultDetail">提交</n-button>
</n-space>
</n-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, h, onMounted } from 'vue'
import {
NButton, NSpace, NIcon, NUpload,NTag, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions,
NDropdown
} 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 router from '@/router';
const message = useMessage()
const route = useRoute()
const qcId =ref( Number(route.params.id))
const traceCodeShow = ref(route.params.traceCodeShow === 'true')
const submitFlag = ref(route.params.submitFlag === 'true')
const MAX_FILE_SIZE = 10 * 1024 * 1024
//
const imageList = ref<any[]>([])
const uploadImages = ref<File[]>([])
const imageExts = ['jpg', 'jpeg', 'png']
//
const fileList = ref<any[]>([])
const uploadFiles = ref<File[]>([])
const fileExts = ['pdf', 'doc', 'docx', 'xls', 'xlsx']
const title = ref(`质检编号: ${route.params.qcNo}`)
const defaultFormDetailData:QcResultDetail = {
qcId: undefined,
resultType: undefined,
qty: 0,
traceCode: undefined,
defectCode: undefined,
handleAction: undefined,
actionStatus: undefined,
remark: '',
title:undefined,
images:[],
attachments:[]
}
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 })
async function handleSubmitResultDetail() {
formDetailOKData.qcId = qcId.value
formDetailScrapData.qcId = qcId.value
formDetailReworkData.qcId = qcId.value
formDetailConcessionData.qcId =qcId.value
formDetailConcessionData.images = uploadImages.value
formDetailConcessionData.attachments = uploadFiles.value
const submitParam = [
formDetailOKData,
formDetailScrapData,
formDetailReworkData,
formDetailConcessionData]
console.log(submitParam);
submitFlag.value? await qualityTestingApi.submit(submitParam) : await qualityTestingApi.submitBF(submitParam)
message.success('提交成功')
router.go(-1)
}
async function handleUploadChange ({ file, fileList }: { file: any, fileList: any[] }) {
const invalidNames: string[] = []
fileList.forEach(item => {
if (!item.file) return
const f = item.file
const ext = f.name.toLowerCase().split('.').pop() || ''
if (!fileExts.includes(ext)) {
invalidNames.push(f.name)
item.status = 'error'
}
if (invalidNames.length) {
message.error(`格式不支持:${invalidNames.join('、')}`)
}
const pendingList = fileList.filter(
item => item.status === 'pending' && item.file && item.status !== 'error'
)
if (pendingList.length === 0) return
const promises = pendingList.map(async (item) => {
try {
const res = await fileApi.uploadDingAttachment(item.file)
uploadFiles.value.push({ uid: item.id, ...res })
item.status = 'success'
item.percentage = 100
message.success("附件上传成功")
} catch (err) {
console.error('附件上传失败', err)
item.status = 'error'
}
})
// await Promise.all(promises)
// const successIds = fileList.filter(x => x.status === 'success').map(x => x.id)
// uploadFiles.value = uploadFiles.value.filter(att => successIds.includes(att.uid))
})
}
async function handleUploadImageChange({ file, fileList }: { file: any, fileList: any[] }) {
const invalidNames: string[] = []
fileList.forEach(item => {
if (!item.file) return
const f = item.file
const ext = f.name.toLowerCase().split('.').pop() || ''
if (!imageExts.includes(ext)) {
invalidNames.push(f.name)
item.status = 'error'
}
if (f.size > MAX_FILE_SIZE) {
message.error(`${f.name} 超出10MB大小限制`)
item.status = 'error'
}
})
if (invalidNames.length) {
message.error(`格式不支持:${invalidNames.join('、')}`)
}
// 2.
const pendingList = fileList.filter(
item => item.status === 'pending' && item.file && item.status !== 'error'
)
if (pendingList.length === 0) return
// 3.
const promises = pendingList.map(async (item) => {
try {
const res = await fileApi.uploadDingImage(item.file)
uploadImages.value.push({ uid: item.id, ...res })
item.status = 'success'
item.percentage = 100
message.success("上传成功")
} catch (err) {
console.error('图片上传失败', err)
item.status = 'error'
}
})
// await Promise.all(promises)
// const successIds = fileList.filter(x => x.status === 'success').map(x => x.id)
// uploadImages.value = uploadImages.value.filter(img => successIds.includes(img.uid))
}
function handleReset() {
Object.assign(formDetailOKData, { ...defaultFormDetailData, resultType: 1, handleAction: '下一道工序', actionStatus: 1 })
Object.assign(formDetailScrapData, { ...defaultFormDetailData, resultType: 2, handleAction: '报废次品', actionStatus: 1 })
Object.assign(formDetailReworkData, { ...defaultFormDetailData, resultType: 3, handleAction: '返工', actionStatus: 1 })
Object.assign(formDetailConcessionData, { ...defaultFormDetailData, resultType: 4, handleAction: '让步接收', actionStatus: 1 })
imageList.value = []
uploadImages.value = []
fileList.value = []
uploadFiles.value = []
message.info("表单已重置")
}
onMounted(()=>{
})
</script>
<style>
</style>

View File

@ -184,7 +184,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, h, onMounted } from 'vue' import { ref, reactive, h, onMounted } from 'vue'
import { NButton, NSpace, NIcon, NUpload, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions } from 'naive-ui' import { NButton, NSpace, NIcon,NTag, NUpload, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions } from 'naive-ui'
import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline, Options } from '@vicons/ionicons5' import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline, Options } from '@vicons/ionicons5'
import { submitLogApi, type SubmitLog } from '@/api/submitLog' import { submitLogApi, type SubmitLog } from '@/api/submitLog'
import { dictDataApi } from '@/api/org' import { dictDataApi } from '@/api/org'
@ -243,18 +243,35 @@ const formRules = {
// //
const columns: DataTableColumns<SubmitLog> = [ const columns: DataTableColumns<SubmitLog> = [
{ type: 'selection' }, { type: 'selection' },
{ title: '工序名称', key: 'processName' }, { title: '物料名称', key: 'materialName',align: 'center',
{ title: '工序编号', key: 'processCode' }, render(rowData, rowIndex) {
{ title: '派工编号', key: 'assingCode' }, return rowData.materialName? rowData.materialName: '-'
{ title: '派工人', key: 'userName' }, },
{ title: '派工状态', key: 'assingStatus', },
{ title: '物料编码', key: 'materialCode',align: 'center',
render(rowData, rowIndex) {
return rowData.materialCode? rowData.materialCode: '-'
},
},
{ title: '物料数量', key: 'materialQty',align: 'center',
render(rowData, rowIndex) {
return rowData.materialQty? rowData.materialQty: '-'
},
},
{ title: '工序名称', key: 'processName',align: 'center' },
{ title: '工序编号', key: 'processCode',align: 'center' },
{ title: '派工编号', key: 'assingCode',align: 'center' },
{ title: '派工人', key: 'userName',align: 'center' },
{ title: '派工状态', key: 'assingStatus',align: 'center',
render: (row) => { render: (row) => {
const val = row.assingStatus const val = row.assingStatus
const opt = qcAssingStatusOptions.value.find(o => o.value === val || String(o.value) === String(val)) const opt = qcAssingStatusOptions.value.find(o => o.value === val || String(o.value) === String(val))
return opt ? opt.label : (val ?? '-') if (!opt) return val ?? '-'
return h(NTag, { type: opt.class, size: 'small' }, { default: () => opt.label })
} }
}, },
{ title: '派工完成时间', key: 'completedTime'}, { title: '派工完成时间', key: 'completedTime',align: 'center'},
{ {
title: '操作', title: '操作',
key: 'actions', key: 'actions',
@ -286,7 +303,9 @@ const submitColumns = ref([
render: (row) => { render: (row) => {
const val = row.submitStatus const val = row.submitStatus
const opt = qcResultTypeOptions.value.find(o => o.value === val || String(o.value) === String(val)) const opt = qcResultTypeOptions.value.find(o => o.value === val || String(o.value) === String(val))
return opt ? opt.label : (val ?? '-') if (!opt) return val ?? '-'
return h(NTag, { type: opt.class, size: 'small' }, { default: () => opt.label })
} }
} }
]) ])
@ -474,19 +493,19 @@ async function handleImportUpload({ file }: UploadCustomRequestOptions) {
async function loadDictOptions() { async function loadDictOptions() {
try { try {
const data = await dictDataApi.listByType('qc_result_type') const data = await dictDataApi.listByType('qc_result_type')
qcResultTypeOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue) })) qcResultTypeOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class:d.listClass }))
}catch {} }catch {}
try { try {
const data = await dictDataApi.listByType('source_type') const data = await dictDataApi.listByType('source_type')
qcSourceTypeOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue) })) qcSourceTypeOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class:d.listClass }))
}catch {} }catch {}
try { try {
const data = await dictDataApi.listByType('qc_status') const data = await dictDataApi.listByType('qc_status')
qcStatusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue) })) qcStatusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class:d.listClass }))
}catch {} }catch {}
try { try {
const data = await dictDataApi.listByType('assing_status') const data = await dictDataApi.listByType('assing_status')
qcAssingStatusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue) })) qcAssingStatusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class:d.listClass }))
}catch {} }catch {}
} }

View File

@ -58,6 +58,18 @@
</div> </div>
</n-card> </n-card>
</div> </div>
<n-grid :cols="2" :x-gap="12">
<n-gi>
<n-card title="产出率" size="small" :bordered="false" content-style="background: transparent">
<n-progress type="circle" status="success" :percentage="stats.outputRate" />
</n-card>
</n-gi>
<n-gi>
<n-card title="报废率" size="small" :bordered="false" content-style="background: transparent">
<n-progress type="circle" status="error" :percentage="stats.scrapRate" />
</n-card>
</n-gi>
</n-grid>
<n-grid :cols="1" :x-gap="24" class="stats-charts"> <n-grid :cols="1" :x-gap="24" class="stats-charts">
<n-gi> <n-gi>
<n-card title="汇报统计图" size="small" :bordered="false" content-style="background: transparent"> <n-card title="汇报统计图" size="small" :bordered="false" content-style="background: transparent">
@ -73,7 +85,8 @@
import { ref, reactive, h, onMounted } from 'vue' import { ref, reactive, h, onMounted } from 'vue'
import { dictDataApi } from '@/api/org' import { dictDataApi } from '@/api/org'
import { submitLogApi } from '@/api/submitLog' import { submitLogApi } from '@/api/submitLog'
import { NButton, NSpace, NIcon,NTag, NUpload, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions } from 'naive-ui'
import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline, Options } from '@vicons/ionicons5'
// //
const statisticTypeOptions = ref<{ label: string; value: any }[]>([]) const statisticTypeOptions = ref<{ label: string; value: any }[]>([])
@ -114,6 +127,9 @@ async function loadStatistics() {
Object.assign(stats,res) Object.assign(stats,res)
console.log(stats);
updateCharts() updateCharts()
}catch{} }catch{}
} }

View File

@ -22,8 +22,7 @@
<div class="login-banner" :style="{ background: `linear-gradient(135deg, ${themeStore.primaryColor} 0%, ${adjustColor(themeStore.primaryColor, -30)} 100%)` }"> <div class="login-banner" :style="{ background: `linear-gradient(135deg, ${themeStore.primaryColor} 0%, ${adjustColor(themeStore.primaryColor, -30)} 100%)` }">
<div class="banner-content"> <div class="banner-content">
<div class="banner-logo"> <div class="banner-logo">
<img v-if="siteLogo" :src="siteLogo" class="logo-img" alt="Logo" /> <SiteLogoMark img-class="logo-img" icon-class="logo-icon" />
<div v-else class="logo-icon">{{ siteName.charAt(0) }}</div>
<span class="logo-text">{{ siteName }}</span> <span class="logo-text">{{ siteName }}</span>
</div> </div>
<h1 class="banner-title">{{ siteDescription || '后台管理系统' }}</h1> <h1 class="banner-title">{{ siteDescription || '后台管理系统' }}</h1>
@ -111,8 +110,7 @@
<div class="login-banner" :style="{ background: `linear-gradient(135deg, ${themeStore.primaryColor} 0%, ${adjustColor(themeStore.primaryColor, -30)} 100%)` }"> <div class="login-banner" :style="{ background: `linear-gradient(135deg, ${themeStore.primaryColor} 0%, ${adjustColor(themeStore.primaryColor, -30)} 100%)` }">
<div class="banner-content"> <div class="banner-content">
<div class="banner-logo"> <div class="banner-logo">
<img v-if="siteLogo" :src="siteLogo" class="logo-img" alt="Logo" /> <SiteLogoMark img-class="logo-img" icon-class="logo-icon" />
<div v-else class="logo-icon">{{ siteName.charAt(0) }}</div>
<span class="logo-text">{{ siteName }}</span> <span class="logo-text">{{ siteName }}</span>
</div> </div>
<h1 class="banner-title">{{ siteDescription || '后台管理系统' }}</h1> <h1 class="banner-title">{{ siteDescription || '后台管理系统' }}</h1>
@ -200,8 +198,11 @@
<div class="login-banner" style="background: transparent;"> <div class="login-banner" style="background: transparent;">
<div class="banner-content"> <div class="banner-content">
<div class="banner-logo"> <div class="banner-logo">
<img v-if="siteLogo" :src="siteLogo" class="logo-img" alt="Logo" /> <SiteLogoMark
<div v-else class="logo-icon" style="background: rgba(255, 255, 255, 0.15); color: #fff; border: 1px solid rgba(255, 255, 255, 0.2);">{{ siteName.charAt(0) }}</div> img-class="logo-img"
icon-class="logo-icon"
:icon-style="glassLogoIconStyle"
/>
<span class="logo-text">{{ siteName }}</span> <span class="logo-text">{{ siteName }}</span>
</div> </div>
<h1 class="banner-title" style="color: #fff;">{{ siteDescription || '后台管理系统' }}</h1> <h1 class="banner-title" style="color: #fff;">{{ siteDescription || '后台管理系统' }}</h1>
@ -350,6 +351,7 @@ import { useSiteStore } from '@/stores/site'
import { useThemeStore } from '@/stores/theme' import { useThemeStore } from '@/stores/theme'
import { authApi } from '@/api/auth' import { authApi } from '@/api/auth'
import { configGroupApi } from '@/api/org' import { configGroupApi } from '@/api/org'
import SiteLogoMark from '@/components/SiteLogoMark.vue'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@ -359,10 +361,14 @@ const siteStore = useSiteStore()
const themeStore = useThemeStore() const themeStore = useThemeStore()
// //
const siteName = computed(() => siteStore.siteName || 'CSY Admin') const siteName = computed(() => siteStore.siteName || 'MES系统')
const siteDescription = computed(() => siteStore.siteDescription || '伊特智造MES') const siteDescription = computed(() => siteStore.siteDescription || '伊特智造MES')
const siteLogo = computed(() => siteStore.siteLogo)
const copyright = computed(() => siteStore.copyright || '版权所有 © 河北伊特') const copyright = computed(() => siteStore.copyright || '版权所有 © 河北伊特')
const glassLogoIconStyle = {
background: 'rgba(255, 255, 255, 0.15)',
color: '#fff',
border: '1px solid rgba(255, 255, 255, 0.2)',
}
// //
const captchaEnabled = ref(false) const captchaEnabled = ref(false)

View File

@ -9,14 +9,14 @@
label-placement="left" label-placement="left"
> >
<n-grid :cols="24" :x-gap="24"> <n-grid :cols="24" :x-gap="24">
<n-form-item-gi :span="6" label="工序编码"> <!-- <n-form-item-gi :span="6" label="工序编码">
<n-input v-model:value="searchForm.processId" placeholder="请输入工序编号" clearable /> <n-input v-model:value="searchForm.processId" placeholder="请输入工序编号" clearable />
</n-form-item-gi> </n-form-item-gi> -->
<n-form-item-gi :span="6" label="工序名称"> <n-form-item-gi :span="6" label="工序名称">
<n-input v-model:value="searchForm.processName" placeholder="请输入工序名称" clearable /> <n-input v-model:value="searchForm.processName" placeholder="请输入工序名称" clearable />
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="6" label="工人名称"> <n-form-item-gi :span="6" label="工人名称">
<n-input v-model:value="searchForm.stringUserName" placeholder="请输入工人名称" clearable /> <n-input v-model:value="searchForm.userName" placeholder="请输入工人名称" clearable />
</n-form-item-gi> </n-form-item-gi>
<!-- <n-form-item-gi :span="6" label="数里"> <!-- <n-form-item-gi :span="6" label="数里">
<n-input v-model:value="searchForm.processId" placeholder="请输入数里" clearable /> <n-input v-model:value="searchForm.processId" placeholder="请输入数里" clearable />
@ -99,7 +99,7 @@
v-model:page="pagination.page" v-model:page="pagination.page"
v-model:page-size="pagination.pageSize" v-model:page-size="pagination.pageSize"
:item-count="pagination.itemCount" :item-count="pagination.itemCount"
:page-sizes="[10, 20, 50, 100]" :page-sizes="[2,10, 20, 50, 100]"
show-size-picker show-size-picker
show-quick-jumper show-quick-jumper
@update:page="handlePageChange" @update:page="handlePageChange"
@ -140,8 +140,14 @@
</n-form-item> </n-form-item>
</n-gi> </n-gi>
<n-gi> <n-gi>
<n-form-item label="工人名称" path="stringUserName"> <n-form-item label="改派工人" path="">
<n-input v-model:value="formData.stringUserName" placeholder="请选择工人" /> <n-select
v-model:value="reassform.userId"
placeholder="请指定改派的员工"
clearable
style="width: 200px"
:options="reassignEmployees"
/>
</n-form-item> </n-form-item>
</n-gi> </n-gi>
<n-gi> <n-gi>
@ -202,8 +208,15 @@
<n-input v-model:value="gxobg.name" disabled /> <n-input v-model:value="gxobg.name" disabled />
</n-form-item> </n-form-item>
<n-form-item label="工人名称" path=""> <n-form-item label="改派工人" path="">
<n-input v-model:value="reassform.userid" placeholder="请选择员工" /> <n-select
v-model:value="reassform.userId"
placeholder="请指定改派的员工"
clearable
style="width: 200px"
:options="reassignEmployees"
/>
</n-form-item> </n-form-item>
<n-form-item label="改派原因" path="reason"> <n-form-item label="改派原因" path="reason">
<n-input type="textarea" v-model:value="reassform.reason" placeholder="请输入改派原因" /> <n-input type="textarea" v-model:value="reassform.reason" placeholder="请输入改派原因" />
@ -259,6 +272,76 @@
</template> </template>
</n-modal> </n-modal>
<!--查询汇报详情-->
<n-modal v-model:show="reportDetailVisible" preset="card" :title="reportDetailTitle" style="width: 1000px">
<n-card hoverable style="margin-bottom: 12px" >
<template #header>
<div style="display:flex;justify-content:space-between;align-items:center;width:100%">
<span>主表信息</span>
<n-button text @click="infoIsCollapse = !infoIsCollapse">
<n-icon :component="infoIsCollapse ? ArrowDownOutline : ArrowUpOutline" />
</n-button>
</div>
</template>
<n-descriptions :column="3" bordered v-show="!infoIsCollapse">
<n-descriptions-item
v-for="item in mappedInfoList"
:key="item.key"
:label="item.label"
>
{{ formatDisplayValue(item.key, item.value) }}
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card title="汇报信息" hoverable style="margin-bottom: 12px" >
<div v-if="qualityTestings.length === 0" style="height:520px;display:flex;align-items:center;justify-content:center;color:#999">
暂无质检单据数据
</div>
<n-tabs
type="line"
animated
placement="left"
style="height: 520px"
v-else>
<n-tab-pane
v-for="item in qualityTestings"
:name="item.qcNo"
:tab="item.qcNo">
<n-card title="质检基础信息" style="margin-bottom:12px">
<n-descriptions :column="3">
<n-descriptions-item label="资源类型">{{formatDisplayValue("resourceType",item.resourceType) }}</n-descriptions-item>
<n-descriptions-item label="质检状态">{{formatDisplayValue("status",item.qcStatus) }}</n-descriptions-item>
<n-descriptions-item label="总数量">{{ item.totalQty }}</n-descriptions-item>
</n-descriptions>
</n-card>
<!--汇报表格-->
<n-card title="汇报列表">
<n-data-table
:columns="submitColumns"
:data="item.submitLogs"
:scroll-x="700"
max-height="320"
:pagination="{ pageSize:3 }"
>
<!--空数据提示插槽-->
<template #empty>
<div style="padding:40px;text-align:center;color:#999">
当前没有数据
</div>
</template>
</n-data-table>
</n-card>
</n-tab-pane>
</n-tabs>
</n-card>
</n-modal>
</div> </div>
</template> </template>
@ -268,11 +351,13 @@ import {
reactive, reactive,
h, h,
onMounted, onMounted,
render,
} from 'vue' } from 'vue'
import { import {
NButton, NButton,
NDropdown,
NIcon, NIcon,
NSpace, NSpace,
NPagination, NPagination,
@ -283,14 +368,12 @@ import {
//useDialog, //useDialog,
type FormInst, type FormInst,
} from 'naive-ui' } from 'naive-ui'
import { import {
SearchOutline, SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline,
RefreshOutline EllipsisHorizontalOutline, ArrowDownOutline, ArrowUpOutline
} from '@vicons/ionicons5' } from '@vicons/ionicons5'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { dictDataApi } from '@/api/org'
import { import {
disworlist, disworlist,
editdiswor, editdiswor,
@ -299,6 +382,10 @@ import {
//workload //workload
} from '@/api/production' } from '@/api/production'
import { submitLogApi } from '@/api/submitLog'
import { SysUser, userApi } from '@/api/system'
//const dialog = useDialog() //const dialog = useDialog()
const message = useMessage() const message = useMessage()
@ -308,14 +395,51 @@ const userStore = useUserStore()
// //
const hasPermission = (permission: string) => userStore.hasPermission(permission) const hasPermission = (permission: string) => userStore.hasPermission(permission)
//
const qcResultTypeOptions = ref<{ label: string; value: any }[]>([])
//
const qcSourceTypeOptions = ref<{ label: string; value: any }[]>([])
//
const qcStatusOptions = ref<{ label: string; value: any }[]>([])
//
const qcAssingStatusOptions = ref<{ label: string; value: any }[]>([])
//
const infoIsCollapse = ref(false)
const reportDetailVisible = ref(false)
const reportDetailTitle = ref<string>('')
const mappedInfoList = ref<Array<{ label: string; key: string; value: any }>>([])
const qualityTestings = ref<any[]>([])
const submitColumns = ref([
{ title: "提交人", key: "sumbitUser" },
{ title: "提交时间", key: "submitCreateTime" },
{ title: "提交数量", key: "quantity" },
{ title: "单据状态", key: "submitStatus",
render: (row) => {
const val = row.submitStatus
const opt = qcResultTypeOptions.value.find(o => o.value === val || String(o.value) === String(val))
if (!opt) return val ?? '-'
return h(NTag, { type: opt.class, size: 'small' }, { default: () => opt.label })
}
}
])
//
const reassignEmployees = ref<Array<{value: number, username: string}>>([])
// ==================== ==================== // ==================== ====================
const searchForm = reactive({ const searchForm = reactive({
page: 1, page: 1,
pageSize: 20, pageSize: 20,
processId:'', processId:null ,
processName:'', processName:null,
stringUserName:'' userName:null
}) })
@ -331,16 +455,40 @@ const pagination = reactive({
itemCount: 0 itemCount: 0
}) })
const columns = [ const columns = [
// { // {
// type: 'selection', // type: 'selection',
// minWidth:60 // minWidth:60
// }, // },
{ {
align:'center', align:"center",
title: '工序编号', title:"物料名称",
key: 'processId', key:"materialName",
minWidth: 150 minWidth:150,
render(row:any) {
var opt = row.materialName
if(opt) return opt
return '-'
}
},
{
align:"center",
title:"物料编码",
key:"materialCode",
minWidth:150,
render(row:any) {
var opt = row.materialCode
if(opt) return opt
return '-'
}
},
{
align:"center",
title:"派工编号",
key:"assingCode",
minWidth:150
}, },
{ {
align:'center', align:'center',
@ -354,27 +502,19 @@ const columns = [
key: 'assingStatus', key: 'assingStatus',
minWidth: 150, minWidth: 150,
render(row:any) { render(row:any) {
let text = '' const val = row.assingStatus
if(row.assingStatus == 0){ const opt = qcAssingStatusOptions.value.find(o => o.value === val || String(o.value) === String(val))
text = '待执行' if (!opt) return val ?? '-'
}else if(row.assingStatus == 1){
text = '执行中' return h(NTag, { type: opt.class, size: 'small' }, { default: () => opt.label })
}else if(row.assingStatus == 2){
text = '质检中'
}else if(row.assingStatus == 3){
text = '已汇报'
}else if(row.assingStatus == 4){
text = '已关闭'
} }
return text
}
//0 1 2+34
}, },
{ {
align:'center', align:'center',
title: '工人名称', title: '工人名称',
key: 'stringUserName', key: 'userName',
minWidth: 200 minWidth: 200
}, },
{ {
@ -389,7 +529,7 @@ const columns = [
key: 'rework', key: 'rework',
minWidth: 100, minWidth: 100,
render(row:any) { render(row:any) {
const quit = row.rework === 3 const quit = row.rework === 1
return h(NTag, {type: quit ? 'error' : 'success', size: 'small'}, {default: () => (quit ? '是' : '否')}) return h(NTag, {type: quit ? 'error' : 'success', size: 'small'}, {default: () => (quit ? '是' : '否')})
} }
}, },
@ -429,9 +569,14 @@ const columns = [
}, },
{ {
align:'center', align:'center',
title: '质检打回数', title: '质检打回数',
key: 'qualityReturnNum', key: 'qualityReturnNum',
minWidth: 150 minWidth: 150,
render(row:any) {
var count = row.qualityReturnNum
if(count>0) return count
return 0
}
}, },
{ {
align:'center', align:'center',
@ -461,7 +606,7 @@ const columns = [
} }
if(row.status == 2){ if(row.status == 2){
return h(NTag, { return h(NTag, {
type:'warning', type:'error',
size: 'small' size: 'small'
}, },
{ {
@ -491,52 +636,79 @@ const columns = [
width: 200, width: 200,
fixed: 'right', fixed: 'right',
render(row:any) { render(row:any) {
const buttons:any = [ 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(NDropdown, {
trigger:'click',
options:[
{label:"改派",key:'transfer'},
{type: 'divider' }, // 线
{label: '启用/暂停',key:'pauseResume'},
{type: 'divider' }, // 线
{label:'查看汇报',key:'view'},
],
onSelect:(key:string)=>{
switch(key) {
case "transfer":
handlreass(row)
break
case "pauseResume":
handlpause(row)
break
case "view":
handleViewReport(row.id)
break
// h(NButton, { }
}
},{
// /
default: () => h(NButton, { size: 'small', quaternary: true }, {
default: () => [h(NIcon, null, { default: () => h(EllipsisHorizontalOutline) })]
})
})
])
// const buttons:any = [
// ]
// if(hasPermission('biz:assingWork: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:assingWork:transfer')){
// buttons.push(h(NButton, {
// size: 'small',
// type:'success',
// ghost:true,
// onClick: () => { handlreass(row) } },
// { default: () => ''}
// ))
// }
// if(hasPermission('biz:assingWork:pauseResume')){
// buttons.push(h(NButton, {
// size: 'small', // size: 'small',
// type:'warning', // type:'warning',
// ghost:true, // ghost:true,
// onClick: () => handleEdit(row) }, // onClick:()=>{ handlpause(row) }
// //{ default: () => [h(NIcon, null, { default: () => h(CreateOutline) }), ' ' ]} // },
// { default: () => ''} // { default: () => ''}
// ) // ))
] // }
if(hasPermission('biz:assingWork: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:assingWork:transfer')){
buttons.push(h(NButton, {
size: 'small',
type:'success',
ghost:true,
onClick: () => { handlreass(row) } },
{ default: () => '改派'}
))
}
if(hasPermission('biz:assingWork:pauseResume')){
buttons.push(h(NButton, {
size: 'small',
type:'warning',
ghost:true,
onClick:()=>{ handlpause(row) }
},
{ default: () => '暂停'}
))
}
// buttons.push( // buttons.push(
// ) // )
return buttons.length > 0 ? h(NSpace, {justify:'center'}, { default: () => buttons }) : '-' // return buttons.length > 0 ? h(NSpace, {justify:'center'}, { default: () => buttons }) : '-'
} }
} }
] ]
@ -572,9 +744,11 @@ function search() {
// //
function reset() { function reset() {
searchForm.processId ='' searchForm.processId =null
searchForm.processName = '' searchForm.processName = null
searchForm.stringUserName = '' searchForm.userName = null
} }
// //
@ -651,9 +825,9 @@ let reassform = reactive<any>({
// userId:'', //id // userId:'', //id
// stringUserName:'', // // stringUserName:'', //
id:'', //id id:null, //id
reason:'', // reason:null, //
userid:456 //Id userId:null //Id
}) })
const reassrules = { const reassrules = {
userid: [{ required: true, message: '请选择员工', trigger: 'blur' }] userid: [{ required: true, message: '请选择员工', trigger: 'blur' }]
@ -722,8 +896,7 @@ function handlpause(n:any) {
pauseformRef.value?.restoreValidation() pauseformRef.value?.restoreValidation()
pauseform.id = n.id pauseform.id = n.id
pauseform.reason = '' pauseform.reason = ''
pauseform.status = n.staus pauseform.status = n.status === '0'?'1':'0'
} }
function pauseSubmit() { function pauseSubmit() {
@ -733,12 +906,10 @@ function pauseSubmit() {
pauseisedbtn.value = true pauseisedbtn.value = true
pauseresume(pauseform).then(() => { pauseresume(pauseform).then(() => {
message.success('操作成功!') message.success('操作成功!')
setTimeout(()=> {
pausemodal.value = false pausemodal.value = false
pauseLoading.value = false pauseLoading.value = false
pauseisedbtn.value = false pauseisedbtn.value = false
getlist() getlist()
},1000)
}).catch(() => { }).catch(() => {
pauseLoading.value = false pauseLoading.value = false
pauseisedbtn.value = false pauseisedbtn.value = false
@ -747,23 +918,94 @@ function pauseSubmit() {
}) })
} }
// function zantqy(row:any) { async function handleViewReport(id:number) {
// dialog.warning({ reportDetailTitle.value='汇报详情'
// title: '', reportDetailVisible.value = true
// content: `"${row.processName}"`,
// positiveText: '', const res = await submitLogApi.getByDispatch(id);
// negativeText: '',
// onPositiveClick: () => { mappedInfoList.value = [
// pauseresume({}).then(() => { {key:"materialCode",label:"物料编码",value:res.materialCode},
// message.success('!') {key:"materialName",label:"物料名称",value:res.materialName},
// getlist() {key:"materialQty",label:"物料数量",value:res.materialQty},
// }) {key:"processCode",label:"工序编号",value:res.processCode},
// } {key:"processName",label:"工序名称",value:res.processName},
// }) {key:"processQty",label:"生产总数",value:res.processQty},
// } {key:"assingCode",label:"派工编号",value:res.assingCode},
{key:"workerName",label:"派工人",value:res.workerName},
{key:"assingQty",label:"派工数量",value:res.assingQty}
]
qualityTestings.value = res.qualityTestings
}
function formatDisplayValue(key: string, value: any): string {
if (value === null || value === undefined) return '-'
//
switch (key) {
case 'resourceType': {
const sourceOpt = qcSourceTypeOptions.value.find(o => o.value === value || String(o.value) === String(value))
return sourceOpt ? sourceOpt.label : String(value)
}
case 'status': {
const statusOpt = qcStatusOptions.value.find(o => o.value === value || String(o.value) === String(value))
return statusOpt ? statusOpt.label : String(value)
}
case 'qcNo':
return value ? new Date(value).toLocaleString('zh-CN') : '-'
case 'inspectTime':
case 'createTime':
case 'updateTime':
return value ? new Date(value).toLocaleString('zh-CN') : '-'
default:
return String(value)
}
}
//
async function loadDictOptions() {
try {
const data = await dictDataApi.listByType('qc_result_type')
qcResultTypeOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class:d.listClass }))
}catch {}
try {
const data = await dictDataApi.listByType('source_type')
qcSourceTypeOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class:d.listClass }))
}catch {}
try {
const data = await dictDataApi.listByType('qc_status')
qcStatusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class:d.listClass }))
}catch {}
try {
const data = await dictDataApi.listByType('assing_status')
qcAssingStatusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class:d.listClass }))
}catch {}
}
//
async function getUserList() {
const res = await userApi.getPathList();
reassignEmployees.value = res.map((user:SysUser)=>(
{
label: user.username,
value: user.id
}
))
}
onMounted(() => { onMounted(() => {
getlist() getlist()
getUserList()
loadDictOptions()
//workload() //workload()
}) })

View File

@ -9,9 +9,9 @@
label-placement="left" label-placement="left"
> >
<n-grid :cols="24" :x-gap="24"> <n-grid :cols="24" :x-gap="24">
<n-form-item-gi :span="6" label="工序编码"> <!-- <n-form-item-gi :span="6" label="工序编码">
<n-input v-model:value="searchForm.processId" placeholder="请输入工序编号" clearable /> <n-input v-model:value="searchForm.processId" placeholder="请输入工序编号" clearable />
</n-form-item-gi> </n-form-item-gi> -->
<n-form-item-gi :span="6" label="工序名称"> <n-form-item-gi :span="6" label="工序名称">
<n-input v-model:value="searchForm.processName" placeholder="请输入工序名称" clearable /> <n-input v-model:value="searchForm.processName" placeholder="请输入工序名称" clearable />
</n-form-item-gi> </n-form-item-gi>
@ -182,7 +182,7 @@ import {
} from '@vicons/ionicons5' } from '@vicons/ionicons5'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { dictDataApi } from '@/api/org'
import { import {
mytasks, mytasks,
inspectio inspectio
@ -198,6 +198,9 @@ const userStore = useUserStore()
const hasPermission = (permission: string) => userStore.hasPermission(permission) const hasPermission = (permission: string) => userStore.hasPermission(permission)
//
const qcAssingStatusOptions = ref<{ label: string; value: any }[]>([])
// ==================== ==================== // ==================== ====================
const searchForm = reactive({ const searchForm = reactive({
page: 1, page: 1,
@ -225,10 +228,32 @@ const columns = [
// minWidth:60 // minWidth:60
// }, // },
{ {
align:'center', align:"center",
title: '工序编号', title:"物料名称",
key: 'processId', key:"materialName",
minWidth: 150 minWidth:150,
render(row:any) {
var opt = row.materialName
if(opt) return opt
return '-'
}
},
{
align:"center",
title:"物料编码",
key:"materialCode",
minWidth:150,
render(row:any) {
var opt = row.materialCode
if(opt) return opt
return '-'
}
},
{
align:"center",
title:"派工编号",
key:"assingCode",
minWidth:150
}, },
{ {
align:'center', align:'center',
@ -242,27 +267,19 @@ const columns = [
key: 'assingStatus', key: 'assingStatus',
minWidth: 150, minWidth: 150,
render(row:any) { render(row:any) {
let text = '' const val = row.assingStatus
if(row.assingStatus == 0){ const opt = qcAssingStatusOptions.value.find(o => o.value === val || String(o.value) === String(val))
text = '待执行' if (!opt) return val ?? '-'
}else if(row.assingStatus == 1){
text = '执行中' return h(NTag, { type: opt.class, size: 'small' }, { default: () => opt.label })
}else if(row.assingStatus == 2){
text = '质检中'
}else if(row.assingStatus == 3){
text = '已汇报'
}else if(row.assingStatus == 4){
text = '已关闭'
}
return text
} }
//0 1 2+34 //0 1 2+34
}, },
{ {
align:'center', align:'center',
title: '工人名称', title: '工人名称',
key: 'stringUserName', key: 'userName',
minWidth: 200 minWidth: 200
}, },
{ {
@ -277,7 +294,7 @@ const columns = [
key: 'rework', key: 'rework',
minWidth: 100, minWidth: 100,
render(row:any) { render(row:any) {
const quit = row.rework === 3 const quit = row.rework === 1
return h(NTag, {type: quit ? 'error' : 'success', size: 'small'}, {default: () => (quit ? '是' : '否')}) return h(NTag, {type: quit ? 'error' : 'success', size: 'small'}, {default: () => (quit ? '是' : '否')})
} }
}, },
@ -418,7 +435,7 @@ function reset() {
// //
function getlist() { function getlist() {
mytasks(searchForm).then((rps:any) => { mytasks(searchForm).then((rps:any) => {
datalist.value = rps.list datalist.value = rps.records
pagination.itemCount = rps.total pagination.itemCount = rps.total
}) })
} }
@ -506,6 +523,14 @@ function pauseSubmit() {
}) })
} }
//
async function loadDictOptions() {
try {
const data = await dictDataApi.listByType('assing_status')
qcAssingStatusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class:d.listClass }))
}catch {}
}
// function zantqy(row:any) { // function zantqy(row:any) {
// dialog.warning({ // dialog.warning({
// title: '', // title: '',
@ -524,6 +549,7 @@ onMounted(() => {
getlist() getlist()
loadDictOptions()
}) })
</script> </script>

View File

@ -5,8 +5,7 @@
<div class="register-banner" :style="{ background: `linear-gradient(135deg, ${themeStore.primaryColor} 0%, ${adjustColor(themeStore.primaryColor, -30)} 100%)` }"> <div class="register-banner" :style="{ background: `linear-gradient(135deg, ${themeStore.primaryColor} 0%, ${adjustColor(themeStore.primaryColor, -30)} 100%)` }">
<div class="banner-content"> <div class="banner-content">
<div class="banner-logo"> <div class="banner-logo">
<img v-if="siteLogo" :src="siteLogo" class="logo-img" alt="Logo" /> <SiteLogoMark img-class="logo-img" icon-class="logo-icon" />
<div v-else class="logo-icon">{{ siteName.charAt(0) }}</div>
<span class="logo-text">{{ siteName }}</span> <span class="logo-text">{{ siteName }}</span>
</div> </div>
<h1 class="banner-title">加入我们</h1> <h1 class="banner-title">加入我们</h1>
@ -116,6 +115,7 @@ import { useSiteStore } from '@/stores/site'
import { useThemeStore } from '@/stores/theme' import { useThemeStore } from '@/stores/theme'
import { authApi } from '@/api/auth' import { authApi } from '@/api/auth'
import { configGroupApi } from '@/api/org' import { configGroupApi } from '@/api/org'
import SiteLogoMark from '@/components/SiteLogoMark.vue'
const router = useRouter() const router = useRouter()
const message = useMessage() const message = useMessage()
@ -124,7 +124,6 @@ const themeStore = useThemeStore()
// //
const siteName = computed(() => siteStore.siteName || 'CSY Admin') const siteName = computed(() => siteStore.siteName || 'CSY Admin')
const siteLogo = computed(() => siteStore.siteLogo)
const copyright = computed(() => siteStore.copyright || '版权所有 © 成都火星网络科技有限公司 2025-2030') const copyright = computed(() => siteStore.copyright || '版权所有 © 成都火星网络科技有限公司 2025-2030')
// //

View File

@ -24,7 +24,7 @@
<n-form-item label="站点Logo"> <n-form-item label="站点Logo">
<div class="logo-upload"> <div class="logo-upload">
<div class="logo-preview" v-if="configs.system.siteLogo"> <div class="logo-preview" v-if="configs.system.siteLogo">
<img :src="configs.system.siteLogo" alt="Logo" /> <SiteLogoMark :src="configs.system.siteLogo" />
<n-button size="small" quaternary type="error" class="logo-delete" @click="configs.system.siteLogo = ''"> <n-button size="small" quaternary type="error" class="logo-delete" @click="configs.system.siteLogo = ''">
<template #icon><n-icon><CloseOutline /></n-icon></template> <template #icon><n-icon><CloseOutline /></n-icon></template>
</n-button> </n-button>
@ -938,6 +938,7 @@ import { configGroupApi, type SysConfigGroup, type SmsLog } from '@/api/org'
import { fileApi } from '@/api/system' import { fileApi } from '@/api/system'
import { wechatApi } from '@/api/wechat' import { wechatApi } from '@/api/wechat'
import { useSiteStore } from '@/stores/site' import { useSiteStore } from '@/stores/site'
import SiteLogoMark from '@/components/SiteLogoMark.vue'
const message = useMessage() const message = useMessage()
const siteStore = useSiteStore() const siteStore = useSiteStore()
@ -1812,6 +1813,7 @@ async function loadWechatMpMenu() {
position: relative; position: relative;
display: inline-block; display: inline-block;
:deep(.logo-img),
img { img {
max-width: 200px; max-width: 200px;
max-height: 80px; max-height: 80px;
@ -1822,6 +1824,20 @@ async function loadWechatMpMenu() {
background: #f9fafb; background: #f9fafb;
} }
:deep(.logo-icon) {
width: 64px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
font-weight: 700;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #f9fafb;
color: #111827;
}
.logo-delete { .logo-delete {
position: absolute; position: absolute;
top: -8px; top: -8px;