This commit is contained in:
andy 2026-08-03 13:34:16 +08:00
commit d80a5983d4
28 changed files with 1089 additions and 1639 deletions

View File

@ -1,82 +0,0 @@
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

@ -1,80 +0,0 @@
import { request } from '@/utils/request'
// MES设备维修保养记录表 类型定义
export interface MaintenanceRecord {
recordId?: string
equipmentId?: string
recordType?: string
itemContent?: string
handleContent?: string
executorId?: number
startTime?: string
endTime?: string
recordStatus?: string
createTime?: string
updateTime?: string
}
// MES设备维修保养记录表 API
export const maintenanceRecordApi = {
// 分页查询
page(params: { page: number; pageSize: number; recordId?: string }) {
return request({ url: '/biz/maintenanceRecord/page', method: 'get', params })
},
// 获取详情
detail(recordId: string) {
return request({ url: `/biz/maintenanceRecord/${recordId}`, method: 'get' })
},
// 新增
create(data: MaintenanceRecord) {
return request({ url: '/biz/maintenanceRecord', method: 'post', data })
},
// 修改
update(data: MaintenanceRecord) {
return request({ url: '/biz/maintenanceRecord', method: 'put', data })
},
// 删除
delete(recordIds: string[]) {
return request({ url: `/biz/maintenanceRecord/${recordIds.join(',')}`, method: 'delete' })
},
// 导出
export(params?: { ids?: string[]; recordId?: string }) {
const p: Record<string, any> = {}
if (params?.ids?.length) p.ids = params.ids.join(',')
if (params?.recordId !== undefined && params?.recordId !== null) p.recordId = params.recordId
return request({ url: `/biz/maintenanceRecord/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/maintenanceRecord/import`,
method: 'post',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
})
},
// 下载导入模板
downloadTemplate() {
return request({ url: `/biz/maintenanceRecord/template`, method: 'get', responseType: 'blob' })
}
}

View File

@ -1,100 +0,0 @@
import { request } from '@/utils/request'
// 委外记录表 类型定义
export interface Outsourcing {
id?: number
name?: string
code?: string
businessType?: number
businessId?: number
businessCode?: string
orderItemId?: number
quantity?: number
supplierId?: number
supplierName?: string
unitPrice?: string
totalAmount?: string
beginPreparationTime?: string
endPreparationTime?: string
beginTime?: string
endTime?: string
remark?: string
createTime?: string
createBy?: string
overdue?: number
}
// 委外记录表 API
export const outsourcingApi = {
// 分页查询
page(params: { page: number; pageSize: number; id?: number; name?: string; businessType?: number }) {
return request({ url: '/biz/outsourcing/page', method: 'get', params })
},
// 获取详情
detail(id: string) {
return request({ url: `/biz/outsourcing/${id}`, method: 'get' })
},
// 新增
create(data: Outsourcing) {
return request({ url: '/biz/outsourcing', method: 'post', data })
},
// 修改
update(data: Outsourcing) {
return request({ url: '/biz/outsourcing', method: 'put', data })
},
// 删除
delete(ids: string[]) {
return request({ url: `/biz/outsourcing/${ids.join(',')}`, method: 'delete' })
},
// 导出
export(params?: { ids?: string[]; id?: number; name?: string; businessType?: number }) {
const p: Record<string, any> = {}
if (params?.ids?.length) p.ids = params.ids.join(',')
if (params?.id !== undefined && params?.id !== null) p.id = params.id
if (params?.name !== undefined && params?.name !== null) p.name = params.name
if (params?.businessType !== undefined && params?.businessType !== null) p.businessType = params.businessType
return request({ url: `/biz/outsourcing/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/outsourcing/import`,
method: 'post',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
})
},
// 下载导入模板
downloadTemplate() {
return request({ url: `/biz/outsourcing/template`, method: 'get', responseType: 'blob' })
}
}

View File

@ -1,441 +0,0 @@
<template>
<div class="page-container">
<n-card>
<!-- 搜索表单 -->
<div class="search-form">
<n-form inline :model="searchForm" label-placement="left">
<n-form-item label="维保记录编号(后端代码自动生成,唯一主键)">
<n-input v-model:value="searchForm.recordId" placeholder="请输入维保记录编号(后端代码自动生成,唯一主键)" 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.recordId"
:scroll-x="1200"
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
@update:checked-row-keys="handleCheck"
/>
</n-card>
<!-- 新增/编辑弹窗 -->
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 600px">
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="100px">
<n-form-item label="设备ID关联mes_equipment设备表主键" path="equipmentId">
<n-input v-model:value="formData.equipmentId" placeholder="请输入设备ID关联mes_equipment设备表主键" />
</n-form-item>
<n-form-item label="记录类型maintain=保养repair=维修" path="recordType">
<n-select v-model:value="formData.recordType" placeholder="请选择记录类型maintain=保养repair=维修" :options="[]" />
</n-form-item>
<n-form-item label="故障现象/保养项目详情" path="itemContent">
<n-input v-model:value="formData.itemContent" type="textarea" placeholder="请输入故障现象/保养项目详情" />
</n-form-item>
<n-form-item label="维修处理措施/保养执行内容" path="handleContent">
<n-input v-model:value="formData.handleContent" type="textarea" placeholder="请输入维修处理措施/保养执行内容" />
</n-form-item>
<n-form-item label="执行人ID关联sys_user系统用户表主键" path="executorId">
<n-input v-model:value="formData.executorId" placeholder="请输入执行人ID关联sys_user系统用户表主键" />
</n-form-item>
<n-form-item label="维保作业开始时间" path="startTime">
<n-date-picker v-model:value="formData.startTime" type="datetime" clearable style="width: 100%" />
</n-form-item>
<n-form-item label="维保作业结束时间,待处理单据为空" path="endTime">
<n-date-picker v-model:value="formData.endTime" type="datetime" clearable style="width: 100%" />
</n-form-item>
<n-form-item label="单据状态pending=待处理finished=已完成" path="recordStatus">
<n-select v-model:value="formData.recordStatus" placeholder="请选择单据状态pending=待处理finished=已完成" :options="recordStatusOptions" />
</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="导入MES设备维修保养记录表" 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 { maintenanceRecordApi, type MaintenanceRecord } from '@/api/maintenanceRecord'
import { dictDataApi } from '@/api/org'
const message = useMessage()
const dialog = useDialog()
//
const searchForm = reactive({
recordId: '',
})
//
const tableData = ref<MaintenanceRecord[]>([])
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: MaintenanceRecord = {
equipmentId: '',
recordType: '',
itemContent: '',
handleContent: '',
executorId: undefined,
startTime: undefined,
endTime: undefined,
recordStatus: '',
}
const formData = reactive<MaintenanceRecord>({ ...defaultFormData })
// //使
const recordStatusOptions = ref<{ label: string; value: any }[]>([])
//
const formRules = {
equipmentId: { required: true, message: '请输入设备ID关联mes_equipment设备表主键', trigger: 'blur' },
recordType: { required: true, message: '请输入记录类型maintain=保养repair=维修', trigger: 'blur' },
itemContent: { required: true, message: '请输入故障现象/保养项目详情', trigger: 'blur' },
handleContent: { required: true, message: '请输入维修处理措施/保养执行内容', trigger: 'blur' },
}
//
const columns: DataTableColumns<MaintenanceRecord> = [
{ type: 'selection' },
{ title: '维保记录编号(后端代码自动生成,唯一主键)', key: 'recordId' },
{ title: '设备ID关联mes_equipment设备表主键', key: 'equipmentId' },
{ title: '记录类型maintain=保养repair=维修', key: 'recordType' },
{ title: '故障现象/保养项目详情', key: 'itemContent' },
{ title: '维修处理措施/保养执行内容', key: 'handleContent' },
{ title: '执行人ID关联sys_user系统用户表主键', key: 'executorId' },
{ title: '维保作业开始时间', key: 'startTime' },
{ title: '维保作业结束时间,待处理单据为空', key: 'endTime' },
{ title: '单据状态pending=待处理finished=已完成', key: 'recordStatus',
render(row) {
const val = row.recordStatus
const opt = recordStatusOptions.value.find(o => o.value === val || String(o.value) === String(val))
return opt ? opt.label : (val ?? '-')
}
},
{ title: '单据创建时间', key: 'createTime', width: 180 },
{ title: '单据最后更新时间', key: 'updateTime', width: 180 },
{
title: '操作',
key: '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 maintenanceRecordApi.page({
page: pagination.page,
pageSize: pagination.pageSize,
recordId: searchForm.recordId || undefined,
})
tableData.value = res.list
pagination.itemCount = res.total
} finally {
loading.value = false
}
}
//
function handleSearch() {
pagination.page = 1
loadData()
}
//
function handleReset() {
searchForm.recordId = ''
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 = '新增MES设备维修保养记录表'
Object.assign(formData, defaultFormData)
modalVisible.value = true
}
//
function handleEdit(row: MaintenanceRecord) {
modalTitle.value = '编辑MES设备维修保养记录表'
Object.assign(formData, row)
if (formData.startTime && typeof formData.startTime === 'string') {
formData.startTime = new Date(formData.startTime.replace(' ', 'T')).getTime()
}
if (formData.endTime && typeof formData.endTime === 'string') {
formData.endTime = new Date(formData.endTime.replace(' ', 'T')).getTime()
}
if (formData.createTime && typeof formData.createTime === 'string') {
formData.createTime = new Date(formData.createTime.replace(' ', 'T')).getTime()
}
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 MaintenanceRecord
if (typeof submitData.startTime === 'number') {
submitData.startTime = new Date(submitData.startTime).toISOString().slice(0, 19).replace('T', ' ')
}
if (typeof submitData.endTime === 'number') {
submitData.endTime = new Date(submitData.endTime).toISOString().slice(0, 19).replace('T', ' ')
}
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.recordId) {
await maintenanceRecordApi.update(submitData)
message.success('修改成功')
} else {
await maintenanceRecordApi.create(submitData)
message.success('新增成功')
}
modalVisible.value = false
loadData()
} catch (error) {
//
}
}
//
function handleDelete(row: MaintenanceRecord) {
dialog.warning({
title: '提示',
content: '确定要删除该记录吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
await maintenanceRecordApi.delete([row.recordId!])
message.success('删除成功')
loadData()
} catch (error) {
//
}
}
})
}
//
function handleBatchDelete() {
dialog.warning({
title: '提示',
content: `确定要删除选中的 ${selectedIds.value.length} 条记录吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
await maintenanceRecordApi.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.recordId) params.recordId = searchForm.recordId
const blob = await maintenanceRecordApi.export(params)
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = 'MES设备维修保养记录表数据.xlsx'
link.click()
window.URL.revokeObjectURL(url)
} catch (error) {
//
}
}
//
async function handleDownloadTemplate() {
try {
const blob = await maintenanceRecordApi.downloadTemplate()
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = 'MES设备维修保养记录表导入模板.xlsx'
link.click()
window.URL.revokeObjectURL(url)
} catch (error) {
//
}
}
//
async function handleImportUpload({ file }: UploadCustomRequestOptions) {
if (!file.file) return
try {
const result = await maintenanceRecordApi.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')
recordStatusOptions.value = data.map(d => ({ label: d.dictLabel, value: d.dictValue }))
} catch {}
}
onMounted(() => {
loadData()
loadDictOptions()
})
</script>
<style scoped>
.search-form {
margin-bottom: 16px;
}
.table-toolbar {
margin-bottom: 16px;
}
</style>

View File

@ -1,492 +0,0 @@
<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 label="委外产品名称">
<n-input v-model:value="searchForm.name" placeholder="请输入委外产品名称" clearable />
</n-form-item>
<n-form-item label="业务类型 1工序委外">
<n-select v-model:value="searchForm.businessType" placeholder="请选择业务类型 1工序委外" clearable style="width: 150px" :options="[]" />
</n-form-item>
<n-form-item>
<n-space>
<n-button type="primary" @click="handleSearch">
<template #icon><n-icon><SearchOutline /></n-icon></template>
搜索
</n-button>
<n-button @click="handleReset">
<template #icon><n-icon><RefreshOutline /></n-icon></template>
重置
</n-button>
</n-space>
</n-form-item>
</n-form>
</div>
<!-- 工具栏 -->
<div class="table-toolbar">
<n-space>
<n-button type="primary" @click="handleAdd">
<template #icon><n-icon><AddOutline /></n-icon></template>
新增
</n-button>
<n-button @click="importModalVisible = true">
<template #icon><n-icon><CloudUploadOutline /></n-icon></template>
导入
</n-button>
<n-button @click="handleExport">
<template #icon><n-icon><DownloadOutline /></n-icon></template>
导出{{ selectedIds.length > 0 ? `(${selectedIds.length})` : '' }}
</n-button>
<n-button type="error" :disabled="selectedIds.length === 0" @click="handleBatchDelete">
<template #icon><n-icon><TrashOutline /></n-icon></template>
删除
</n-button>
</n-space>
</div>
<!-- 表格 -->
<n-data-table
:columns="columns"
:data="tableData"
:loading="loading"
:pagination="pagination"
:row-key="(row) => row.id"
:scroll-x="1200"
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
@update:checked-row-keys="handleCheck"
/>
</n-card>
<!-- 新增/编辑弹窗 -->
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 800px">
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="100px">
<n-grid :cols="2" :x-gap="24">
<n-form-item-gi label="委外产品名称" path="name">
<n-input v-model:value="formData.name" placeholder="请输入委外产品名称" />
</n-form-item-gi>
<n-form-item-gi label="委外产品编码" path="code">
<n-input v-model:value="formData.code" placeholder="请输入委外产品编码" />
</n-form-item-gi>
<n-form-item-gi label="业务类型 1工序委外" path="businessType">
<n-select v-model:value="formData.businessType" placeholder="请选择业务类型 1工序委外" :options="[]" />
</n-form-item-gi>
<n-form-item-gi label="业务id" path="businessId">
<n-input v-model:value="formData.businessId" placeholder="请输入业务id" />
</n-form-item-gi>
<n-form-item-gi label="业务编码" path="businessCode">
<n-input v-model:value="formData.businessCode" placeholder="请输入业务编码" />
</n-form-item-gi>
<n-form-item-gi label="生产订单id" path="orderItemId">
<n-input v-model:value="formData.orderItemId" placeholder="请输入生产订单id" />
</n-form-item-gi>
<n-form-item-gi label="委外数量" path="quantity">
<n-input v-model:value="formData.quantity" placeholder="请输入委外数量" />
</n-form-item-gi>
<n-form-item-gi label="供应商id" path="supplierId">
<n-input v-model:value="formData.supplierId" placeholder="请输入供应商id" />
</n-form-item-gi>
<n-form-item-gi label="供应商名称" path="supplierName">
<n-input v-model:value="formData.supplierName" placeholder="请输入供应商名称" />
</n-form-item-gi>
<n-form-item-gi label="单价" path="unitPrice">
<n-input v-model:value="formData.unitPrice" placeholder="请输入单价" />
</n-form-item-gi>
<n-form-item-gi label="委外金额(数量*单价)" path="totalAmount">
<n-input v-model:value="formData.totalAmount" placeholder="请输入委外金额(数量*单价)" />
</n-form-item-gi>
<n-form-item-gi label="预计委外开始时间" path="beginPreparationTime">
<n-date-picker v-model:value="formData.beginPreparationTime" type="datetime" clearable style="width: 100%" />
</n-form-item-gi>
<n-form-item-gi label="预计委外结束时间" path="endPreparationTime">
<n-date-picker v-model:value="formData.endPreparationTime" type="datetime" clearable style="width: 100%" />
</n-form-item-gi>
<n-form-item-gi label="实际开始时间" path="beginTime">
<n-date-picker v-model:value="formData.beginTime" type="datetime" clearable style="width: 100%" />
</n-form-item-gi>
<n-form-item-gi label="实际结束时间" path="endTime">
<n-date-picker v-model:value="formData.endTime" type="datetime" clearable style="width: 100%" />
</n-form-item-gi>
<n-form-item-gi label="是否超期" path="overdue">
<n-input v-model:value="formData.overdue" placeholder="请输入是否超期" />
</n-form-item-gi>
</n-grid>
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="modalVisible = false">取消</n-button>
<n-button type="primary" @click="handleSubmit">确定</n-button>
</n-space>
</template>
</n-modal>
<!-- 导入弹窗 -->
<n-modal v-model:show="importModalVisible" preset="card" title="导入委外记录表" style="width: 500px">
<n-space vertical>
<n-alert type="info">
<template #header>导入说明</template>
<ul style="margin: 0; padding-left: 16px; line-height: 1.8">
<li>请先下载导入模板按模板格式填写数据</li>
<li>支持 .xlsx .xls 格式</li>
</ul>
</n-alert>
<n-space>
<n-button type="primary" @click="handleDownloadTemplate">
<template #icon><n-icon><DownloadOutline /></n-icon></template>
下载模板
</n-button>
</n-space>
<n-upload :max="1" accept=".xlsx,.xls" :show-file-list="true" :custom-request="handleImportUpload">
<n-upload-dragger>
<div style="margin-bottom: 12px">
<n-icon size="48" :depth="3"><CloudUploadOutline /></n-icon>
</div>
<n-text style="font-size: 16px">点击或拖拽文件到此处上传</n-text>
<n-p depth="3" style="margin: 8px 0 0 0">支持 .xlsx .xls 格式</n-p>
</n-upload-dragger>
</n-upload>
</n-space>
<template #footer>
<n-button @click="importModalVisible = false">关闭</n-button>
</template>
</n-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, h, onMounted } from 'vue'
import { NButton, NSpace, NIcon, NUpload, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions } from 'naive-ui'
import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline } from '@vicons/ionicons5'
import { outsourcingApi, type Outsourcing } from '@/api/outsourcing'
const message = useMessage()
const dialog = useDialog()
//
const searchForm = reactive({
id: null as number | null,
name: '',
businessType: null as number | null,
})
//
const tableData = ref<Outsourcing[]>([])
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: Outsourcing = {
name: '',
code: '',
businessType: undefined,
businessId: undefined,
businessCode: '',
orderItemId: undefined,
quantity: undefined,
supplierId: undefined,
supplierName: '',
unitPrice: undefined,
totalAmount: undefined,
beginPreparationTime: undefined,
endPreparationTime: undefined,
beginTime: undefined,
endTime: undefined,
overdue: undefined,
}
const formData = reactive<Outsourcing>({ ...defaultFormData })
// //使
//
const formRules = {
businessType: { required: true, message: '请输入业务类型 1工序委外', trigger: 'blur' },
supplierName: { required: true, message: '请输入供应商名称', trigger: 'blur' },
}
//
const columns: DataTableColumns<Outsourcing> = [
{ type: 'selection' },
{ title: '主键id', key: 'id' },
{ title: '委外产品名称', key: 'name' },
{ title: '委外产品编码', key: 'code' },
{ title: '业务类型 1工序委外', key: 'businessType' },
{ title: '业务id', key: 'businessId' },
{ title: '业务编码', key: 'businessCode' },
{ title: '生产订单id', key: 'orderItemId' },
{ title: '委外数量', key: 'quantity' },
{ title: '供应商id', key: 'supplierId' },
{ title: '供应商名称', key: 'supplierName' },
{ title: '单价', key: 'unitPrice' },
{ title: '委外金额(数量*单价)', key: 'totalAmount' },
{ title: '预计委外开始时间', key: 'beginPreparationTime' },
{ title: '预计委外结束时间', key: 'endPreparationTime' },
{ title: '实际开始时间', key: 'beginTime' },
{ title: '实际结束时间', key: 'endTime' },
{ title: '备注', key: 'remark' },
{ title: '创建时间', key: 'createTime', width: 180 },
{ title: '创建人', key: 'createBy' },
{ title: '是否超期', key: 'overdue' },
{
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 outsourcingApi.page({
page: pagination.page,
pageSize: pagination.pageSize,
id: searchForm.id || undefined,
name: searchForm.name || undefined,
businessType: searchForm.businessType || undefined,
})
tableData.value = res.list
pagination.itemCount = res.total
} finally {
loading.value = false
}
}
//
function handleSearch() {
pagination.page = 1
loadData()
}
//
function handleReset() {
searchForm.id = null
searchForm.name = ''
searchForm.businessType = 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: Outsourcing) {
modalTitle.value = '编辑委外记录表'
Object.assign(formData, row)
if (formData.beginPreparationTime && typeof formData.beginPreparationTime === 'string') {
formData.beginPreparationTime = new Date(formData.beginPreparationTime.replace(' ', 'T')).getTime()
}
if (formData.endPreparationTime && typeof formData.endPreparationTime === 'string') {
formData.endPreparationTime = new Date(formData.endPreparationTime.replace(' ', 'T')).getTime()
}
if (formData.beginTime && typeof formData.beginTime === 'string') {
formData.beginTime = new Date(formData.beginTime.replace(' ', 'T')).getTime()
}
if (formData.endTime && typeof formData.endTime === 'string') {
formData.endTime = new Date(formData.endTime.replace(' ', 'T')).getTime()
}
if (formData.createTime && typeof formData.createTime === 'string') {
formData.createTime = new Date(formData.createTime.replace(' ', 'T')).getTime()
}
modalVisible.value = true
}
//
async function handleSubmit() {
await formRef.value?.validate()
try {
const submitData = { ...formData } as Outsourcing
if (typeof submitData.beginPreparationTime === 'number') {
submitData.beginPreparationTime = new Date(submitData.beginPreparationTime).toISOString().slice(0, 19).replace('T', ' ')
}
if (typeof submitData.endPreparationTime === 'number') {
submitData.endPreparationTime = new Date(submitData.endPreparationTime).toISOString().slice(0, 19).replace('T', ' ')
}
if (typeof submitData.beginTime === 'number') {
submitData.beginTime = new Date(submitData.beginTime).toISOString().slice(0, 19).replace('T', ' ')
}
if (typeof submitData.endTime === 'number') {
submitData.endTime = new Date(submitData.endTime).toISOString().slice(0, 19).replace('T', ' ')
}
if (typeof submitData.createTime === 'number') {
submitData.createTime = new Date(submitData.createTime).toISOString().slice(0, 19).replace('T', ' ')
}
if (submitData.id) {
await outsourcingApi.update(submitData)
message.success('修改成功')
} else {
await outsourcingApi.create(submitData)
message.success('新增成功')
}
modalVisible.value = false
loadData()
} catch (error) {
//
}
}
//
function handleDelete(row: Outsourcing) {
dialog.warning({
title: '提示',
content: '确定要删除该记录吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
await outsourcingApi.delete([row.id!])
message.success('删除成功')
loadData()
} catch (error) {
//
}
}
})
}
//
function handleBatchDelete() {
dialog.warning({
title: '提示',
content: `确定要删除选中的 ${selectedIds.value.length} 条记录吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
await outsourcingApi.delete(selectedIds.value)
message.success('删除成功')
selectedIds.value = []
loadData()
} catch (error) {
//
}
}
})
}
//
async function handleExport() {
try {
const params: Record<string, any> = {}
if (selectedIds.value.length > 0) params.ids = selectedIds.value
if (searchForm.id != null) params.id = searchForm.id
if (searchForm.name) params.name = searchForm.name
if (searchForm.businessType != null) params.businessType = searchForm.businessType
const blob = await outsourcingApi.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 outsourcingApi.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 outsourcingApi.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() {
}
onMounted(() => {
loadData()
loadDictOptions()
})
</script>
<style scoped>
.search-form {
margin-bottom: 16px;
}
.table-toolbar {
margin-bottom: 16px;
}
</style>

View File

@ -0,0 +1,71 @@
import { request } from '@/utils/request'
// 异常过滤表 类型定义
export interface ExceptionFilter {
id?: number
title?: string
filterCondition?: string
description?: string
status?: 0
deleted?: number
}
// 异常过滤表 API
export const exceptionFilterApi = {
// 分页查询
page(params: { page: number; pageSize: number; id?: number; status?: number }) {
return request({ url: '/biz/exceptionFilter/page', method: 'get', params })
},
// 获取详情
detail(id: string) {
return request({ url: `/biz/exceptionFilter/${id}`, method: 'get' })
},
// 新增
create(data: ExceptionFilter) {
return request({ url: '/biz/exceptionFilter', method: 'post', data })
},
// 修改
update(data: ExceptionFilter) {
return request({ url: '/biz/exceptionFilter', method: 'put', data })
},
// 删除
delete(ids: string[]) {
return request({ url: `/biz/exceptionFilter/${ids.join(',')}`, method: 'delete' })
},
// 导出
export(params?: { ids?: string[]; id?: number; status?: number }) {
const p: Record<string, any> = {}
if (params?.ids?.length) p.ids = params.ids.join(',')
if (params?.id !== undefined && params?.id !== null) p.id = params.id
if (params?.status !== undefined && params?.status !== null) p.status = params.status
return request({ url: `/biz/exceptionFilter/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/exceptionFilter/import`,
method: 'post',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
})
},
// 下载导入模板
downloadTemplate() {
return request({ url: `/biz/exceptionFilter/template`, method: 'get', responseType: 'blob' })
}
}

View File

@ -19,6 +19,7 @@ export interface SysNotice {
errorNotificationId?:number,
filler?:number,
filingStatus?:number,
deviceName?:string
}
export interface NoticeChannelOption {

View File

@ -44,6 +44,8 @@ export interface OrderItem {
assingWorkOperationTime?: string
starter?: number
mainCode?: string
orderCode?: string
@ -123,7 +125,11 @@ export const orderItemApi = {
//派工
assignWork(data: any) {
return request({ url: '/biz/orderItem/assignWork', method: 'post', data })
},
},
// 开工
startWork(id: number) {
return request({ url: `/biz/orderItem/${id}/startWork`, method: 'post' })
},
//根据生产编号查询预生产订单
preCreation(data: any) {
return request({ url: '/biz/orderItem/preCreation', method: 'post', data })

View File

@ -88,6 +88,12 @@ export interface ProcessPlanItemVO {
planFinishTime?: string
/** 计划开始(部分接口直接返回 beginTime */
beginTime?: string
/** 计划结束(部分接口直接返回 endTime */
endTime?: string
actualStartTime?: string | null
actualFinishTime?: string | null
@ -242,9 +248,9 @@ export function processItemToEntity(
orderItemId,
beginTime: item.planStartTime,
beginTime: item.planStartTime || item.beginTime,
endTime: item.planFinishTime,
endTime: item.planFinishTime || item.endTime,
procurement: item.procurement,
@ -469,6 +475,18 @@ export const orderProcessPlanApi = {
})
},
/**
*
* orderItemId + planId sort10/20/30
*/
reorder(data: ProcessPlanReorderPayload) {
return request({
url: '/biz/orderProcessPlan/reorder',
method: 'put',
data,
})
},
}
/** 委外可选工序项 */
@ -509,3 +527,16 @@ export interface OutsourcingCreate {
remark?: string
}
/** 工序重排明细 */
export interface ProcessPlanReorderItem {
planId: number
/** 可选;不传则按数组顺序由后端分配 sort */
sort?: number
}
/** 订单级工序重排请求 */
export interface ProcessPlanReorderPayload {
orderItemId: number
items: ProcessPlanReorderItem[]
}

View File

@ -89,6 +89,10 @@ export interface KingdeeProcessRoute {
planStartTime: string | null
/** 计划结束时间 yyyy-MM-dd HH:mm:ss */
planFinishTime: string | null
/** 是否质检 1是 0否后端按控制码推导后返回 */
qualityInspection?: number | null
/** 是否入库 1是 0否后端仅末序为 1 */
storageEntry?: number | null
}
/** 解析工序名称,兼容旧字段及后端误映射 */

View File

@ -2,7 +2,6 @@
import {onMounted, ref, watch} from "vue";
import { Notifications } from '@vicons/ionicons5'
import { ErrorNotification } from '@/api/errorNotification.ts'
import {} from '@/api/system.ts'
import {noticeApi, SysNotice} from '@/api/message'
import type {FormInst} from "naive-ui";
@ -12,7 +11,7 @@ const emit = defineEmits<{
const props = defineProps<{
show: boolean
errorNotifiaction:ErrorNotification,
notice:SysNotice,
}>()
@ -72,7 +71,7 @@ watch(showModal, (val) => {
async function handleSubmit() {
try {
await formRef.value?.validate()
formData.value.id = props.errorNotifiaction.noticeId
formData.value.id = props.notice.id
formData.value.filingStatus = 1
//
noticeApi.update({
@ -114,7 +113,7 @@ async function handleSubmit() {
<template #1>
<n-grid x-gap="12" :cols="1">
<n-gi>
<h2>异常内容</h2>
<h2>通知内容</h2>
</n-gi>
</n-grid>
<n-form
@ -122,21 +121,21 @@ async function handleSubmit() {
label-width="100"
style="margin: 10px"
>
<n-form-item label="异常名称" path="title">
<n-form-item label="通知标题" path="title">
<n-input
v-model:value="props.errorNotifiaction.title"
v-model:value="props.notice.title"
disabled
/>
</n-form-item>
<n-form-item label="异常设备" path="devices">
<n-form-item label="异常设备" path="title">
<n-input
v-model:value="props.errorNotifiaction.devices"
v-model:value="props.notice.deviceName"
disabled
/>
</n-form-item>
<n-form-item label="通知内容" path="notifierContent">
<n-input
v-model:value="errorNotifiaction.notifierContent"
v-model:value="props.notice.content"
type="textarea"
disabled
/>
@ -172,7 +171,7 @@ async function handleSubmit() {
<div v-if="!isFili">
<n-grid x-gap="12" :cols="1">
<n-gi>
<h2>异常内容</h2>
<h2>通知内容</h2>
</n-gi>
</n-grid>
<n-form
@ -180,21 +179,21 @@ async function handleSubmit() {
label-width="100"
style="margin: 10px"
>
<n-form-item label="异常名称" path="title">
<n-form-item label="通知标题" path="title">
<n-input
v-model:value="props.errorNotifiaction.title"
v-model:value="props.notice.title"
disabled
/>
</n-form-item>
<n-form-item label="异常设备" path="devices">
<n-form-item label="异常设备" path="title">
<n-input
v-model:value="props.errorNotifiaction.devices"
v-model:value="props.notice.deviceName"
disabled
/>
</n-form-item>
<n-form-item label="通知内容" path="notifierContent">
<n-input
v-model:value="errorNotifiaction.notifierContent"
v-model:value="props.notice.content"
type="textarea"
disabled
/>

View File

@ -335,7 +335,6 @@
<!--异常填报弹窗-->
<ErrorReportModal
v-model:show="showErrorReportModal"
:errorNotifiaction ="errorNotifiaction"
:notice = "notice"
/>
</n-layout>
@ -609,40 +608,22 @@ function stripHtml(html: string | undefined): string {
}
//
//
const errorNotifiaction = ref({
title: '',
devices: '',
sendingMethodName: '',
notifierName: '',
notifierContent: ''
})
//
const notice = ref<SysNotice>( {})
async function handleNoticeClick(item: SysNotice) {
const res = await errorNotificationApi.detail(item.errorNotificationId);
errorNotifiaction.value = {...res}
errorNotifiaction.value.noticeId = item.id
notice.value = item
//
showErrorReportModal.value = true
// if (item.abnormalReporting) {
//
// }else {
// //
// if (item.id) {
// try {
// await noticeApi.markAsRead(item.id)
// //
// loadUnreadCount()
// } catch (error) {
// //
// }
// }
// router.push({ path: '/message/notice', query: { id: item.id?.toString() } })
// }
//
if (item.id) {
try {
await noticeApi.markAsRead(item.id)
//
loadUnreadCount()
} catch (error) {
//
}
}
}
//

View File

@ -1,4 +1,8 @@
import { normalizeKingdeeProcessRoute, type KingdeePrdMo, type KingdeeProcessRoute } from '@/api/orderProject'
import {
normalizeKingdeeProcessRoute,
type KingdeePrdMo,
type KingdeeProcessRoute,
} from '@/api/orderProject'
export type KingdeeMoTableRow = Omit<KingdeePrdMo, 'children'> & { processRoutes: KingdeeProcessRoute[] }
@ -11,6 +15,9 @@ export function mapKingdeeMoListToTableRows(data: KingdeePrdMo[]): KingdeeMoTabl
...r,
planStartTime: r.planStartTime ?? null,
planFinishTime: r.planFinishTime ?? null,
// 质检/入库由后端查询金蝶时已写入,前端原样带出
qualityInspection: r.qualityInspection ?? null,
storageEntry: r.storageEntry ?? null,
}))
return { ...rest, processRoutes: routes }
})

View File

@ -327,8 +327,8 @@ const columns: DataTableColumns<BasicProcessPlan> = [
return h(NTag,{type: opt ? 'success' : 'error', size: 'small'},{default: () => (opt ? '是' : '否')})
}
},
{ title: '工作中心', key: 'workCenterName' },
{ title: '生产车间', key: 'departmentName' },
{ title: '工作中心', key: 'sectionName' },
{ title: '生产车间', key: 'workShopName' },
{ title: '工序说明', key: 'operDescription' },
{ title: '状态', key: 'status',
render(row){

View File

@ -112,12 +112,18 @@
/>
</n-form-item>
<n-form-item label="所属工段" path="sectionId">
<n-select
<!-- <n-select
v-model:value="formData.sectionId"
:options="sectionList"
placeholder="请选择所属工段"
clearable
style="width: 200px"
/> -->
<n-tree-select
v-model:value="formData.sectionId"
cascade
checkable
:options="sectionList"
/>
</n-form-item>
<n-form-item label="设备类型" path="deviceTypeId">
@ -267,12 +273,14 @@ import {deviceApi, type Device} from '@/api/device'
import {dictDataApi} from '@/api/org'
import {sectionApi} from '@/api/section'
import {SysUser, userApi} from '@/api/system'
import {deptApi, type SysDept , userApi} from '@/api/system'
import {DeviceAbnormalRecord, deviceAbnormalRecordApi} from "@/api/deviceAbnormalRecord.ts";
import DeviceAbnormalRecordPage from '@/views/biz/device/DeviceAbnomarlRecord.vue'
import DeviceOperationLogPage from "@/views/biz/device/DeviceOperationLog.vue";
import DeviceDispatchLogPage from "@/views/biz/device/DeviceDispatchLog.vue";
import { S } from 'vue-router/dist/router-CWoNjPRp.mjs'
const message = useMessage()
const dialog = useDialog()
const router = useRouter()
@ -283,8 +291,15 @@ const deviceId = ref<number>(undefined)
//
const deviceManagerList = ref<{ label: string, value: any }[]>([])
let sectionList = reactive<{ label: string, value: any }[]>([])
//
interface TreeNode {
label: string;
key: number | string;
children?: TreeNode[];
}
let sectionList = reactive<TreeNode[]>([])
// const sectionList = ref<SysDept[]>([]) //
//
const deviceErrorLogList = ref<DeviceAbnormalRecord[]>([])
const deviceAbnormalRecordDrawerVisible = ref<Boolean>(false) //
@ -664,11 +679,26 @@ async function loadDictOptions() {
}
}
//
async function loadSection() {
const data = await sectionApi.listNoParam()
sectionList = data.map((d: any) => ({label: d.sectionName, value: (Number(d.id) || d.id)}))
const data = await deptApi.tree()
sectionList = buildOptions(data);
}
function buildOptions(d: any[]): TreeNode[] {
return d.map(item => {
// id0||
const idNum = Number(item.id);
const key = !isNaN(idNum) ? idNum : item.id;
return {
label: item.deptName,
key,
//
children: Array.isArray(item.children) && item.children.length
? buildOptions(item.children)
: undefined
};
});
}
//

View File

@ -4,8 +4,11 @@
<!-- 搜索表单 -->
<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 label="主键ID">
<n-input v-model:value="searchForm.id" placeholder="请输入主键ID" clearable />
</n-form-item>
<n-form-item label="状态">
<n-select v-model:value="searchForm.status" placeholder="请选择状态" clearable style="width: 150px" :options="statusOptions" />
</n-form-item>
<n-form-item>
<n-space>
@ -61,29 +64,26 @@
<!-- 新增/编辑弹窗 -->
<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 label="异常过滤标题" path="title">
<n-input v-model:value="formData.title" placeholder="请输入异常过滤标题" />
</n-form-item>
<n-form-item label="让步接收数量" path="concessionQty">
<n-input v-model:value="formData.concessionQty" placeholder="请输入让步接收数量" />
<n-form-item label="过滤条件" path="filterCondition">
<n-input v-model:value="formData.filterCondition" type="textarea" placeholder="请输入过滤条件" />
</n-form-item>
<n-form-item label="申请原因" path="applyRemark">
<n-input v-model:value="formData.applyRemark" type="textarea" placeholder="请输入申请原因" />
<n-form-item label="描述" path="description">
<n-input v-model:value="formData.description" 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 label="状态">
<n-radio-group v-model:value="formData.status" :default-value="0" >
<n-space>
<n-radio :value="1">
启用
</n-radio>
<n-radio :value="0">
禁用
</n-radio>
</n-space>
</n-radio-group>
</n-form-item>
</n-form>
<template #footer>
@ -95,7 +95,7 @@
</n-modal>
<!-- 导入弹窗 -->
<n-modal v-model:show="importModalVisible" preset="card" title="导入让步接收申请表" style="width: 500px">
<n-modal v-model:show="importModalVisible" preset="card" title="导入异常过滤表" style="width: 500px">
<n-space vertical>
<n-alert type="info">
<template #header>导入说明</template>
@ -131,7 +131,7 @@
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 { exceptionFilterApi, type ExceptionFilter } from '@/api/exceptionFilter'
import { dictDataApi } from '@/api/org'
const message = useMessage()
@ -140,10 +140,11 @@ const dialog = useDialog()
//
const searchForm = reactive({
id: null as number | null,
status: null as number | null,
})
//
const tableData = ref<ConcessionApply[]>([])
const tableData = ref<ExceptionFilter[]>([])
const loading = ref(false)
const selectedIds = ref<number[]>([])
const pagination = reactive({
@ -159,47 +160,35 @@ 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 defaultFormData: ExceptionFilter = {
title: '',
filterCondition: '',
description: '',
status: undefined,
}
const formData = reactive<ConcessionApply>({ ...defaultFormData })
const formData = reactive<ExceptionFilter>({ ...defaultFormData })
// //使
const approveStatusOptions = ref<{ label: string; value: any }[]>([])
const statusOptions = ref<{ label: string; value: any }[]>([])
//
const formRules = {
}
//
const columns: DataTableColumns<ConcessionApply> = [
const columns: DataTableColumns<ExceptionFilter> = [
{ 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',
{ title: '主键ID', key: 'id' },
{ title: '异常过滤标题', key: 'title' },
{ title: '过滤条件', key: 'filterCondition' },
{ title: '描述', key: 'description' },
{ title: '状态', key: 'status',
render(row) {
const val = row.approveStatus
const opt = approveStatusOptions.value.find(o => o.value === val || String(o.value) === String(val))
const val = row.status
const opt = statusOptions.value.find(o => o.value === val || String(o.value) === String(val))
return opt ? opt.label : (val ?? '-')
}
},
{ title: '创建时间', key: 'createTime', width: 180 },
{ title: '更新时间', key: 'updateTime', width: 180 },
{
title: '操作',
key: 'actions',
@ -222,10 +211,11 @@ const columns: DataTableColumns<ConcessionApply> = [
async function loadData() {
loading.value = true
try {
const res = await concessionApplyApi.page({
const res = await exceptionFilterApi.page({
page: pagination.page,
pageSize: pagination.pageSize,
id: searchForm.id || undefined,
status: searchForm.status || undefined,
})
tableData.value = res.list
pagination.itemCount = res.total
@ -244,6 +234,8 @@ function handleSearch() {
function handleReset() {
searchForm.id = null
searchForm.status = null
handleSearch()
}
@ -266,21 +258,15 @@ function handleCheck(keys: Array<string | number>) {
//
function handleAdd() {
modalTitle.value = '新增让步接收申请表'
modalTitle.value = '新增异常过滤表'
Object.assign(formData, defaultFormData)
modalVisible.value = true
}
//
function handleEdit(row: ConcessionApply) {
modalTitle.value = '编辑让步接收申请表'
function handleEdit(row: ExceptionFilter) {
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
}
@ -288,18 +274,12 @@ function handleEdit(row: ConcessionApply) {
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', ' ')
}
const submitData = { ...formData } as ExceptionFilter
if (submitData.id) {
await concessionApplyApi.update(submitData)
await exceptionFilterApi.update(submitData)
message.success('修改成功')
} else {
await concessionApplyApi.create(submitData)
await exceptionFilterApi.create(submitData)
message.success('新增成功')
}
modalVisible.value = false
@ -310,7 +290,7 @@ async function handleSubmit() {
}
//
function handleDelete(row: ConcessionApply) {
function handleDelete(row: ExceptionFilter) {
dialog.warning({
title: '提示',
content: '确定要删除该记录吗?',
@ -318,7 +298,7 @@ function handleDelete(row: ConcessionApply) {
negativeText: '取消',
onPositiveClick: async () => {
try {
await concessionApplyApi.delete([row.id!])
await exceptionFilterApi.delete([row.id!])
message.success('删除成功')
loadData()
} catch (error) {
@ -337,7 +317,7 @@ function handleBatchDelete() {
negativeText: '取消',
onPositiveClick: async () => {
try {
await concessionApplyApi.delete(selectedIds.value)
await exceptionFilterApi.delete(selectedIds.value)
message.success('删除成功')
selectedIds.value = []
loadData()
@ -354,11 +334,12 @@ async function handleExport() {
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)
if (searchForm.status != null) params.status = searchForm.status
const blob = await exceptionFilterApi.export(params)
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '让步接收申请表数据.xlsx'
link.download = '异常过滤表数据.xlsx'
link.click()
window.URL.revokeObjectURL(url)
} catch (error) {
@ -369,11 +350,11 @@ async function handleExport() {
//
async function handleDownloadTemplate() {
try {
const blob = await concessionApplyApi.downloadTemplate()
const blob = await exceptionFilterApi.downloadTemplate()
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '让步接收申请表导入模板.xlsx'
link.download = '异常过滤表导入模板.xlsx'
link.click()
window.URL.revokeObjectURL(url)
} catch (error) {
@ -385,7 +366,7 @@ async function handleDownloadTemplate() {
async function handleImportUpload({ file }: UploadCustomRequestOptions) {
if (!file.file) return
try {
const result = await concessionApplyApi.importData(file.file)
const result = await exceptionFilterApi.importData(file.file)
if (result.fail > 0) {
dialog.warning({
title: '导入结果',
@ -406,7 +387,7 @@ async function handleImportUpload({ file }: UploadCustomRequestOptions) {
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) }))
statusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue) }))
} catch {}
}

View File

@ -37,10 +37,10 @@
<n-button type="error" :disabled="kingdeeLoading" @click="reCrawlReorderForm">
重新读取数据
</n-button>
<n-button type="primary" :disabled="kingdeeLoading" @click="handleSaveDraft">
<n-button type="primary" :disabled="kingdeeLoading" :loading="actionLoading" @click="handleSaveDraft">
保存草稿
</n-button>
<n-button type="info" :disabled="kingdeeLoading" @click="handleSyncOrderAndPlan">
<n-button type="info" :disabled="kingdeeLoading" :loading="actionLoading" @click="handleSyncOrderAndPlan">
同步订单计划
</n-button>
<n-button @click="$emit('close-modal')">关闭</n-button>
@ -54,7 +54,7 @@
<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 { type KingdeePrdMo, type KingdeeProcessRoute, resolveProcessName } from '@/api/orderProject'
import { useRouter } from 'vue-router'
import { CalendarOutline } from '@vicons/ionicons5'
@ -72,20 +72,27 @@ const props = defineProps({
selectedId:{
type: Number,
default: null
},
/** 父页面查询/重读数据时的加载状态 */
loading: {
type: Boolean,
default: false
}
})
onMounted(() => {
kingdeeTableData.value = props.kingdeeTableData as KingdeeMoTableRow[]
kingdeeTableData.value = (props.kingdeeTableData as KingdeeMoTableRow[]) || []
})
watch(()=>props.kingdeeTableData,()=>{
kingdeeTableData.value = props.kingdeeTableData as KingdeeMoTableRow[]
},{ deep: true } )
watch(() => props.kingdeeTableData, () => {
kingdeeTableData.value = (props.kingdeeTableData as KingdeeMoTableRow[]) || []
}, { deep: true })
const kingdeeModalVisible = ref(false)
const kingdeeLoading = ref(false)
/** 子组件自身操作(保存/同步)的加载状态 */
const actionLoading = ref(false)
/** 表格与按钮共用:父级查询 loading 或本地操作 loading */
const kingdeeLoading = computed(() => props.loading || actionLoading.value)
/** 主表行:工序仅存 processRoutes避免 children 触发树形重复行 */
type KingdeeMoTableRow = Omit<KingdeePrdMo, 'children'> & { processRoutes: KingdeeProcessRoute[] }
const kingdeeTableData = ref<KingdeeMoTableRow[]>([])
@ -93,7 +100,7 @@ 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 kingdeeProcessScrollX = 1540
const kingdeeMoColumns: DataTableColumns<KingdeeMoTableRow> = [
{
@ -155,7 +162,12 @@ const kingdeeProcessColumns: DataTableColumns<KingdeeProcessRoute> = [
return row.operNumber != null ? String(row.operNumber) : '-'
}
},
{ title: '工序名称', key: 'processProperty', width: 100, align: 'center' },
{
title: '工序名称', key: 'processName', width: 100, align: 'center',
render(row) {
return resolveProcessName(row) || '-'
}
},
{ 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 } },
@ -179,6 +191,28 @@ const kingdeeProcessColumns: DataTableColumns<KingdeeProcessRoute> = [
},
{ title: '活动单位', key: 'activityUnit', width: 80, align: 'center' },
{ title: '控制码', key: 'optCtrlCodeName', width: 120, align: 'center', ellipsis: { tooltip: true } },
{
title: '是否质检', key: 'qualityInspection', width: 88, align: 'center',
render(row) {
const yes = Number(row.qualityInspection) === 1
return h(NTag, { size: 'small', type: yes ? 'success' : 'default', bordered: false }, {
default: () => (yes ? '是' : '否'),
})
}
},
{
title: '是否入库', key: 'storageEntry', width: 88, align: 'center',
render(row) {
const yes = Number(row.storageEntry) === 1
return h(NTag, {
size: 'small',
type: yes ? 'warning' : 'default',
bordered: false,
}, {
default: () => (yes ? '是' : '否'),
})
}
},
]
@ -298,29 +332,32 @@ function handleGanttSchedule() {
router.push('/biz/orderProject/gantt')
}
//
// loading
async function handleSyncOrderAndPlan() {
if (!props.selectedId) return
kingdeeLoading.value = true
if (!props.selectedId || actionLoading.value) return
actionLoading.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')
}
const res = await new Promise<unknown>((resolve, reject) => {
emit('synchronizationCallback', props.selectedId, draftData, (result: unknown, err?: unknown) => {
if (err) reject(err)
else resolve(result)
})
kingdeeModalVisible.value = false
} catch (error) {
})
if (res === 'success') {
message.success('同步订单计划成功')
emit('close-modal')
}
} catch {
//
} finally {
kingdeeLoading.value = false
actionLoading.value = false
}
}
@ -336,15 +373,8 @@ const reCrawlReorderForm = () => {
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
kingdeeLoading.value = true
try {
emit('reCrawlCallback', props.selectedId, true)
message.success('重启读取成功')
} catch (error) {
//
} finally {
kingdeeLoading.value = false
}
// reCrawlReorderForm kingdeeLoading
emit('reCrawlCallback', props.selectedId, true)
}
})
@ -356,7 +386,7 @@ const handleSaveDraft = () => {
// // 稿
if (!props.selectedId) return
kingdeeLoading.value = true
actionLoading.value = true
try {
const draftData = kingdeeTableData.value.map(mo => {
const { processRoutes, ...rest } = mo
@ -367,7 +397,7 @@ const handleSaveDraft = () => {
} catch (error) {
//
} finally {
kingdeeLoading.value = false
actionLoading.value = false
}
}

View File

@ -1,99 +1,95 @@
<template>
<div class="page-container">
<n-card>
<!-- 搜索表单 -->
<div class="search-form">
<n-form inline :model="searchForm" label-placement="left">
<n-form-item label="物料编码">
<n-input v-model:value="searchForm.code" placeholder="请输入物料编码" clearable />
</n-form-item>
<n-form-item label="项目名称">
<n-input v-model:value="searchForm.projectName" placeholder="请输入产品名称" clearable />
</n-form-item>
<n-form-item label="开始时间">
<n-date-picker v-model:value="searchForm.beginTime" type="datetime" clearable />
</n-form-item>
<n-form-item label="结束时间">
<n-date-picker v-model:value="searchForm.endTime" type="datetime" 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="order-layout">
<!-- 左侧生产令号 -->
<n-card class="order-tree-card" size="small">
<template #header>
<div class="order-tree-header">生产令号</div>
</template>
<div class="order-search">
<n-input v-model:value="projectTreeSearch" placeholder="搜索生产令号" clearable size="small">
<template #prefix>
<n-icon><SearchOutline /></n-icon>
</template>
</n-input>
</div>
<div class="order-tree-wrapper">
<n-tree
:data="projectTreeData"
:pattern="projectTreeSearch"
:selected-keys="selectedProjectKeys"
:node-props="projectNodeProps"
key-field="id"
label-field="label"
selectable
block-line
/>
</div>
</n-card>
<!-- 工具栏 -->
<div class="table-toolbar">
<n-space>
<!--<n-button
size="small"
type="primary"
@click="handleAdd"
<!-- 右侧生产订单列表 -->
<n-card class="order-list-card" size="small">
<template #header>
<div class="card-header">
<span>{{ selectedProjectLabel ? `${selectedProjectLabel}】生产订单` : '全部生产订单' }}</span>
</div>
</template>
<!-- 搜索表单 -->
<div class="search-form">
<n-form inline :model="searchForm" label-placement="left">
<n-form-item label="物料编码">
<n-input v-model:value="searchForm.code" placeholder="请输入物料编码" clearable />
</n-form-item>
<n-form-item label="开始时间">
<n-date-picker v-model:value="searchForm.beginTime" type="datetime" clearable />
</n-form-item>
<n-form-item label="结束时间">
<n-date-picker v-model:value="searchForm.endTime" type="datetime" 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>
<n-data-table
size="small"
remote
striped
:border="false"
:single-line="false"
:columns="columns"
:data="tableData"
:loading="loading"
/>
<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 #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>
<!-- 表格 -->
<!--
:row-key="(row) => row.id"
:scroll-x="1200"
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
@update:checked-row-keys="handleCheck"
:pagination="pagination"
-->
<!--flex-height-->
<n-data-table
size="small"
remote
striped
:border="false"
:single-line="false"
:columns="columns"
:data="tableData"
:loading="loading"
/>
<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>
<template #prefix>
{{ pagination.itemCount }}
</template>
</n-pagination>
</div>
</n-card>
</div>
<!-- 新增/编辑弹窗 -->
<n-modal
@ -370,7 +366,8 @@
>
<KingdeeVue
:kingdeeTableData="kingdeeTableData"
:selectedId="kingdeeOrderOtemId"
:selectedId="kingdeeOrderOtemId"
:loading="kingdeeLoading"
@close-modal="kingdeeModalVisible = false"
@holdTemporarilyCallback="holdTemporarilySave"
@synchronizationCallback="synchronizationSave"
@ -381,16 +378,19 @@
</template>
<script setup lang="ts">
import { ref, reactive, h, onMounted } from 'vue'
import { NButton, NSpace, NIcon, useMessage, useDialog, type UploadCustomRequestOptions} from 'naive-ui'
import { ref, reactive, h, computed, onMounted, type HTMLAttributes } from 'vue'
import { NButton, NSpace, NIcon, NTag, useMessage, useDialog, type UploadCustomRequestOptions, type TreeOption } from 'naive-ui'
import { SearchOutline, RefreshOutline, CreateOutline, PlayOutline, DocumentTextOutline } from '@vicons/ionicons5'
import { orderItemApi, type OrderItem } from '@/api/orderItem'
import { useUserStore } from '@/stores/user'
import { VueDraggable } from 'vue-draggable-plus'
import KingdeeVue from '@/views/biz/kingdee/index.vue'
import { type KingdeePrdMo, type KingdeeProcessRoute } from '@/api/orderProject'
import { type KingdeePrdMo } from '@/api/orderProject'
import { mapKingdeeMoListToTableRows, type KingdeeMoTableRow } from '@/utils/kingdeeSchedule'
import { userApi } from '@/api/system'
import { dictDataApi } from '@/api/org'
const message = useMessage()
const dialog = useDialog()
@ -416,6 +416,56 @@ const searchForm = reactive<any>({
endTime: null as number | null,
})
//
type ProjectTreeNode = {
id: number
label: string
projectCode: string
projectName: string
}
const projectTreeSearch = ref('')
const projectTreeData = ref<ProjectTreeNode[]>([])
const selectedProjectKeys = ref<Array<string | number>>([])
const selectedProjectId = ref<number | undefined>(undefined)
const selectedProjectLabel = ref('')
const projectNodeProps = ({ option }: { option: TreeOption }): HTMLAttributes => {
return {
onClick() {
const id = option.id as number
if (selectedProjectId.value === id) {
selectedProjectId.value = undefined
selectedProjectKeys.value = []
selectedProjectLabel.value = ''
} else {
selectedProjectId.value = id
selectedProjectKeys.value = [id]
selectedProjectLabel.value = (option.projectCode as string) || (option.label as string) || ''
}
pagination.page = 1
loadData()
}
}
}
async function loadProjectTree() {
try {
const data = await orderItemApi.xmoptions({})
projectTreeData.value = (Array.isArray(data) ? data : []).map((item: any) => {
const projectCode = item?.additionParams?.projectCode || ''
const projectName = item?.label || ''
return {
id: Number(item.value),
label: projectCode || projectName,
projectCode,
projectName,
}
}).filter((n: ProjectTreeNode) => n.id && n.label)
} catch {
projectTreeData.value = []
}
}
//
@ -466,25 +516,30 @@ const defaultFormData = {
const formData = reactive<any>({ ...defaultFormData })
// //使
const workshopList = ref<{ label: string; value: any; class: any }[]>([])
//
const formRules = {
}
//
const columns = [
// computed
const columns = computed(() => [
{
type: 'selection'
},
{
{
align:'center',
title: '产品名称',
key: 'projectName',
// ellipsis: {
// tooltip: true
// }
minWidth: 150,
title: '生产令号',
key: 'mainCode',
width: '140'
},
{
align:'center',
title: '订单编号',
key: 'orderCode',
width:"120"
},
{
align:'center',
title: '物料名称',
@ -497,12 +552,7 @@ const columns = [
key: 'code',
width:"120"
},
{
align:'center',
title: '订单编号',
key: 'orderCode',
width:"120"
},
{
align:'center',
title: '工艺路线编码',
@ -513,43 +563,58 @@ const columns = [
align:'center',
title: '生产车间',
key: 'proWorkshop',
width:"120"
},
{
align:'center',
title: '产品序列',
key: 'sort',
width:"120"
width:"120",
render(row: any) {
const val = row.proWorkshop
// dictValue WorkShopName
const opt = workshopList.value.find(o =>
o.value === val ||
String(o.value) === String(val) ||
o.label === val ||
String(o.label) === String(val)
)
if (!opt) return val ?? '-'
const tagType = opt.class && ['default', 'primary', 'info', 'success', 'warning', 'error'].includes(opt.class)
? opt.class
: 'info'
return h(NTag, { type: tagType, size: 'small' }, { default: () => opt.label })
}
},
// {
// align:'center',
// title: '',
// key: 'sort',
// width:"120"
// },
{
align:'center',
title: '生产数量',
key: 'quantity',
width:"120"
},
{
align:'center',
title: '是否半成品',
key: 'sfProduct',
width:"120"
},
{
align:'center',
title: '是否采购',
key: 'procurement',
width:"120"
},
// {
// align:'center',
// title: '',
// key: 'sfProduct',
// width:"120"
// },
// {
// align:'center',
// title: '',
// key: 'procurement',
// width:"120"
// },
{
align:'center',
title: '开始时间',
key: 'beginTime',
width:"120"
width:"160"
},
{
align:'center',
title: '结束时间',
key: 'endTime',
width:"120"
width:"160"
},
{
align:'center',
@ -585,7 +650,10 @@ const columns = [
align:'center',
title: '是否开工',
key: 'starter',
width:"120"
width:"120",
render(row: any) {
return row.starter === 1 ? '已开工' : '未开工'
}
},
{
align:'center',
@ -597,54 +665,48 @@ const columns = [
align:'center',
title: '操作',
key: 'actions',
width: 190,
width: 260,
fixed:"right",
render(row:any) {
const buttons: ReturnType<typeof h>[] = []
const buttons:any = []
if(hasPermission('biz:orderItem:edit')){
buttons.push(h(NButton, {
size: 'small',
type:'primary',
ghost:true,
onClick: () => { handleEdit(row) } },
{ default: () => '编辑'}
))
}
if(hasPermission('biz:orderItem:assign')){
buttons.push(h(NButton, {
size: 'small',
type:'success',
ghost:true,
onClick: () => { disphand(row) } },
{ 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: () => '补单'}
))
}
if (hasPermission('biz:orderItem:edit')) {
buttons.push(h(NButton, {
size: 'small',
quaternary: true,
onClick: () => handleEdit(row),
}, {
default: () => [h(NIcon, null, { default: () => h(CreateOutline) }), ' 编辑'],
}))
}
// 线
if (hasPermission('biz:orderItem:startWork') && row.starter !== 1 && row.parentId && row.routeCode) {
buttons.push(h(NButton, {
size: 'small',
quaternary: true,
type: 'warning',
onClick: () => handleStartWork(row),
}, {
default: () => [h(NIcon, null, { default: () => h(PlayOutline) }), ' 开工'],
}))
}
if (hasPermission('biz:orderItem:reorderForm')) {
buttons.push(h(NButton, {
size: 'small',
quaternary: true,
type: 'info',
onClick: () => handleReorderForm(row, false),
}, {
default: () => [h(NIcon, null, { default: () => h(DocumentTextOutline) }), ' 补单'],
}))
}
return buttons.length > 0 ? h(NSpace, {justify:'center'}, { default: () => buttons }) : '-'
// return h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'nowrap' } }, [
// h(NButton, { size: 'small', quaternary: true, onClick: () => handleEdit(row) }, {
// default: () => [h(NIcon, null, { default: () => h(CreateOutline) }), ' ']
// }),
// h(NButton, { size: 'small', quaternary: true, type: 'error', onClick: () => handleDelete(row) }, {
// default: () => [h(NIcon, null, { default: () => h(TrashOutline) }), ' ']
// })
// ])
return buttons.length > 0
? h('div', { style: { display: 'flex', alignItems: 'center', gap: '4px', flexWrap: 'nowrap', justifyContent: 'center' } }, buttons)
: '-'
}
}
]
])
//
let scrollX = ref(0)
@ -666,11 +728,9 @@ async function loadData() {
const res = await orderItemApi.page({
page: pagination.page,
pageSize: pagination.pageSize,
code: searchForm.code,
projectName: searchForm.projectName,
orderCode: searchForm.orderCode,
routeCode: searchForm.routeCode,
proWorkshop: searchForm.proWorkshop,
projectId: selectedProjectId.value,
code: searchForm.code || undefined,
projectName: searchForm.projectName || undefined,
beginTime: searchForm.beginTime,
endTime: searchForm.endTime
})
@ -696,6 +756,10 @@ function handleReset() {
searchForm.proWorkshop = ''
searchForm.beginTime = null
searchForm.endTime = null
selectedProjectId.value = undefined
selectedProjectKeys.value = []
selectedProjectLabel.value = ''
projectTreeSearch.value = ''
handleSearch()
}
@ -800,22 +864,14 @@ 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){
kingdeeLoading.value = true
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 }
})
kingdeeTableData.value = mapKingdeeMoListToTableRows(Array.isArray(data) ? data : [])
} finally {
kingdeeLoading.value = false
}
@ -829,9 +885,8 @@ async function handleReorderForm(row:OrderItem, reCrawl:Boolean){
kingdeeOrderOtemId.value = row.id
kingdeeModalTitle.value = `金蝶生产订单${row.code ? ` - ${row.code}` : ''}`
kingdeeModalVisible.value = true
kingdeeLoading.value = true
reCrawlReorderForm(row.id, false)
kingdeeLoading.value = false
kingdeeTableData.value = []
await reCrawlReorderForm(row.id, reCrawl)
}
// 稿
@ -857,19 +912,14 @@ async function holdTemporarilySave(id: number, data: KingdeePrdMo[]) {
// }
// }
//
async function synchronizationSave(id: number, data: KingdeePrdMo[], callback:Function) {
// callback
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)
})
const rps = await orderItemApi.synchronizationSave(id, data)
callback(rps)
} catch (error) {
//
}
callback(null, error)
}
}
@ -1077,6 +1127,25 @@ function handleDelete(row: OrderItem) {
})
}
//
function handleStartWork(row: OrderItem) {
dialog.warning({
title: '提示',
content: `确定开工订单【${row.orderCode || row.name}】?开工后将生成工单。`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
await orderItemApi.startWork(row.id!)
message.success('开工成功')
loadData()
} catch (error) {
//
}
}
})
}
//
function handleBatchDelete() {
dialog.warning({
@ -1102,6 +1171,7 @@ async function handleExport() {
try {
const params: Record<string, any> = {}
if (selectedIds.value.length > 0) params.ids = selectedIds.value
if (searchForm.productionCode) params.productionCode = searchForm.productionCode
if (searchForm.code) params.code = searchForm.code
if (searchForm.projectName) params.projectName = searchForm.projectName
if (searchForm.orderCode) params.orderCode = searchForm.orderCode
@ -1159,9 +1229,14 @@ async function handleImportUpload({ file }: UploadCustomRequestOptions) {
//
async function loadDictOptions() {
try {
const data = await dictDataApi.listByType('work_shop')
workshopList.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue), class: d.listClass }))
} catch {}
}
onMounted(() => {
loadProjectTree()
loadData()
loadDictOptions()
@ -1172,6 +1247,41 @@ onMounted(() => {
</script>
<style scoped>
.order-layout {
display: flex;
gap: 12px;
height: 100%;
}
.order-tree-card {
width: 280px;
flex-shrink: 0;
}
.order-tree-header {
font-weight: bold;
}
.order-search {
margin-bottom: 10px;
}
.order-tree-wrapper {
height: calc(100vh - 280px);
overflow-y: auto;
}
.order-list-card {
flex: 1;
min-width: 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.search-form {
margin-bottom: 16px;
}

View File

@ -139,7 +139,14 @@
<div class="col-actions">
<n-button text type="primary" size="tiny" @click="openDetail(order)">详情</n-button>
<n-button text type="info" size="tiny" @click="openPlmModel(order)">模型</n-button>
<n-button text type="primary" size="tiny" disabled title="订单级编辑待支持">编辑</n-button>
<!-- 订单级编辑打开工序顺序重排弹窗不进单工序编辑 -->
<n-button
text
type="primary"
size="tiny"
:disabled="!hasPermission('biz:orderProcessPlan:edit')"
@click="openReorder(order)"
>编辑</n-button>
<n-dropdown :options="moreOptions" trigger="click" @select="(k) => onMoreSelect(k, order)">
<n-button text size="tiny">更多</n-button>
</n-dropdown>
@ -190,6 +197,11 @@
v-for="(proc, idx) in sortedProcessList(order.processList)"
:key="proc.planId ?? idx"
:process="proc"
:material-code="order.materialCode"
:order-code="order.orderCode"
:order-begin-time="order.beginTime"
:order-end-time="order.endTime"
:pro-workshop="order.proWorkshop"
:is-last="idx === (order.processList?.length ?? 0) - 1"
:can-edit="hasPermission('biz:orderItem:edit')"
:can-assign="hasPermission('biz:orderItem:assign')"
@ -246,6 +258,11 @@
v-for="(proc, idx) in (detailData.processList)"
:key="proc.planId ?? idx"
:process="proc"
:material-code="detailData.materialCode"
:order-code="detailData.orderCode"
:order-begin-time="detailData.beginTime"
:order-end-time="detailData.endTime"
:pro-workshop="detailData.proWorkshop"
:is-last="idx === (detailData.processList?.length ?? 0) - 1"
:can-edit="hasPermission('biz:orderItem:edit')"
:can-assign="hasPermission('biz:orderItem:assign')"
@ -261,7 +278,81 @@
</n-spin>
</n-modal>
<!-- 新增/编辑 -->
<!-- 订单级工序顺序重排拖拽 / 上移下移只提交 planId 顺序 -->
<n-modal
v-model:show="reorderVisible"
preset="card"
title="调整工序顺序"
style="width: 640px"
:mask-closable="false"
@after-leave="resetReorder"
>
<n-spin :show="reorderLoading">
<n-alert type="info" style="margin-bottom: 12px">
拖拽行或使用上移/下移调整顺序保存后按 102030 重写工序号存在派工工单或已有转出数量的工序不可调整
</n-alert>
<div v-if="reorderOrderMeta" class="reorder-meta">
<span>令号{{ reorderOrderMeta.mainCode || '-' }}</span>
<span>物料{{ reorderOrderMeta.materialName || '-' }}</span>
<span>数量{{ reorderOrderMeta.quantity ?? '-' }}</span>
</div>
<div v-if="!reorderList.length && !reorderLoading" class="reorder-empty">暂无工序可调整</div>
<VueDraggable
v-else
v-model="reorderList"
:animation="150"
handle=".reorder-drag-handle"
class="reorder-list"
>
<div
v-for="(proc, idx) in reorderList"
:key="proc.planId ?? idx"
class="reorder-item"
>
<n-icon class="reorder-drag-handle" size="18" :depth="3">
<MenuOutline />
</n-icon>
<span class="reorder-index">{{ idx + 1 }}</span>
<div class="reorder-body">
<div class="reorder-title">
<em v-if="proc.operNumber != null">{{ proc.operNumber }}</em>
{{ proc.processName || '-' }}
</div>
<div class="reorder-sub">
{{ proc.processCode || '-' }} · {{ proc.workOrderCode || '-' }}
</div>
</div>
<n-space :size="4">
<n-button
size="tiny"
quaternary
:disabled="idx === 0"
@click="moveReorderItem(idx, -1)"
>上移</n-button>
<n-button
size="tiny"
quaternary
:disabled="idx === reorderList.length - 1"
@click="moveReorderItem(idx, 1)"
>下移</n-button>
</n-space>
</div>
</VueDraggable>
</n-spin>
<template #footer>
<n-space justify="end">
<n-button @click="reorderVisible = false">取消</n-button>
<n-button
type="primary"
:loading="reorderSubmitLoading"
:disabled="!reorderList.length"
@click="submitReorder"
>保存顺序</n-button>
</n-space>
</template>
</n-modal>
<!-- 新增/编辑单工序字段顺序请用上方重排弹窗 -->
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 800px">
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="120px">
<n-grid :cols="2">
@ -276,8 +367,9 @@
</n-form-item>
</n-gi>
<n-gi>
<!-- 顺序请走订单行编辑重排避免单条改 sort 撞号断链 -->
<n-form-item label="工序顺序" path="sort">
<n-input v-model:value="formData.sort" placeholder="请输入工序顺序" />
<n-input v-model:value="formData.sort" placeholder="请通过订单编辑调整顺序" disabled />
</n-form-item>
</n-gi>
<n-gi>
@ -329,6 +421,16 @@
<span>{{dispatchform.quantity}}</span>
</n-gi>
</n-grid>
<n-grid :cols="2">
<n-gi>
<span class="pgClass">已完成</span>
<span>{{dispatchform.completedQty ?? 0}}</span>
</n-gi>
<n-gi>
<span class="pgClass">本次待派</span>
<span style="color: #2080f0; font-weight: 600">{{dispatchform.remainQty}}</span>
</n-gi>
</n-grid>
<n-grid :cols="2">
<n-gi>
<span class="pgClass">开始时间</span>
@ -570,7 +672,9 @@ import {
DownloadOutline, ChevronDownOutline, CheckmarkCircleOutline, CheckmarkCircle,
EllipseOutline, TimeOutline, PrintOutline, SyncOutline, PeopleOutline,
GitBranchOutline, CloseCircleOutline, LockClosedOutline, TrashOutline, EyeSharp,
MenuOutline,
} from '@vicons/ionicons5'
import { VueDraggable } from 'vue-draggable-plus'
import {
orderProcessPlanApi,
toOrderProcessPlanPayload,
@ -612,6 +716,15 @@ const detailVisible = ref(false)
const detailLoading = ref(false)
const detailData = ref<OrderProcessPlanVO | null>(null)
/** 订单级工序重排弹窗状态 */
const reorderVisible = ref(false)
const reorderLoading = ref(false)
const reorderSubmitLoading = ref(false)
const reorderOrderItemId = ref<number | null>(null)
const reorderOrderMeta = ref<OrderProcessPlanVO | null>(null)
/** 弹窗内可拖拽的工序列表(按当前展示顺序) */
const reorderList = ref<ProcessPlanItemVO[]>([])
const modalVisible = ref(false)
const modalTitle = ref('')
const formRef = ref()
@ -628,7 +741,7 @@ const dispatchmodal = ref(false)
const dispatchLoading = ref(false)
const dispatchformRef = ref()
const dispatchform = reactive<any>({
id: '', name: '', beginTime: '', endTime: '', quantity: '',workCenterName:'',proWorkshop:'',
id: '', name: '', beginTime: '', endTime: '', quantity: '',workCenterName:'',
list: [{ sectionId:'',deviceId:'', quantity: '' }],
})
const dispatchrules = {}
@ -1012,6 +1125,79 @@ async function openDetail(order: OrderProcessPlanVO) {
}
}
/** 打开订单级工序顺序调整(拉详情,按 operNumber 排序后供拖拽) */
async function openReorder(order: OrderProcessPlanVO) {
if (!order.orderItemId) {
message.warning('缺少生产订单id')
return
}
reorderVisible.value = true
reorderLoading.value = true
reorderOrderItemId.value = order.orderItemId
reorderOrderMeta.value = order
reorderList.value = []
try {
const detail = await orderProcessPlanApi.detail(order.orderItemId)
reorderOrderMeta.value = detail
// operNumber sort
reorderList.value = sortedProcessList(detail.processList).map((p) => ({ ...p }))
if (!reorderList.value.length) {
message.warning('当前订单暂无工序计划')
}
} catch {
//
reorderList.value = sortedProcessList(order.processList).map((p) => ({ ...p }))
} finally {
reorderLoading.value = false
}
}
function resetReorder() {
reorderOrderItemId.value = null
reorderOrderMeta.value = null
reorderList.value = []
reorderSubmitLoading.value = false
}
/** 上移 / 下移delta: -1 上移,+1 下移) */
function moveReorderItem(index: number, delta: number) {
const next = index + delta
if (next < 0 || next >= reorderList.value.length) return
const list = [...reorderList.value]
const [item] = list.splice(index, 1)
list.splice(next, 0, item)
reorderList.value = list
}
/** 提交重排:只传 orderItemId + planId 顺序 */
async function submitReorder() {
if (!reorderOrderItemId.value) {
message.warning('缺少生产订单id')
return
}
if (!reorderList.value.length) {
message.warning('暂无工序可保存')
return
}
const missing = reorderList.value.some((p) => p.planId == null)
if (missing) {
message.error('存在缺少 planId 的工序,请刷新后重试')
return
}
reorderSubmitLoading.value = true
try {
await orderProcessPlanApi.reorder({
orderItemId: reorderOrderItemId.value,
items: reorderList.value.map((p) => ({ planId: p.planId! })),
})
message.success('工序顺序已更新')
reorderVisible.value = false
loadData()
} finally {
reorderSubmitLoading.value = false
}
}
function handleAdd() {
modalTitle.value = '新增工序计划'
Object.assign(formData, defaultFormData)
@ -1063,26 +1249,33 @@ async function handleSubmit() {
function disphand(process: ProcessPlanItemVO, order?: OrderProcessPlanVO) {
const entity = processItemToEntity(process, order?.orderItemId,order?.proWorkshop)
const planQty = Number(entity.quantity) || 0
const completedQty = Number(process.completedQty) || 0
const remainQty = Math.max(0, planQty - completedQty)
dispatchmodal.value = true
dispatchformRef.value?.restoreValidation()
dispatchform.id = entity.id
dispatchform.name = entity.name
dispatchform.beginTime = entity.beginTime
dispatchform.endTime = entity.endTime
// beginTime/endTime
dispatchform.beginTime = entity.beginTime || order?.beginTime || ''
dispatchform.endTime = entity.endTime || order?.endTime || ''
// quantity
dispatchform.quantity = entity.quantity
dispatchform.completedQty = completedQty
dispatchform.remainQty = remainQty
dispatchform.orderItemId = entity.orderItemId
dispatchform.sort = entity.sort
dispatchform.workCenterName = entity.workCenterName
dispatchform.workShop = entity.proWorkshop
dispatchform.list = [{ sectionId:'',deviceId:'', quantity: '' }]
console.log(dispatchform)
dispatchform.list = [{ sectionId: '', deviceId: '', quantity: remainQty || '' }]
}
function addpgnum() {
dispatchform.list.push({ sectionId:'',deviceId:'', quantity: '' })
dispatchform.list.push({ sectionId: '', deviceId: '', quantity: '' })
}
function deletenum(index: number) {
@ -1091,18 +1284,27 @@ function deletenum(index: number) {
function dispatchSubmit() {
let total = 0
dispatchform.list.forEach((n: any) => {
dispatchform.list.forEach((n: any) => {
total += Number(n.quantity) || 0
})
dispatchformRef.value?.validate((v: boolean) => {
if (v) return
if (total !== Number(dispatchform.quantity)) {
message.error('派工总数量需等于生产总数', { duration: 4000 })
const remainQty = Number(dispatchform.remainQty)
if (remainQty <= 0) {
message.warning('当前工序已全部完成,无需再派工')
return
}
dispatchformRef.value?.validate((errors: any) => {
// Naive UI errors
if (errors) return
if (total !== remainQty) {
message.error(`派工总数量需等于待派数量 ${remainQty}(计划 ${dispatchform.quantity},已完成 ${dispatchform.completedQty ?? 0}`, {
duration: 4000,
})
return
}
dispatchLoading.value = true
// quantity
orderProcessPlanApi.assignWork(dispatchform).then(() => {
message.success('派工成功')
dispatchmodal.value = false
@ -1476,4 +1678,83 @@ onMounted(loadData)
font-size: 12px;
color: #8c8c8c;
}
.reorder-meta {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 12px;
font-size: 13px;
color: #595959;
}
.reorder-empty {
padding: 24px 0;
text-align: center;
color: #8c8c8c;
font-size: 13px;
}
.reorder-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 420px;
overflow-y: auto;
}
.reorder-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: #fafbfc;
border: 1px solid #eef0f3;
border-radius: 8px;
}
.reorder-drag-handle {
cursor: grab;
flex-shrink: 0;
color: #8c8c8c;
}
.reorder-drag-handle:active {
cursor: grabbing;
}
.reorder-index {
width: 22px;
text-align: center;
font-size: 13px;
font-weight: 600;
color: #2080f0;
flex-shrink: 0;
}
.reorder-body {
flex: 1;
min-width: 0;
}
.reorder-title {
font-size: 14px;
font-weight: 600;
color: #1a1a1a;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.reorder-title em {
font-style: normal;
margin-right: 6px;
color: #2080f0;
}
.reorder-sub {
margin-top: 2px;
font-size: 12px;
color: #8c8c8c;
}
</style>

View File

@ -2,7 +2,13 @@
<div class="process-card-wrap">
<div class="process-card" :class="themeClass">
<div class="card-head">
<span class="card-title">{{ process.processName || '-' }}</span>
<div class="card-title-block">
<span class="card-title">
<em v-if="process.operNumber != null" class="card-oper">{{ process.operNumber }}</em>
{{ process.processName || '-' }}
</span>
<span v-if="materialCode" class="card-material">{{ materialCode }}</span>
</div>
<n-tag size="small" :bordered="false" :type="statusTag.type">{{ statusTag.label }}</n-tag>
</div>
@ -39,22 +45,28 @@
</div>
<div class="card-flow">
<div class="flow-item">
<span>转入</span>
<strong>{{ process.transferInQty ?? 0 }}</strong>
</div>
<div class="flow-item">
<span>剩余物料</span>
<strong>{{ remainQty }}</strong>
</div>
<div class="flow-item">
<span>下道</span>
<span></span>
<strong>{{ process.transferOutQty ?? 0 }}</strong>
</div>
</div>
<div class="card-meta">
<div class="meta-line"><span>生产订单</span><em>{{ orderCode || '-' }}</em></div>
<div class="meta-line"><span>工单号</span><em>{{ process.workOrderCode || '-' }}</em></div>
<div class="meta-line"><span>人员</span><em>{{ personnel }}</em></div>
<div class="meta-line"><span>车间</span><em>{{ process.departmentName || '-' }}</em></div>
<div class="meta-line"><span>车间</span><em>{{ workshopText }}</em></div>
<div class="meta-line"><span>工位</span><em>{{ process.workCenterName || '-' }}</em></div>
<div class="meta-line"><span>设备</span><em>-</em></div>
<div class="meta-line"><span>设备</span><em>{{ deviceText }}</em></div>
<div class="meta-line"><span>计划时间</span><em>{{ planTimeText }}</em></div>
<div class="meta-line"><span>实际开始</span><em>{{ process.actualStartTime || '-' }}</em></div>
</div>
@ -110,6 +122,16 @@ import { PROCESS_STATUS_MAP, type ProcessPlanItemVO } from '@/api/orderProcessPl
const props = defineProps<{
process: ProcessPlanItemVO
/** 物料编码(订单头) */
materialCode?: string
/** 生产订单编号(订单头 orderCode */
orderCode?: string
/** 订单计划开始(订单头 beginTime工序无计划时间时回退 */
orderBeginTime?: string
/** 订单计划结束(订单头 endTime */
orderEndTime?: string
/** 生产车间(订单头 proWorkshop */
proWorkshop?: string
isLast?: boolean
canEdit?: boolean
canAssign?: boolean
@ -171,15 +193,39 @@ const railColor = computed(() => {
const personnel = computed(() => {
if (props.process.assignByName) return props.process.assignByName
const list = props.process.assignWorkList
if (list?.length) return `${list.length} 人派工`
if (list?.length) {
const names = list
.map((a: any) => a.userName || a.userId)
.filter(Boolean)
if (names.length) return names.join('、')
return `${list.length} 人派工`
}
return '-'
})
const workshopText = computed(() => {
return props.process.departmentName || props.proWorkshop || '-'
})
const deviceText = computed(() => {
const list = props.process.assignWorkList as any[] | undefined
if (!list?.length) return '-'
const devices = list
.map((a) => a.deviceName || a.deviceId)
.filter(Boolean)
return devices.length ? [...new Set(devices)].join('、') : '-'
})
const planTimeText = computed(() => {
const start = props.process.planStartTime
const end = props.process.planFinishTime
// 退 beginTime/endTime
const start = props.process.planStartTime || props.process.beginTime || props.orderBeginTime
const end = props.process.planFinishTime || props.process.endTime || props.orderEndTime
if (!start && !end) return '-'
if (start && end) return `${start.slice(0, 16)} ~ ${end.slice(11, 16)}`
if (start && end) {
const startText = start.length >= 16 ? start.slice(0, 16) : start
const endText = end.length >= 16 ? end.slice(11, 16) : end
return `${startText} ~ ${endText}`
}
return start || end || '-'
})
</script>
@ -252,6 +298,29 @@ const planTimeText = computed(() => {
white-space: nowrap;
}
.card-oper {
font-style: normal;
font-weight: 700;
margin-right: 4px;
opacity: 0.95;
}
.card-title-block {
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.card-material {
font-size: 11px;
font-weight: 500;
opacity: 0.85;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-progress {
display: flex;
justify-content: center;

View File

@ -233,6 +233,16 @@
<n-input v-model:value="dispatchform.quantity" disabled />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="已完成">
<n-input :value="String(dispatchform.completedQty ?? 0)" disabled />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="本次待派">
<n-input :value="String(dispatchform.remainQty ?? 0)" disabled />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="开始时间">
<n-input v-model:value="dispatchform.beginTime" disabled />
@ -816,6 +826,8 @@ let dispatchform = reactive<any>({
endTime:'',
userid:'',
quantity:'',
completedQty: 0,
remainQty: 0,
list:[{
userId:'',
quantity:''
@ -866,17 +878,22 @@ function deletenum(index:any) {
function disphand(process: ProcessPlanItemVO, order?: OrderProcessPlanVO) {
const entity = processItemToEntity(process, order?.orderItemId)
const planQty = Number(entity.quantity) || 0
const completedQty = Number(process.completedQty) || 0
const remainQty = Math.max(0, planQty - completedQty)
dispatchmodal.value = true
dispatchformRef.value?.restoreValidation()
dispatchform.id = entity.id
dispatchform.name = entity.name
dispatchform.beginTime = entity.beginTime
dispatchform.endTime = entity.endTime
dispatchform.beginTime = entity.beginTime || order?.beginTime || ''
dispatchform.endTime = entity.endTime || order?.endTime || ''
dispatchform.quantity = entity.quantity
dispatchform.completedQty = completedQty
dispatchform.remainQty = remainQty
dispatchform.userid = ''
dispatchform.list = [{
userId: '',
quantity: '',
quantity: remainQty || '',
}]
}
@ -885,10 +902,15 @@ function dispatchSubmit() {
dispatchform.list.forEach((n:any) => {
total = n.quantity*1+total
})
const remainQty = Number(dispatchform.remainQty)
if (remainQty <= 0) {
message.warning('当前工序已全部完成,无需再派工')
return
}
dispatchformRef.value?.validate((v:any) => {
if(!v){
if(total != dispatchform.quantity){
message.error('派工总数量需等于生产总数',{
if(total != remainQty){
message.error(`派工总数量需等于待派数量 ${remainQty}(计划 ${dispatchform.quantity},已完成 ${dispatchform.completedQty ?? 0}`,{
duration:4000
})
}else{

View File

@ -159,10 +159,9 @@
const res = await sectionApi.list({sectionName:props.section,workShop:props.workShop})
const mesSection = res.MesSection;
const mesDevice = res.MesDevice
console.log(mesSection);
sectionList.push({
label:mesSection.sectionName,
label:mesSection.deptName,
value:mesSection.id
})
props.obj.sectionId = mesSection.id

View File

@ -150,7 +150,8 @@
>
<KingdeeVue
:kingdeeTableData="kingdeeTableData"
:selectedId="kingdeeCurrentProjectId"
:selectedId="kingdeeCurrentProjectId"
:loading="kingdeeLoading"
@close-modal="kingdeeModalVisible = false"
@holdTemporarilyCallback="handleSaveDraft"
@synchronizationCallback="handleSyncOrderAndPlan"
@ -194,7 +195,8 @@
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 { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline, EyeOutline, CalendarOutline } from '@vicons/ionicons5'
import { orderProjectApi, type OrderProject, type KingdeePrdMo, type KingdeeProcessRoute, normalizeKingdeeProcessRoute } from '@/api/orderProject'
import { orderProjectApi, type OrderProject, type KingdeePrdMo, type KingdeeProcessRoute } from '@/api/orderProject'
import { mapKingdeeMoListToTableRows } from '@/utils/kingdeeSchedule'
import { dictDataApi } from '@/api/org'
import { useUserStore } from '@/stores/user'
@ -660,21 +662,14 @@ function handleGanttSchedule() {
}
async function reCrawlReorderForm(id:number,reCrawl:Boolean){
kingdeeLoading.value = true
try {
const data = await orderProjectApi.getKingdeeOrder(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) => normalizeKingdeeProcessRoute({
...r,
planStartTime: r.planStartTime ?? null,
planFinishTime: r.planFinishTime ?? null,
}))
return { ...rest, processRoutes: routes }
})
kingdeeTableData.value = mapKingdeeMoListToTableRows(Array.isArray(data) ? data : [])
if (kingdeeTableData.value.length === 0) {
message.info('未查询到金蝶生产订单数据')
} else if (reCrawl) {
message.success('重新读取成功')
}
} finally {
kingdeeLoading.value = false

View File

@ -311,7 +311,7 @@ const defaultFormData: QcItem = {
plans: '',
description: '',
createby: undefined,
status:undefined
status:0
}
const formData = reactive<QcItem>({ ...defaultFormData })
@ -319,6 +319,8 @@ const formData = reactive<QcItem>({ ...defaultFormData })
//
const formRules = {
name: [{ required: true, message: '请输入质检项名称', trigger: 'blur' }],
spec: [{ required: true, message: '请输入质检项规格', trigger: 'blur' }],
}
//
@ -497,15 +499,16 @@ async function handleSubmit() {
if (typeof submitData.updateTime === 'number') {
submitData.updateTime = new Date(submitData.updateTime).toISOString().slice(0, 19).replace('T', ' ')
}
if (submitData.id) {
await qcItemApi.update(submitData)
message.success('修改成功')
} else {
await qcItemApi.create(submitData)
message.success('新增成功')
}
modalVisible.value = false
loadData()
console.log(submitData)
// if (submitData.id) {
// await qcItemApi.update(submitData)
// message.success('')
// } else {
// await qcItemApi.create(submitData)
// message.success('')
// }
// modalVisible.value = false
// loadData()
} catch (error) {
//
}

View File

@ -79,13 +79,19 @@
<n-input v-model:value="formData.deviceName" placeholder="请输入设备名称" />
</n-form-item>
<n-form-item label="所属工段" path="sectionId">
<n-select
v-model:value="formData.sectionId"
:options="sectionList"
placeholder="请选择所属工段"
clearable
style="width: 200px"
/>
<!-- <n-select-->
<!-- v-model:value="formData.sectionId"-->
<!-- :options="sectionList"-->
<!-- placeholder="请选择所属工段"-->
<!-- clearable-->
<!-- style="width: 200px"-->
<!-- />-->
<n-tree-select
v-model:value="formData.sectionId"
cascade
checkable
:options="sectionList"
/>
</n-form-item>
<n-form-item label="备注" path="remark">
<n-input v-model:value="formData.remark" type="textarea" placeholder="请输入备注" />
@ -144,8 +150,9 @@ import { NButton, NSpace,NTag, NIcon, NUpload, useMessage, useDialog, type DataT
import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline } from '@vicons/ionicons5'
import { deviceApi, type Device } from '@/api/device'
import { dictDataApi } from '@/api/org'
import {deptApi, dictDataApi} from '@/api/org'
import {sectionApi, type Section} from '@/api/section'
import {TreeNode} from "echarts/types/src/data/Tree";
const message = useMessage()
@ -208,6 +215,7 @@ const columns: DataTableColumns<Device> = [
{ type: 'selection' },
{ title: '工位编码', key: 'deviceCode' },
{ title: '工位名称', key: 'deviceName' },
{ title: '工位类型', key: 'deviceType' },
{ title: '所属工段', key: 'sectionName' },
{ title: '状态', key: 'status',
render:(row) =>{
@ -431,8 +439,24 @@ async function loadDictOptions() {
//
async function loadSection() {
const data = await sectionApi.listNoParam()
sectionList = data.map((d:any)=> ({ label: d.sectionName, value: (Number(d.id) || d.id) }))
const data = await deptApi.tree()
sectionList = buildOptions(data);
}
function buildOptions(d: any[]): TreeNode[] {
return d.map(item => {
// id0||
const idNum = Number(item.id);
const key = !isNaN(idNum) ? idNum : item.id;
return {
label: item.deptName,
key,
//
children: Array.isArray(item.children) && item.children.length
? buildOptions(item.children)
: undefined
};
});
}
onMounted(() => {

View File

@ -18,7 +18,7 @@
:data="AssingWorkTableData"
:loading="tableLoading"
:row-key="RowKey"
:scroll-x="600"
:scroll-x="1800"
:max-height="1000"
size="small"
style="margin-bottom:15px"

View File

@ -507,7 +507,7 @@ async function loadData() {
async function loadRoles() {
try {
const roles = await roleApi.roleList()
const roles = await roleApi.list()
roleOptions.value = roles.map((role: SysRole) => ({
label: role.name,
value: role.id!
@ -519,7 +519,7 @@ async function loadRoles() {
async function loadPostOptions() {
try {
const posts = await postApi.postList()
const posts = await postApi.list()
postOptions.value = posts.map(p => ({
label: p.postName,
value: p.id!

View File

@ -20,7 +20,8 @@ export default defineConfig({
port: 3000,
proxy: {
'/api': {
target:'http://192.168.12.4:8888/',
target:'http://192.168.12.12:8888/', //'http://192.168.5.230:8888',
//target:'http://192.168.12.4:8888/',
//target:'http://192.168.12.230:8888',
//target:'http://192.168.5.230:8888',
//target:'http://192.168.5.232:8888/',