委外,设备状态
This commit is contained in:
parent
852d93e6fd
commit
fdd0da6b46
100
mes-ui/src/api/outsourcing.ts
Normal file
100
mes-ui/src/api/outsourcing.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
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' })
|
||||||
|
}
|
||||||
|
}
|
||||||
492
mes-ui/src/views/biz/outsourcing/index.vue
Normal file
492
mes-ui/src/views/biz/outsourcing/index.vue
Normal file
@ -0,0 +1,492 @@
|
|||||||
|
<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>
|
||||||
@ -8,16 +8,37 @@ export interface Device{
|
|||||||
deviceTypeId:number,
|
deviceTypeId:number,
|
||||||
deviceType:string,
|
deviceType:string,
|
||||||
sectionId:number,
|
sectionId:number,
|
||||||
|
sectionName?:string,
|
||||||
spec:string,
|
spec:string,
|
||||||
manufacturer:string,
|
manufacturer:string,
|
||||||
manufactureDate:string,
|
manufactureDate:string,
|
||||||
workshopId:number,
|
workshopId:number,
|
||||||
remark:string,
|
remark:string,
|
||||||
status:number,
|
status:number,
|
||||||
|
deviceFlag?:string,
|
||||||
|
/** 新代 SynFactory equipID */
|
||||||
|
synEquipId?: number | null,
|
||||||
createTime:string,
|
createTime:string,
|
||||||
updateTime:string
|
updateTime:string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DeviceRealtimeVO {
|
||||||
|
deviceId?: number
|
||||||
|
deviceCode?: string
|
||||||
|
displayName: string
|
||||||
|
synEquipId: number
|
||||||
|
bound: boolean
|
||||||
|
connected?: number | null
|
||||||
|
cncStatus?: number | null
|
||||||
|
cncStatusText?: string
|
||||||
|
partCount?: number | null
|
||||||
|
mainProgram?: string | null
|
||||||
|
alarmCount?: number
|
||||||
|
alarmMessages?: string[]
|
||||||
|
dataTimestamp?: number
|
||||||
|
sectionName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const deviceApi = {
|
export const deviceApi = {
|
||||||
|
|
||||||
@ -25,6 +46,7 @@ export const deviceApi = {
|
|||||||
page:number,
|
page:number,
|
||||||
pageSize:number,
|
pageSize:number,
|
||||||
deviceCode:any
|
deviceCode:any
|
||||||
|
deviceFlag?: string
|
||||||
}) {
|
}) {
|
||||||
return request({
|
return request({
|
||||||
url:"/biz/device/page",
|
url:"/biz/device/page",
|
||||||
@ -33,6 +55,21 @@ export const deviceApi = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** 设备机联网看板:MES名称 + 新代即时状态 */
|
||||||
|
realtimeBoard(equipIds?: string | number[]) {
|
||||||
|
const params: Record<string, any> = {}
|
||||||
|
if (Array.isArray(equipIds) && equipIds.length) {
|
||||||
|
params.equipIds = equipIds.join(',')
|
||||||
|
} else if (typeof equipIds === 'string' && equipIds) {
|
||||||
|
params.equipIds = equipIds
|
||||||
|
}
|
||||||
|
return request<DeviceRealtimeVO[]>({
|
||||||
|
url: '/biz/device/realtime-board',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
create(data:Device) {
|
create(data:Device) {
|
||||||
return request({
|
return request({
|
||||||
url:"/biz/device",
|
url:"/biz/device",
|
||||||
|
|||||||
@ -64,6 +64,8 @@ export interface ProcessPlanItemVO {
|
|||||||
|
|
||||||
planId?: number
|
planId?: number
|
||||||
|
|
||||||
|
workOrderCode?: string
|
||||||
|
|
||||||
processCode?: string
|
processCode?: string
|
||||||
|
|
||||||
operNumber?: number
|
operNumber?: number
|
||||||
@ -442,5 +444,60 @@ export const orderProcessPlanApi = {
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** 委外第一步:获取可选工序 */
|
||||||
|
getOutsourcingOptions(processPlanId: number) {
|
||||||
|
return request<OutsourcingOptions>({
|
||||||
|
url: `/biz/orderProcessPlan/outsourcing/options/${processPlanId}`,
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 委外第二步:创建委外单 */
|
||||||
|
createOutsourcing(data: OutsourcingCreate) {
|
||||||
|
return request<number>({
|
||||||
|
url: '/biz/orderProcessPlan/outsourcing',
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 委外可选工序项 */
|
||||||
|
export interface OutsourcingProcessOption {
|
||||||
|
planId?: number
|
||||||
|
workOrderCode?: string
|
||||||
|
processCode?: string
|
||||||
|
operNumber?: number
|
||||||
|
processName?: string
|
||||||
|
quantity?: number
|
||||||
|
outsource?: number
|
||||||
|
planStartTime?: string
|
||||||
|
planFinishTime?: string
|
||||||
|
current?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 委外可选工序(第一步) */
|
||||||
|
export interface OutsourcingOptions {
|
||||||
|
orderItemId?: number
|
||||||
|
mainCode?: string
|
||||||
|
materialCode?: string
|
||||||
|
materialName?: string
|
||||||
|
orderCode?: string
|
||||||
|
quantity?: number
|
||||||
|
currentProcessPlanId?: number
|
||||||
|
processList?: OutsourcingProcessOption[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建委外单(第二步) */
|
||||||
|
export interface OutsourcingCreate {
|
||||||
|
processPlanId: number
|
||||||
|
supplierId?: number
|
||||||
|
supplierName: string
|
||||||
|
quantity: number
|
||||||
|
unitPrice?: number
|
||||||
|
planStartTime?: string
|
||||||
|
planEndTime?: string
|
||||||
|
remark?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
105
src/api/outsourcing.ts
Normal file
105
src/api/outsourcing.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { request } from '@/utils/request'
|
||||||
|
|
||||||
|
/** 委外工单 */
|
||||||
|
export interface Outsourcing {
|
||||||
|
id?: number
|
||||||
|
outsourceCode?: string
|
||||||
|
name?: string
|
||||||
|
code?: string
|
||||||
|
businessType?: number
|
||||||
|
businessId?: number
|
||||||
|
businessCode?: string
|
||||||
|
orderItemId?: number
|
||||||
|
quantity?: number
|
||||||
|
supplierId?: number
|
||||||
|
supplierName?: string
|
||||||
|
unitPrice?: number | string
|
||||||
|
totalAmount?: number | string
|
||||||
|
beginPreparationTime?: string
|
||||||
|
endPreparationTime?: string
|
||||||
|
beginTime?: string
|
||||||
|
endTime?: string
|
||||||
|
remark?: string
|
||||||
|
createTime?: string
|
||||||
|
createBy?: string
|
||||||
|
overdue?: number
|
||||||
|
/** 0草稿 1执行中 2已完成 3已关闭 */
|
||||||
|
status?: number
|
||||||
|
/** 1正常 2紧急 3低 */
|
||||||
|
priority?: number
|
||||||
|
issuedQuantity?: number | string
|
||||||
|
receivedQuantity?: number | string
|
||||||
|
qualifiedQuantity?: number | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OUTSOURCE_STATUS_MAP: Record<number, { label: string; type: 'default' | 'info' | 'success' | 'warning' | 'error' }> = {
|
||||||
|
0: { label: '草稿', type: 'default' },
|
||||||
|
1: { label: '执行中', type: 'info' },
|
||||||
|
2: { label: '已完成', type: 'success' },
|
||||||
|
3: { label: '已关闭', type: 'default' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OUTSOURCE_PRIORITY_MAP: Record<number, { label: string; type: 'default' | 'info' | 'success' | 'warning' | 'error' }> = {
|
||||||
|
1: { label: '正常', type: 'info' },
|
||||||
|
2: { label: '紧急', type: 'error' },
|
||||||
|
3: { label: '低', type: 'default' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export const outsourcingApi = {
|
||||||
|
page(params: {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
id?: number
|
||||||
|
name?: string
|
||||||
|
businessType?: number
|
||||||
|
outsourceCode?: string
|
||||||
|
supplierName?: string
|
||||||
|
status?: number
|
||||||
|
}) {
|
||||||
|
return request<{ list: Outsourcing[]; total: number; page: number; pageSize: number }>({
|
||||||
|
url: '/biz/outsourcing/page',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
detail(id: number | string) {
|
||||||
|
return request<Outsourcing>({ 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: number[]) {
|
||||||
|
return request({ url: `/biz/outsourcing/${ids.join(',')}`, method: 'delete' })
|
||||||
|
},
|
||||||
|
|
||||||
|
export(params?: { ids?: number[]; id?: number; name?: string; businessType?: number }) {
|
||||||
|
const p: Record<string, unknown> = {}
|
||||||
|
if (params?.ids?.length) p.ids = params.ids.join(',')
|
||||||
|
if (params?.id != null) p.id = params.id
|
||||||
|
if (params?.name) p.name = params.name
|
||||||
|
if (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' })
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -148,6 +148,24 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/views/biz/orderProcessPlan/index.vue'),
|
component: () => import('@/views/biz/orderProcessPlan/index.vue'),
|
||||||
meta: { title: '工序计划', icon: 'ListOutline' }
|
meta: { title: '工序计划', icon: 'ListOutline' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'biz/orderProcessPlan/board',
|
||||||
|
name: 'orderProcessPlanBoard',
|
||||||
|
component: () => import('@/views/biz/orderProcessPlan/board.vue'),
|
||||||
|
meta: { title: '工序计划看板', icon: 'ListOutline', activeMenu: '/biz/orderProcessPlan' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'biz/device',
|
||||||
|
name: 'device',
|
||||||
|
component: () => import('@/views/biz/device/index.vue'),
|
||||||
|
meta: { title: '设备管理', icon: 'HardwareChipOutline' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'biz/device/board',
|
||||||
|
name: 'deviceRealtimeBoard',
|
||||||
|
component: () => import('@/views/biz/device/board.vue'),
|
||||||
|
meta: { title: '设备状态看板', icon: 'GridOutline', activeMenu: '/biz/device' }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'biz/user',
|
path: 'biz/user',
|
||||||
name: 'user',
|
name: 'user',
|
||||||
@ -264,6 +282,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/views/biz/flowingAround/index.vue'),
|
component: () => import('@/views/biz/flowingAround/index.vue'),
|
||||||
meta: { title: '流转记录表', icon: 'ListOutline' }
|
meta: { title: '流转记录表', icon: 'ListOutline' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'biz/outsourcing',
|
||||||
|
name: 'outsourcing',
|
||||||
|
component: () => import('@/views/biz/outsourcing/index.vue'),
|
||||||
|
meta: { title: '工序委外', icon: 'ListOutline' }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// 开发工具
|
// 开发工具
|
||||||
path: 'tool/gen',
|
path: 'tool/gen',
|
||||||
|
|||||||
@ -116,8 +116,8 @@ async function decryptResponseData(data: string): Promise<any> {
|
|||||||
// 创建axios实例
|
// 创建axios实例
|
||||||
// 后端 API 统一使用 /api 前缀
|
// 后端 API 统一使用 /api 前缀
|
||||||
const service: AxiosInstance = axios.create({
|
const service: AxiosInstance = axios.create({
|
||||||
// baseURL:'/api',
|
baseURL:'/api',
|
||||||
baseURL: BASE_API,
|
//baseURL: BASE_API,
|
||||||
timeout: 30000
|
timeout: 30000
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
438
src/views/biz/device/board.vue
Normal file
438
src/views/biz/device/board.vue
Normal file
@ -0,0 +1,438 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<n-card>
|
||||||
|
<div class="table-toolbar">
|
||||||
|
<n-space align="center">
|
||||||
|
<n-button type="primary" :loading="loading" @click="loadBoard">
|
||||||
|
<template #icon><n-icon><RefreshOutline /></n-icon></template>
|
||||||
|
刷新
|
||||||
|
</n-button>
|
||||||
|
<n-switch v-model:value="autoRefresh" @update:value="onAutoRefreshChange">
|
||||||
|
<template #checked>自动刷新 15s</template>
|
||||||
|
<template #unchecked>手动刷新</template>
|
||||||
|
</n-switch>
|
||||||
|
</n-space>
|
||||||
|
<n-button quaternary @click="router.push('/biz/device')">
|
||||||
|
返回设备管理
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-row">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-num">{{ list.length }}</span>
|
||||||
|
<span class="stat-label">设备总数</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item running">
|
||||||
|
<span class="stat-dot running" />
|
||||||
|
<span class="stat-num">{{ countByStatus(1) }}</span>
|
||||||
|
<span class="stat-label">加工中</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item idle">
|
||||||
|
<span class="stat-dot idle" />
|
||||||
|
<span class="stat-num">{{ countByStatus(0) }}</span>
|
||||||
|
<span class="stat-label">闲置</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item alarm">
|
||||||
|
<span class="stat-dot alarm" />
|
||||||
|
<span class="stat-num">{{ countByStatus(2) }}</span>
|
||||||
|
<span class="stat-label">警报</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item offline">
|
||||||
|
<span class="stat-dot offline" />
|
||||||
|
<span class="stat-num">{{ offlineCount }}</span>
|
||||||
|
<span class="stat-label">离线</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-spin :show="loading">
|
||||||
|
<n-empty v-if="!loading && list.length === 0" description="暂无设备状态数据" />
|
||||||
|
<div v-else class="card-grid">
|
||||||
|
<div
|
||||||
|
v-for="item in list"
|
||||||
|
:key="item.synEquipId"
|
||||||
|
class="device-card"
|
||||||
|
:class="cardClass(item)"
|
||||||
|
>
|
||||||
|
<div class="card-head">
|
||||||
|
<div class="head-left">
|
||||||
|
<div class="status-lamp" :class="lampClass(item)" />
|
||||||
|
<div class="name-block">
|
||||||
|
<div class="name">{{ item.displayName }}</div>
|
||||||
|
<div class="meta">
|
||||||
|
<span v-if="item.deviceCode">{{ item.deviceCode }}</span>
|
||||||
|
<span v-if="item.deviceCode && item.sectionName"> · </span>
|
||||||
|
<span v-if="item.sectionName">{{ item.sectionName }}</span>
|
||||||
|
<span v-if="!item.deviceCode && !item.sectionName">设备监控</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<n-tag :type="statusTagType(item)" size="small">
|
||||||
|
{{ item.cncStatusText || '未知' }}
|
||||||
|
</n-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="metric-grid">
|
||||||
|
<div class="metric">
|
||||||
|
<div class="metric-label">当前程序</div>
|
||||||
|
<div class="metric-value program">{{ item.mainProgram || '—' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<div class="metric-label">累计产量</div>
|
||||||
|
<div class="metric-value">{{ item.partCount ?? '—' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<div class="metric-label">通讯状态</div>
|
||||||
|
<div class="metric-value">
|
||||||
|
<span class="link-status" :class="item.connected === 1 ? 'on' : 'off'">
|
||||||
|
{{ item.connected === 1 ? '在线' : item.connected === 0 ? '离线' : '—' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<div class="metric-label">运行状态</div>
|
||||||
|
<div class="metric-value">{{ item.cncStatusText || '—' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="item.alarmCount" class="alarm-box">
|
||||||
|
<n-icon size="14"><WarningOutline /></n-icon>
|
||||||
|
<span>{{ (item.alarmMessages || []).slice(0, 2).join(';') || `当前警报 ${item.alarmCount} 条` }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-spin>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import { RefreshOutline, WarningOutline } from '@vicons/ionicons5'
|
||||||
|
import { deviceApi, type DeviceRealtimeVO } from '@/api/device'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const list = ref<DeviceRealtimeVO[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const autoRefresh = ref(true)
|
||||||
|
let timer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
|
const offlineCount = computed(() => list.value.filter((i) => i.connected === 0).length)
|
||||||
|
|
||||||
|
function countByStatus(status: number) {
|
||||||
|
return list.value.filter((i) => i.connected !== 0 && i.cncStatus === status).length
|
||||||
|
}
|
||||||
|
|
||||||
|
function cardClass(item: DeviceRealtimeVO) {
|
||||||
|
if (item.connected === 0) return 'is-offline'
|
||||||
|
if (item.cncStatus === 2) return 'is-alarm'
|
||||||
|
if (item.cncStatus === 1) return 'is-running'
|
||||||
|
if (item.cncStatus === 0) return 'is-idle'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function lampClass(item: DeviceRealtimeVO) {
|
||||||
|
if (item.connected === 0) return 'offline'
|
||||||
|
if (item.cncStatus === 2) return 'alarm'
|
||||||
|
if (item.cncStatus === 1) return 'running'
|
||||||
|
if (item.cncStatus === 0) return 'idle'
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusTagType(item: DeviceRealtimeVO) {
|
||||||
|
if (item.connected === 0) return 'default'
|
||||||
|
if (item.cncStatus === 2) return 'error'
|
||||||
|
if (item.cncStatus === 1) return 'success'
|
||||||
|
if (item.cncStatus === 0) return 'info'
|
||||||
|
return 'warning'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBoard() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
list.value = ((await deviceApi.realtimeBoard()) || []).filter((i) => i.bound)
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.message || '加载设备状态失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearTimer() {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAutoRefreshChange(val: boolean) {
|
||||||
|
clearTimer()
|
||||||
|
if (val) {
|
||||||
|
timer = setInterval(loadBoard, 15000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadBoard()
|
||||||
|
onAutoRefreshChange(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearTimer()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.table-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
min-width: 100px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item.running { background: #e8f8ef; border-color: #b7ebc6; }
|
||||||
|
.stat-item.idle { background: #eef5ff; border-color: #b3d4ff; }
|
||||||
|
.stat-item.alarm { background: #ffecec; border-color: #ffc2c8; }
|
||||||
|
.stat-item.offline { background: #f5f5f5; border-color: #e5e7eb; }
|
||||||
|
|
||||||
|
.stat-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
}
|
||||||
|
.stat-dot.running { background: #18a058; box-shadow: 0 0 0 3px rgba(24, 160, 88, 0.2); }
|
||||||
|
.stat-dot.idle { background: #2080f0; }
|
||||||
|
.stat-dot.alarm { background: #d03050; }
|
||||||
|
.stat-dot.offline { background: #9aa0a6; }
|
||||||
|
|
||||||
|
.stat-num {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2329;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #646a73;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-card {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-left: 3px solid #d0d3d9;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-card:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-card.is-running { border-left-color: #18a058; }
|
||||||
|
.device-card.is-idle { border-left-color: #2080f0; }
|
||||||
|
.device-card.is-alarm { border-left-color: #d03050; }
|
||||||
|
.device-card.is-offline {
|
||||||
|
border-left-color: #c2c2c2;
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.head-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-lamp {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-top: 5px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #c2c2c2;
|
||||||
|
}
|
||||||
|
.status-lamp.running {
|
||||||
|
background: #18a058;
|
||||||
|
box-shadow: 0 0 0 3px rgba(24, 160, 88, 0.22);
|
||||||
|
animation: pulse 1.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.status-lamp.idle { background: #2080f0; }
|
||||||
|
.status-lamp.alarm {
|
||||||
|
background: #d03050;
|
||||||
|
box-shadow: 0 0 0 3px rgba(208, 48, 80, 0.22);
|
||||||
|
animation: pulse 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.status-lamp.offline { background: #9aa0a6; }
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.55; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2329;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8a8f98;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #f7f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8a8f98;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-value {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2329;
|
||||||
|
word-break: break-all;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-value.program {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-status.on { color: #18a058; }
|
||||||
|
.link-status.off { color: #8a8f98; }
|
||||||
|
|
||||||
|
.alarm-box {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff1f0;
|
||||||
|
color: #d03050;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 暗色主题:自定义区域不吃 Naive 组件变量,单独提亮 */
|
||||||
|
body.dark-theme .stat-item {
|
||||||
|
background: #27272a !important;
|
||||||
|
border-color: #3f3f46 !important;
|
||||||
|
}
|
||||||
|
body.dark-theme .stat-item.running {
|
||||||
|
background: rgba(24, 160, 88, 0.18) !important;
|
||||||
|
border-color: rgba(52, 211, 153, 0.4) !important;
|
||||||
|
}
|
||||||
|
body.dark-theme .stat-item.idle {
|
||||||
|
background: rgba(32, 128, 240, 0.18) !important;
|
||||||
|
border-color: rgba(96, 165, 250, 0.4) !important;
|
||||||
|
}
|
||||||
|
body.dark-theme .stat-item.alarm {
|
||||||
|
background: rgba(208, 48, 80, 0.18) !important;
|
||||||
|
border-color: rgba(251, 113, 133, 0.4) !important;
|
||||||
|
}
|
||||||
|
body.dark-theme .stat-item.offline {
|
||||||
|
background: #27272a !important;
|
||||||
|
border-color: #3f3f46 !important;
|
||||||
|
}
|
||||||
|
body.dark-theme .stat-num {
|
||||||
|
color: #f4f4f5 !important;
|
||||||
|
}
|
||||||
|
body.dark-theme .stat-label {
|
||||||
|
color: #a1a1aa !important;
|
||||||
|
}
|
||||||
|
body.dark-theme .device-card {
|
||||||
|
background: #18181c !important;
|
||||||
|
border-color: #3f3f46 !important;
|
||||||
|
}
|
||||||
|
body.dark-theme .device-card.is-offline {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
body.dark-theme .name {
|
||||||
|
color: #f4f4f5 !important;
|
||||||
|
}
|
||||||
|
body.dark-theme .meta {
|
||||||
|
color: #a1a1aa !important;
|
||||||
|
}
|
||||||
|
body.dark-theme .metric {
|
||||||
|
background: #27272a !important;
|
||||||
|
}
|
||||||
|
body.dark-theme .metric-label {
|
||||||
|
color: #a1a1aa !important;
|
||||||
|
}
|
||||||
|
body.dark-theme .metric-value {
|
||||||
|
color: #f4f4f5 !important;
|
||||||
|
}
|
||||||
|
body.dark-theme .link-status.on {
|
||||||
|
color: #34d399 !important;
|
||||||
|
}
|
||||||
|
body.dark-theme .link-status.off {
|
||||||
|
color: #a1a1aa !important;
|
||||||
|
}
|
||||||
|
body.dark-theme .alarm-box {
|
||||||
|
background: rgba(208, 48, 80, 0.2) !important;
|
||||||
|
color: #fb7185 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -29,6 +29,9 @@
|
|||||||
<template #icon><n-icon><AddOutline /></n-icon></template>
|
<template #icon><n-icon><AddOutline /></n-icon></template>
|
||||||
新增设备
|
新增设备
|
||||||
</n-button>
|
</n-button>
|
||||||
|
<n-button @click="goRealtimeBoard">
|
||||||
|
设备状态看板
|
||||||
|
</n-button>
|
||||||
<!-- <n-button @click="importModalVisible = true">
|
<!-- <n-button @click="importModalVisible = true">
|
||||||
<template #icon><n-icon><CloudUploadOutline /></n-icon></template>
|
<template #icon><n-icon><CloudUploadOutline /></n-icon></template>
|
||||||
导入
|
导入
|
||||||
@ -63,6 +66,16 @@
|
|||||||
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="100px">
|
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="100px">
|
||||||
<n-form-item label="设备名称" path="deviceName">
|
<n-form-item label="设备名称" path="deviceName">
|
||||||
<n-input v-model:value="formData.deviceName" placeholder="请输入设备名称" />
|
<n-input v-model:value="formData.deviceName" placeholder="请输入设备名称" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="新代设备ID" path="synEquipId">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="formData.synEquipId"
|
||||||
|
placeholder="SynFactory equipID,如 1、2、3"
|
||||||
|
:min="1"
|
||||||
|
:show-button="false"
|
||||||
|
style="width: 100%"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="所属工段" path="sectionId">
|
<n-form-item label="所属工段" path="sectionId">
|
||||||
<n-select
|
<n-select
|
||||||
@ -141,6 +154,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, h, onMounted } from 'vue'
|
import { ref, reactive, h, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { NButton, NSpace,NTag, NIcon, NUpload, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions } from 'naive-ui'
|
import { NButton, NSpace,NTag, NIcon, NUpload, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions } from 'naive-ui'
|
||||||
import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline } from '@vicons/ionicons5'
|
import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline } from '@vicons/ionicons5'
|
||||||
import { deviceApi, type Device } from '@/api/device'
|
import { deviceApi, type Device } from '@/api/device'
|
||||||
@ -151,6 +165,7 @@ import {sectionApi, type Section} from '@/api/section'
|
|||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
let sectionList = reactive<{label:string,value:any}[]>([])
|
let sectionList = reactive<{label:string,value:any}[]>([])
|
||||||
|
|
||||||
@ -190,7 +205,8 @@ const defaultFormData: Device = {
|
|||||||
workshopId: '',
|
workshopId: '',
|
||||||
remark: '',
|
remark: '',
|
||||||
status:0,
|
status:0,
|
||||||
deviceFlag:'mes_equipment'
|
deviceFlag:'mes_equipment',
|
||||||
|
synEquipId: null as number | null,
|
||||||
}
|
}
|
||||||
const formData = reactive<Device>({ ...defaultFormData })
|
const formData = reactive<Device>({ ...defaultFormData })
|
||||||
|
|
||||||
@ -209,6 +225,9 @@ const columns: DataTableColumns<Device> = [
|
|||||||
{ type: 'selection' },
|
{ type: 'selection' },
|
||||||
{ title: '设备编码', key: 'deviceCode' },
|
{ title: '设备编码', key: 'deviceCode' },
|
||||||
{ title: '设备名称', key: 'deviceName' },
|
{ title: '设备名称', key: 'deviceName' },
|
||||||
|
{ title: '新代ID', key: 'synEquipId', width: 90,
|
||||||
|
render: (row) => row.synEquipId ?? '-'
|
||||||
|
},
|
||||||
{ title: '设备类型', key: 'deviceType' },
|
{ title: '设备类型', key: 'deviceType' },
|
||||||
{ title: '所属工段', key: 'sectionName' },
|
{ title: '所属工段', key: 'sectionName' },
|
||||||
{ title: '设备型号', key: 'spec' },
|
{ title: '设备型号', key: 'spec' },
|
||||||
@ -294,6 +313,10 @@ function handleAdd() {
|
|||||||
modalVisible.value = true
|
modalVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goRealtimeBoard() {
|
||||||
|
router.push('/biz/device/board')
|
||||||
|
}
|
||||||
|
|
||||||
// 编辑
|
// 编辑
|
||||||
function handleEdit(row: Device) {
|
function handleEdit(row: Device) {
|
||||||
modalTitle.value = '编辑设备表'
|
modalTitle.value = '编辑设备表'
|
||||||
|
|||||||
@ -80,7 +80,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="order in tableData" :key="orderRowKey(order)" class="order-block">
|
<div v-for="order in tableData" :key="orderRowKey(order)" class="order-block">
|
||||||
<div class="order-row" :class="{ expanded: isExpanded(order) }">
|
<div class="order-row" :class="{ expanded: isExpanded(order), 'derived-expanded': isDerivedExpanded(order) }">
|
||||||
<div class="col-check">
|
<div class="col-check">
|
||||||
<n-checkbox
|
<n-checkbox
|
||||||
:checked="selectedOrderIds.includes(order.orderItemId!)"
|
:checked="selectedOrderIds.includes(order.orderItemId!)"
|
||||||
@ -92,11 +92,12 @@
|
|||||||
quaternary
|
quaternary
|
||||||
circle
|
circle
|
||||||
size="tiny"
|
size="tiny"
|
||||||
@click="toggleExpand(order)"
|
title="展开返工单 / 拆分工单"
|
||||||
|
@click="toggleDerivedExpand(order)"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon>
|
<n-icon>
|
||||||
<RemoveOutline v-if="isExpanded(order)" />
|
<RemoveOutline v-if="isDerivedExpanded(order)" />
|
||||||
<AddOutline v-else />
|
<AddOutline v-else />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</template>
|
</template>
|
||||||
@ -105,7 +106,7 @@
|
|||||||
<div class="col-product">
|
<div class="col-product">
|
||||||
<div class="product-name">{{ order.materialName || '-' }}</div>
|
<div class="product-name">{{ order.materialName || '-' }}</div>
|
||||||
<div class="product-code">
|
<div class="product-code">
|
||||||
<span>{{ order.orderCode || order.mainCode || '-' }}</span>
|
<span>{{ formatWorkOrderCodes(order) }}</span>
|
||||||
<n-tag v-if="isOverdue(order)" size="tiny" type="error" :bordered="false">逾期</n-tag>
|
<n-tag v-if="isOverdue(order)" size="tiny" type="error" :bordered="false">逾期</n-tag>
|
||||||
<n-tag v-if="hasOutsource(order)" size="tiny" type="warning" :bordered="false">委外</n-tag>
|
<n-tag v-if="hasOutsource(order)" size="tiny" type="warning" :bordered="false">委外</n-tag>
|
||||||
</div>
|
</div>
|
||||||
@ -116,8 +117,12 @@
|
|||||||
<n-tag v-if="order.proWorkshop" size="tiny" type="info" :bordered="false">{{ order.proWorkshop }}</n-tag>
|
<n-tag v-if="order.proWorkshop" size="tiny" type="info" :bordered="false">{{ order.proWorkshop }}</n-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-kit">-</div>
|
<div class="col-kit">-</div>
|
||||||
<div class="col-flow">
|
<div class="col-flow" :class="{ 'flow-expanded': isExpanded(order) }">
|
||||||
<ProcessFlowStepper :list="order.processList" />
|
<ProcessFlowStepper
|
||||||
|
:list="order.processList"
|
||||||
|
clickable
|
||||||
|
@click="toggleExpand(order)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-plan">
|
<div class="col-plan">
|
||||||
<div>{{ formatShortTime(order.beginTime) }}</div>
|
<div>{{ formatShortTime(order.beginTime) }}</div>
|
||||||
@ -141,6 +146,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isDerivedExpanded(order)" class="order-derived-expand">
|
||||||
|
<div class="derived-section">
|
||||||
|
<div class="derived-section-head">
|
||||||
|
<span class="derived-section-title">返工单</span>
|
||||||
|
<n-button text type="primary" size="tiny" @click="onMoreSelect('createRework', order)">创建返工单</n-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="!getDerivedOrders(order).rework.length" class="derived-empty">暂无返工单</div>
|
||||||
|
<div v-else class="derived-list">
|
||||||
|
<div
|
||||||
|
v-for="item in getDerivedOrders(order).rework"
|
||||||
|
:key="item.id"
|
||||||
|
class="derived-item"
|
||||||
|
>
|
||||||
|
<span class="derived-code">{{ item.code }}</span>
|
||||||
|
<span class="derived-meta">{{ item.quantity ?? '-' }} · {{ item.statusLabel }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="derived-section">
|
||||||
|
<div class="derived-section-head">
|
||||||
|
<span class="derived-section-title">拆分工单</span>
|
||||||
|
<n-button text type="primary" size="tiny" @click="onMoreSelect('splitWorkOrder', order)">拆分工单</n-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="!getDerivedOrders(order).split.length" class="derived-empty">暂无拆分工单</div>
|
||||||
|
<div v-else class="derived-list">
|
||||||
|
<div
|
||||||
|
v-for="item in getDerivedOrders(order).split"
|
||||||
|
:key="item.id"
|
||||||
|
class="derived-item"
|
||||||
|
>
|
||||||
|
<span class="derived-code">{{ item.code }}</span>
|
||||||
|
<span class="derived-meta">{{ item.quantity ?? '-' }} · {{ item.statusLabel }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="isExpanded(order)" class="order-expand">
|
<div v-if="isExpanded(order)" class="order-expand">
|
||||||
<div v-if="!(order.processList?.length)" class="expand-empty">暂无工序明细</div>
|
<div v-if="!(order.processList?.length)" class="expand-empty">暂无工序明细</div>
|
||||||
<div v-else class="card-flow-scroll">
|
<div v-else class="card-flow-scroll">
|
||||||
@ -151,8 +193,10 @@
|
|||||||
:is-last="idx === (order.processList?.length ?? 0) - 1"
|
:is-last="idx === (order.processList?.length ?? 0) - 1"
|
||||||
:can-edit="hasPermission('biz:orderItem:edit')"
|
:can-edit="hasPermission('biz:orderItem:edit')"
|
||||||
:can-assign="hasPermission('biz:orderItem:assign')"
|
:can-assign="hasPermission('biz:orderItem:assign')"
|
||||||
|
:can-outsource="hasPermission('biz:orderProcessPlan:add')"
|
||||||
@edit="(p) => handleEdit(p, order)"
|
@edit="(p) => handleEdit(p, order)"
|
||||||
@assign="(p) => disphand(p, order)"
|
@assign="(p) => disphand(p, order)"
|
||||||
|
@outsource="(p) => openOutsource(order, p)"
|
||||||
@view-model="(p) => openPlmModel(order, p)"
|
@view-model="(p) => openPlmModel(order, p)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -184,7 +228,7 @@
|
|||||||
<template v-if="detailData">
|
<template v-if="detailData">
|
||||||
<n-descriptions :column="3" bordered size="small" label-placement="left">
|
<n-descriptions :column="3" bordered size="small" label-placement="left">
|
||||||
<n-descriptions-item label="生产令号">{{ detailData.mainCode || '-' }}</n-descriptions-item>
|
<n-descriptions-item label="生产令号">{{ detailData.mainCode || '-' }}</n-descriptions-item>
|
||||||
<n-descriptions-item label="订单编码">{{ detailData.orderCode || '-' }}</n-descriptions-item>
|
<n-descriptions-item label="工单号">{{ formatWorkOrderCodes(detailData) }}</n-descriptions-item>
|
||||||
<n-descriptions-item label="物料">{{ detailData.materialName || '-' }}</n-descriptions-item>
|
<n-descriptions-item label="物料">{{ detailData.materialName || '-' }}</n-descriptions-item>
|
||||||
<n-descriptions-item label="数量">{{ detailData.quantity ?? '-' }}</n-descriptions-item>
|
<n-descriptions-item label="数量">{{ detailData.quantity ?? '-' }}</n-descriptions-item>
|
||||||
<n-descriptions-item label="工艺路线">{{ detailData.routeCode || '-' }}</n-descriptions-item>
|
<n-descriptions-item label="工艺路线">{{ detailData.routeCode || '-' }}</n-descriptions-item>
|
||||||
@ -195,14 +239,16 @@
|
|||||||
</n-descriptions>
|
</n-descriptions>
|
||||||
<div class="detail-cards">
|
<div class="detail-cards">
|
||||||
<ProcessCard
|
<ProcessCard
|
||||||
v-for="(proc, idx) in sortedProcessList(detailData.processList)"
|
v-for="(proc, idx) in (detailData.processList)"
|
||||||
:key="proc.planId ?? idx"
|
:key="proc.planId ?? idx"
|
||||||
:process="proc"
|
:process="proc"
|
||||||
:is-last="idx === (detailData.processList?.length ?? 0) - 1"
|
:is-last="idx === (detailData.processList?.length ?? 0) - 1"
|
||||||
:can-edit="hasPermission('biz:orderItem:edit')"
|
:can-edit="hasPermission('biz:orderItem:edit')"
|
||||||
:can-assign="hasPermission('biz:orderItem:assign')"
|
:can-assign="hasPermission('biz:orderItem:assign')"
|
||||||
|
:can-outsource="hasPermission('biz:orderProcessPlan:add')"
|
||||||
@edit="(p) => handleEdit(p, detailData!)"
|
@edit="(p) => handleEdit(p, detailData!)"
|
||||||
@assign="(p) => disphand(p, detailData!)"
|
@assign="(p) => disphand(p, detailData!)"
|
||||||
|
@outsource="(p) => openOutsource(detailData!, p)"
|
||||||
@view-model="(p) => openPlmModel(detailData!, p)"
|
@view-model="(p) => openPlmModel(detailData!, p)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -316,6 +362,148 @@
|
|||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
|
||||||
|
<!-- 工序委外 -->
|
||||||
|
<n-modal
|
||||||
|
v-model:show="outsourceModalVisible"
|
||||||
|
preset="card"
|
||||||
|
:title="outsourceStep === 1 ? '创建工序委外 - 选择工序' : '创建工序委外 - 填写信息'"
|
||||||
|
style="width: 720px"
|
||||||
|
:mask-closable="false"
|
||||||
|
@after-leave="resetOutsourceForm"
|
||||||
|
>
|
||||||
|
<n-spin :show="outsourceOptionsLoading">
|
||||||
|
<template v-if="outsourceStep === 1 && outsourceOptions">
|
||||||
|
<n-descriptions :column="2" bordered size="small" label-placement="left" style="margin-bottom: 16px">
|
||||||
|
<n-descriptions-item label="生产令号">{{ outsourceOptions.mainCode || '-' }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="订单编码">{{ outsourceOptions.orderCode || '-' }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="物料编码">{{ outsourceOptions.materialCode || '-' }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="物料名称">{{ outsourceOptions.materialName || '-' }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="生产数量">{{ outsourceOptions.quantity ?? '-' }}</n-descriptions-item>
|
||||||
|
</n-descriptions>
|
||||||
|
<n-radio-group v-model:value="outsourceSelectedPlanId" style="width: 100%">
|
||||||
|
<n-space vertical :size="8" style="width: 100%">
|
||||||
|
<div
|
||||||
|
v-for="proc in outsourceOptions.processList"
|
||||||
|
:key="proc.planId"
|
||||||
|
class="outsource-process-item"
|
||||||
|
:class="{ active: outsourceSelectedPlanId === proc.planId }"
|
||||||
|
@click="outsourceSelectedPlanId = proc.planId!"
|
||||||
|
>
|
||||||
|
<n-radio :value="proc.planId" />
|
||||||
|
<div class="outsource-process-body">
|
||||||
|
<div class="outsource-process-title">
|
||||||
|
<span>{{ proc.processName || '-' }}</span>
|
||||||
|
<n-tag v-if="proc.current" size="tiny" type="info" :bordered="false">当前</n-tag>
|
||||||
|
<n-tag v-if="proc.outsource === 1" size="tiny" type="warning" :bordered="false">已委外</n-tag>
|
||||||
|
</div>
|
||||||
|
<div class="outsource-process-meta">
|
||||||
|
<span>工单号:{{ proc.workOrderCode || '-' }}</span>
|
||||||
|
<span>工序号:{{ proc.operNumber ?? '-' }}</span>
|
||||||
|
<span>数量:{{ proc.quantity ?? '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-space>
|
||||||
|
</n-radio-group>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="outsourceStep === 2">
|
||||||
|
<n-card size="small" style="margin-bottom: 16px">
|
||||||
|
<n-grid :cols="2" :x-gap="12">
|
||||||
|
<n-gi>
|
||||||
|
<span class="pgClass">委外工序:</span>
|
||||||
|
<span>{{ selectedOutsourceProcess?.processName || '-' }}</span>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi>
|
||||||
|
<span class="pgClass">工单号:</span>
|
||||||
|
<span>{{ selectedOutsourceProcess?.workOrderCode || '-' }}</span>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi>
|
||||||
|
<span class="pgClass">物料:</span>
|
||||||
|
<span>{{ outsourceOptions?.materialName || '-' }}</span>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi>
|
||||||
|
<span class="pgClass">工序数量:</span>
|
||||||
|
<span>{{ selectedOutsourceProcess?.quantity ?? '-' }}</span>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
</n-card>
|
||||||
|
<n-form
|
||||||
|
ref="outsourceFormRef"
|
||||||
|
:model="outsourceForm"
|
||||||
|
:rules="outsourceRules"
|
||||||
|
label-placement="left"
|
||||||
|
label-width="110"
|
||||||
|
>
|
||||||
|
<n-form-item label="供应商" path="supplierName">
|
||||||
|
<n-input v-model:value="outsourceForm.supplierName" placeholder="请输入供应商名称" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="委外数量" path="quantity">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="outsourceForm.quantity"
|
||||||
|
:min="1"
|
||||||
|
:max="selectedOutsourceProcess?.quantity ?? undefined"
|
||||||
|
:precision="0"
|
||||||
|
placeholder="请输入委外数量"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="单价" path="unitPrice">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="outsourceForm.unitPrice"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
placeholder="请输入单价(选填)"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="计划开始时间" path="planStartTime">
|
||||||
|
<n-date-picker
|
||||||
|
v-model:value="outsourceForm.planStartTime"
|
||||||
|
type="datetime"
|
||||||
|
clearable
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="计划结束时间" path="planEndTime">
|
||||||
|
<n-date-picker
|
||||||
|
v-model:value="outsourceForm.planEndTime"
|
||||||
|
type="datetime"
|
||||||
|
clearable
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="备注" path="remark">
|
||||||
|
<n-input
|
||||||
|
v-model:value="outsourceForm.remark"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="请输入备注(选填)"
|
||||||
|
:autosize="{ minRows: 2, maxRows: 4 }"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</template>
|
||||||
|
</n-spin>
|
||||||
|
<template #footer>
|
||||||
|
<n-space justify="end">
|
||||||
|
<n-button @click="outsourceModalVisible = false">取消</n-button>
|
||||||
|
<n-button v-if="outsourceStep === 2" @click="outsourceStep = 1">上一步</n-button>
|
||||||
|
<n-button
|
||||||
|
v-if="outsourceStep === 1"
|
||||||
|
type="primary"
|
||||||
|
:disabled="!outsourceSelectedPlanId"
|
||||||
|
@click="goOutsourceStep2"
|
||||||
|
>下一步</n-button>
|
||||||
|
<n-button
|
||||||
|
v-else
|
||||||
|
type="primary"
|
||||||
|
:loading="outsourceSubmitLoading"
|
||||||
|
@click="submitOutsource"
|
||||||
|
>确认创建</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
</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-space vertical>
|
||||||
@ -338,17 +526,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted, h, computed, type Component } from 'vue'
|
||||||
import {
|
import {
|
||||||
NButton, NSpace, NIcon, NTag, NSpin, NModal, NForm, NFormItem, NGrid, NGi,
|
NButton, NSpace, NIcon, NTag, NSpin, NModal, NForm, NFormItem, NGrid, NGi,
|
||||||
NInput, NDatePicker, NCheckbox, NEmpty, NPagination, NDropdown, NDescriptions,
|
NInput, NInputNumber, NDatePicker, NCheckbox, NEmpty, NPagination, NDropdown, NDescriptions,
|
||||||
NDescriptionsItem, NAlert, NUpload, NText, useMessage, useDialog,
|
NDescriptionsItem, NAlert, NUpload, NText, NCard, NRadioGroup, NRadio, useMessage, useDialog,
|
||||||
type UploadCustomRequestOptions,
|
type UploadCustomRequestOptions, type FormRules,
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import {
|
import {
|
||||||
AddOutline, RemoveOutline, SearchOutline, RefreshOutline, CloudUploadOutline,
|
AddOutline, RemoveOutline, SearchOutline, RefreshOutline, CloudUploadOutline,
|
||||||
DownloadOutline, ChevronDownOutline, CheckmarkCircleOutline, CheckmarkCircle,
|
DownloadOutline, ChevronDownOutline, CheckmarkCircleOutline, CheckmarkCircle,
|
||||||
EllipseOutline, TimeOutline,
|
EllipseOutline, TimeOutline, PrintOutline, SyncOutline, PeopleOutline,
|
||||||
|
GitBranchOutline, CloseCircleOutline, LockClosedOutline, TrashOutline,
|
||||||
} from '@vicons/ionicons5'
|
} from '@vicons/ionicons5'
|
||||||
import {
|
import {
|
||||||
orderProcessPlanApi,
|
orderProcessPlanApi,
|
||||||
@ -357,6 +546,8 @@ import {
|
|||||||
ORDER_STATUS_MAP,
|
ORDER_STATUS_MAP,
|
||||||
type OrderProcessPlanVO,
|
type OrderProcessPlanVO,
|
||||||
type ProcessPlanItemVO,
|
type ProcessPlanItemVO,
|
||||||
|
type OutsourcingOptions,
|
||||||
|
type OutsourcingProcessOption,
|
||||||
} from '@/api/orderProcessPlan'
|
} from '@/api/orderProcessPlan'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import ProcessFlowStepper from './components/ProcessFlowStepper.vue'
|
import ProcessFlowStepper from './components/ProcessFlowStepper.vue'
|
||||||
@ -377,6 +568,7 @@ const searchForm = reactive({
|
|||||||
|
|
||||||
const tableData = ref<OrderProcessPlanVO[]>([])
|
const tableData = ref<OrderProcessPlanVO[]>([])
|
||||||
const expandedIds = ref<Set<number>>(new Set())
|
const expandedIds = ref<Set<number>>(new Set())
|
||||||
|
const derivedExpandedIds = ref<Set<number>>(new Set())
|
||||||
const selectedOrderIds = ref<number[]>([])
|
const selectedOrderIds = ref<number[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const pagination = reactive({ page: 1, pageSize: 10, itemCount: 0 })
|
const pagination = reactive({ page: 1, pageSize: 10, itemCount: 0 })
|
||||||
@ -409,6 +601,31 @@ const dispatchrules = {}
|
|||||||
const importModalVisible = ref(false)
|
const importModalVisible = ref(false)
|
||||||
const plmModelDrawerRef = ref<InstanceType<typeof PlmModelDrawer> | null>(null)
|
const plmModelDrawerRef = ref<InstanceType<typeof PlmModelDrawer> | null>(null)
|
||||||
|
|
||||||
|
const outsourceModalVisible = ref(false)
|
||||||
|
const outsourceOptionsLoading = ref(false)
|
||||||
|
const outsourceSubmitLoading = ref(false)
|
||||||
|
const outsourceStep = ref(1)
|
||||||
|
const outsourceOptions = ref<OutsourcingOptions | null>(null)
|
||||||
|
const outsourceSelectedPlanId = ref<number | null>(null)
|
||||||
|
const outsourceFormRef = ref()
|
||||||
|
const outsourceForm = reactive({
|
||||||
|
supplierName: '',
|
||||||
|
quantity: null as number | null,
|
||||||
|
unitPrice: null as number | null,
|
||||||
|
planStartTime: null as number | null,
|
||||||
|
planEndTime: null as number | null,
|
||||||
|
remark: '',
|
||||||
|
})
|
||||||
|
const outsourceRules: FormRules = {
|
||||||
|
supplierName: [{ required: true, message: '请输入供应商', trigger: 'blur' }],
|
||||||
|
quantity: [{ required: true, type: 'number', message: '请输入委外数量', trigger: ['blur', 'change'] }],
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedOutsourceProcess = computed<OutsourcingProcessOption | null>(() => {
|
||||||
|
if (!outsourceSelectedPlanId.value) return null
|
||||||
|
return outsourceOptions.value?.processList?.find(p => p.planId === outsourceSelectedPlanId.value) ?? null
|
||||||
|
})
|
||||||
|
|
||||||
function openPlmModel(order: OrderProcessPlanVO, process?: ProcessPlanItemVO) {
|
function openPlmModel(order: OrderProcessPlanVO, process?: ProcessPlanItemVO) {
|
||||||
const ctx: PlmModelOpenContext = {
|
const ctx: PlmModelOpenContext = {
|
||||||
orderItemId: order.orderItemId,
|
orderItemId: order.orderItemId,
|
||||||
@ -427,13 +644,66 @@ const batchOptions = [
|
|||||||
{ label: '全部展开', key: 'expandAll' },
|
{ label: '全部展开', key: 'expandAll' },
|
||||||
{ label: '全部收起', key: 'collapseAll' },
|
{ label: '全部收起', key: 'collapseAll' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
function dropdownIcon(icon: Component) {
|
||||||
|
return () => h(NIcon, null, { default: () => h(icon) })
|
||||||
|
}
|
||||||
|
|
||||||
|
function dangerDropdownItem(label: string, key: string, icon: Component) {
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
key,
|
||||||
|
icon: () => h(NIcon, { color: '#d03050' }, { default: () => h(icon) }),
|
||||||
|
props: { style: { color: '#d03050' } },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const moreOptions = [
|
const moreOptions = [
|
||||||
{ label: '展开工序', key: 'expand' },
|
{
|
||||||
{ label: '收起工序', key: 'collapse' },
|
type: 'group',
|
||||||
|
label: '查看与编辑',
|
||||||
|
key: 'group-view-edit',
|
||||||
|
children: [
|
||||||
|
{ label: '打印', key: 'print', icon: dropdownIcon(PrintOutline) },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'group',
|
||||||
|
label: '派生操作',
|
||||||
|
key: 'group-derived',
|
||||||
|
children: [
|
||||||
|
{ label: '创建返工单', key: 'createRework', icon: dropdownIcon(SyncOutline) },
|
||||||
|
{ label: '创建工序委外', key: 'createOutsource', icon: dropdownIcon(PeopleOutline) },
|
||||||
|
{ label: '拆分工单', key: 'splitWorkOrder', icon: dropdownIcon(GitBranchOutline) },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'group',
|
||||||
|
label: '状态控制',
|
||||||
|
key: 'group-status',
|
||||||
|
children: [
|
||||||
|
dangerDropdownItem('撤回', 'withdraw', CloseCircleOutline),
|
||||||
|
dangerDropdownItem('冻结工单', 'freeze', LockClosedOutline),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'group',
|
||||||
|
label: '危险操作',
|
||||||
|
key: 'group-danger',
|
||||||
|
children: [
|
||||||
|
dangerDropdownItem('删除', 'delete', TrashOutline),
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
function orderRowKey(row: OrderProcessPlanVO) {
|
function orderRowKey(row: OrderProcessPlanVO) {
|
||||||
return row.orderItemId ?? `order-${row.orderCode}`
|
return row.orderItemId ?? `order-${row.mainCode}-${formatWorkOrderCodes(row)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 汇总展示订单下各道工序的主工单号 */
|
||||||
|
function formatWorkOrderCodes(order?: OrderProcessPlanVO | null) {
|
||||||
|
const codes = (order?.processList ?? []).map((p) => p.workOrderCode).filter(Boolean)
|
||||||
|
return codes.length ? codes.join('、') : '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortedProcessList(list?: ProcessPlanItemVO[]) {
|
function sortedProcessList(list?: ProcessPlanItemVO[]) {
|
||||||
@ -444,6 +714,10 @@ function isExpanded(order: OrderProcessPlanVO) {
|
|||||||
return order.orderItemId != null && expandedIds.value.has(order.orderItemId)
|
return order.orderItemId != null && expandedIds.value.has(order.orderItemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDerivedExpanded(order: OrderProcessPlanVO) {
|
||||||
|
return order.orderItemId != null && derivedExpandedIds.value.has(order.orderItemId)
|
||||||
|
}
|
||||||
|
|
||||||
function toggleExpand(order: OrderProcessPlanVO) {
|
function toggleExpand(order: OrderProcessPlanVO) {
|
||||||
if (order.orderItemId == null) return
|
if (order.orderItemId == null) return
|
||||||
const next = new Set(expandedIds.value)
|
const next = new Set(expandedIds.value)
|
||||||
@ -452,6 +726,26 @@ function toggleExpand(order: OrderProcessPlanVO) {
|
|||||||
expandedIds.value = next
|
expandedIds.value = next
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleDerivedExpand(order: OrderProcessPlanVO) {
|
||||||
|
if (order.orderItemId == null) return
|
||||||
|
const next = new Set(derivedExpandedIds.value)
|
||||||
|
if (next.has(order.orderItemId)) next.delete(order.orderItemId)
|
||||||
|
else next.add(order.orderItemId)
|
||||||
|
derivedExpandedIds.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 派生工单(返工 / 拆分),待后端字段接入后替换占位逻辑 */
|
||||||
|
function getDerivedOrders(order: OrderProcessPlanVO) {
|
||||||
|
const raw = order as OrderProcessPlanVO & {
|
||||||
|
reworkOrders?: Array<{ id: string | number; code?: string; quantity?: number; statusLabel?: string }>
|
||||||
|
splitOrders?: Array<{ id: string | number; code?: string; quantity?: number; statusLabel?: string }>
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
rework: raw.reworkOrders ?? [],
|
||||||
|
split: raw.splitOrders ?? [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSelect(id: number, checked: boolean) {
|
function toggleSelect(id: number, checked: boolean) {
|
||||||
if (checked) selectedOrderIds.value = [...selectedOrderIds.value, id]
|
if (checked) selectedOrderIds.value = [...selectedOrderIds.value, id]
|
||||||
else selectedOrderIds.value = selectedOrderIds.value.filter(v => v !== id)
|
else selectedOrderIds.value = selectedOrderIds.value.filter(v => v !== id)
|
||||||
@ -531,8 +825,121 @@ function onBatchSelect(key: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onMoreSelect(key: string, order: OrderProcessPlanVO) {
|
function onMoreSelect(key: string, order: OrderProcessPlanVO) {
|
||||||
if (key === 'expand') toggleExpand(order)
|
if (key === 'createOutsource') {
|
||||||
if (key === 'collapse' && isExpanded(order)) toggleExpand(order)
|
openOutsource(order)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
print: '打印',
|
||||||
|
createRework: '创建返工单',
|
||||||
|
splitWorkOrder: '拆分工单',
|
||||||
|
withdraw: '撤回',
|
||||||
|
freeze: '冻结工单',
|
||||||
|
delete: '删除',
|
||||||
|
}
|
||||||
|
if (labels[key]) message.info(`${labels[key]}功能待后端接口支持`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDefaultPlanId(order: OrderProcessPlanVO, process?: ProcessPlanItemVO) {
|
||||||
|
if (process?.planId) return process.planId
|
||||||
|
const list = sortedProcessList(order.processList)
|
||||||
|
if (!list.length) return null
|
||||||
|
const current = list.find(p => p.operNumber === order.currentOperNumber)
|
||||||
|
return current?.planId ?? list[0].planId ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetOutsourceForm() {
|
||||||
|
outsourceStep.value = 1
|
||||||
|
outsourceOptions.value = null
|
||||||
|
outsourceSelectedPlanId.value = null
|
||||||
|
outsourceForm.supplierName = ''
|
||||||
|
outsourceForm.quantity = null
|
||||||
|
outsourceForm.unitPrice = null
|
||||||
|
outsourceForm.planStartTime = null
|
||||||
|
outsourceForm.planEndTime = null
|
||||||
|
outsourceForm.remark = ''
|
||||||
|
outsourceFormRef.value?.restoreValidation()
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillOutsourceFormFromProcess(proc?: OutsourcingProcessOption | null) {
|
||||||
|
if (!proc) return
|
||||||
|
outsourceForm.quantity = proc.quantity ?? null
|
||||||
|
outsourceForm.planStartTime = parseDateTime(proc.planStartTime)
|
||||||
|
outsourceForm.planEndTime = parseDateTime(proc.planFinishTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openOutsource(order: OrderProcessPlanVO, process?: ProcessPlanItemVO) {
|
||||||
|
const planId = resolveDefaultPlanId(order, process)
|
||||||
|
if (!planId) {
|
||||||
|
message.warning('当前订单暂无可委外工序')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resetOutsourceForm()
|
||||||
|
outsourceModalVisible.value = true
|
||||||
|
outsourceOptionsLoading.value = true
|
||||||
|
try {
|
||||||
|
const options = await orderProcessPlanApi.getOutsourcingOptions(planId)
|
||||||
|
outsourceOptions.value = options
|
||||||
|
const preferredId = process?.planId ?? options.currentProcessPlanId ?? options.processList?.[0]?.planId ?? null
|
||||||
|
outsourceSelectedPlanId.value = preferredId
|
||||||
|
if (process?.planId) {
|
||||||
|
outsourceStep.value = 2
|
||||||
|
fillOutsourceFormFromProcess(selectedOutsourceProcess.value)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
outsourceModalVisible.value = false
|
||||||
|
} finally {
|
||||||
|
outsourceOptionsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goOutsourceStep2() {
|
||||||
|
if (!outsourceSelectedPlanId.value) {
|
||||||
|
message.warning('请选择委外工序')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fillOutsourceFormFromProcess(selectedOutsourceProcess.value)
|
||||||
|
outsourceStep.value = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSubmitDateTime(ts: number | null) {
|
||||||
|
if (!ts) return undefined
|
||||||
|
return new Date(ts).toISOString().slice(0, 19).replace('T', ' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitOutsource() {
|
||||||
|
await outsourceFormRef.value?.validate()
|
||||||
|
if (!outsourceSelectedPlanId.value) {
|
||||||
|
message.warning('请选择委外工序')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const maxQty = selectedOutsourceProcess.value?.quantity
|
||||||
|
if (maxQty != null && (outsourceForm.quantity ?? 0) > maxQty) {
|
||||||
|
message.error(`委外数量不能大于工序数量 ${maxQty}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (outsourceForm.planStartTime && outsourceForm.planEndTime
|
||||||
|
&& outsourceForm.planEndTime < outsourceForm.planStartTime) {
|
||||||
|
message.error('计划结束时间不能早于计划开始时间')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
outsourceSubmitLoading.value = true
|
||||||
|
try {
|
||||||
|
await orderProcessPlanApi.createOutsourcing({
|
||||||
|
processPlanId: outsourceSelectedPlanId.value,
|
||||||
|
supplierName: outsourceForm.supplierName.trim(),
|
||||||
|
quantity: outsourceForm.quantity!,
|
||||||
|
unitPrice: outsourceForm.unitPrice ?? undefined,
|
||||||
|
planStartTime: formatSubmitDateTime(outsourceForm.planStartTime),
|
||||||
|
planEndTime: formatSubmitDateTime(outsourceForm.planEndTime),
|
||||||
|
remark: outsourceForm.remark || undefined,
|
||||||
|
})
|
||||||
|
message.success('委外单创建成功')
|
||||||
|
outsourceModalVisible.value = false
|
||||||
|
loadData()
|
||||||
|
} finally {
|
||||||
|
outsourceSubmitLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openDetail(order: OrderProcessPlanVO) {
|
async function openDetail(order: OrderProcessPlanVO) {
|
||||||
@ -761,6 +1168,14 @@ onMounted(loadData)
|
|||||||
background: rgba(32, 128, 240, 0.04);
|
background: rgba(32, 128, 240, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.order-row.derived-expanded {
|
||||||
|
background: rgba(24, 160, 88, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-row.expanded.derived-expanded {
|
||||||
|
background: linear-gradient(90deg, rgba(24, 160, 88, 0.04) 0%, rgba(32, 128, 240, 0.04) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
.col-check,
|
.col-check,
|
||||||
.col-expand {
|
.col-expand {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -825,6 +1240,10 @@ onMounted(loadData)
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.col-flow.flow-expanded :deep(.flow-stepper) {
|
||||||
|
background: rgba(32, 128, 240, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
.life-cell {
|
.life-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -846,6 +1265,69 @@ onMounted(loadData)
|
|||||||
border-top: 1px dashed #e8eaed;
|
border-top: 1px dashed #e8eaed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.order-derived-expand {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px 12px 68px;
|
||||||
|
background: linear-gradient(180deg, rgba(24, 160, 88, 0.03) 0%, #fff 100%);
|
||||||
|
border-top: 1px dashed #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.derived-section {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #eef0f3;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.derived-section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.derived-section-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.derived-empty {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.derived-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.derived-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: #fafbfc;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.derived-code {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.derived-meta {
|
||||||
|
color: #8c8c8c;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.card-flow-scroll,
|
.card-flow-scroll,
|
||||||
.detail-cards {
|
.detail-cards {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -882,4 +1364,44 @@ onMounted(loadData)
|
|||||||
.pgClass {
|
.pgClass {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.outsource-process-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outsource-process-item:hover,
|
||||||
|
.outsource-process-item.active {
|
||||||
|
border-color: #2080f0;
|
||||||
|
background: rgba(32, 128, 240, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.outsource-process-body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outsource-process-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outsource-process-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -50,6 +50,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-meta">
|
<div class="card-meta">
|
||||||
|
<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>{{ personnel }}</em></div>
|
||||||
<div class="meta-line"><span>车间</span><em>{{ process.departmentName || '-' }}</em></div>
|
<div class="meta-line"><span>车间</span><em>{{ process.departmentName || '-' }}</em></div>
|
||||||
<div class="meta-line"><span>工位</span><em>{{ process.workCenterName || '-' }}</em></div>
|
<div class="meta-line"><span>工位</span><em>{{ process.workCenterName || '-' }}</em></div>
|
||||||
@ -74,6 +75,13 @@
|
|||||||
>编辑</n-button>
|
>编辑</n-button>
|
||||||
<n-button size="tiny" type="info" ghost @click="emit('viewModel', process)">模型</n-button>
|
<n-button size="tiny" type="info" ghost @click="emit('viewModel', process)">模型</n-button>
|
||||||
<n-button size="tiny" quaternary disabled>返工</n-button>
|
<n-button size="tiny" quaternary disabled>返工</n-button>
|
||||||
|
<n-button
|
||||||
|
v-if="canOutsource"
|
||||||
|
size="tiny"
|
||||||
|
type="warning"
|
||||||
|
ghost
|
||||||
|
@click="emit('outsource', process)"
|
||||||
|
>委外</n-button>
|
||||||
<n-button
|
<n-button
|
||||||
size="tiny"
|
size="tiny"
|
||||||
:type="process.processStatus === 4 ? 'success' : 'default'"
|
:type="process.processStatus === 4 ? 'success' : 'default'"
|
||||||
@ -99,12 +107,14 @@ const props = defineProps<{
|
|||||||
isLast?: boolean
|
isLast?: boolean
|
||||||
canEdit?: boolean
|
canEdit?: boolean
|
||||||
canAssign?: boolean
|
canAssign?: boolean
|
||||||
|
canOutsource?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
edit: [process: ProcessPlanItemVO]
|
edit: [process: ProcessPlanItemVO]
|
||||||
assign: [process: ProcessPlanItemVO]
|
assign: [process: ProcessPlanItemVO]
|
||||||
viewModel: [process: ProcessPlanItemVO]
|
viewModel: [process: ProcessPlanItemVO]
|
||||||
|
outsource: [process: ProcessPlanItemVO]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const totalQty = computed(() => props.process.quantity ?? 0)
|
const totalQty = computed(() => props.process.quantity ?? 0)
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flow-stepper">
|
<div
|
||||||
|
class="flow-stepper"
|
||||||
|
:class="{ clickable }"
|
||||||
|
@click="onClick"
|
||||||
|
>
|
||||||
<template v-for="(step, idx) in steps" :key="step.key">
|
<template v-for="(step, idx) in steps" :key="step.key">
|
||||||
<div
|
<div
|
||||||
class="flow-step"
|
class="flow-step"
|
||||||
@ -23,8 +27,15 @@ import { NIcon } from 'naive-ui'
|
|||||||
import { CheckmarkOutline } from '@vicons/ionicons5'
|
import { CheckmarkOutline } from '@vicons/ionicons5'
|
||||||
import type { ProcessPlanItemVO } from '@/api/orderProcessPlan'
|
import type { ProcessPlanItemVO } from '@/api/orderProcessPlan'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
list?: ProcessPlanItemVO[]
|
list?: ProcessPlanItemVO[]
|
||||||
|
clickable?: boolean
|
||||||
|
}>(), {
|
||||||
|
clickable: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
click: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const steps = computed(() => {
|
const steps = computed(() => {
|
||||||
@ -40,6 +51,10 @@ const steps = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function onClick() {
|
||||||
|
if (props.clickable) emit('click')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -51,6 +66,18 @@ const steps = computed(() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flow-stepper.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
margin: -4px -6px;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-stepper.clickable:hover {
|
||||||
|
background: rgba(32, 128, 240, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
.flow-step {
|
.flow-step {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@ -437,7 +437,7 @@ const expandedRowKeys = ref<Array<string | number>>([])
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const selectedIds = ref<number[]>([])
|
const selectedIds = ref<number[]>([])
|
||||||
const orderScrollX = 2090
|
const orderScrollX = 2090
|
||||||
const processScrollX = 2620
|
const processScrollX = 2788
|
||||||
|
|
||||||
function orderRowKey(row: OrderProcessPlanVO) {
|
function orderRowKey(row: OrderProcessPlanVO) {
|
||||||
return row.orderItemId ?? `order-${row.mainCode}-${row.orderCode}`
|
return row.orderItemId ?? `order-${row.mainCode}-${row.orderCode}`
|
||||||
@ -585,7 +585,17 @@ const orderColumns: DataTableColumns<OrderProcessPlanVO> = [
|
|||||||
{ title: '生产令号', key: 'mainCode', width: 140, align: 'center', ellipsis: { tooltip: true } },
|
{ title: '生产令号', key: 'mainCode', width: 140, align: 'center', ellipsis: { tooltip: true } },
|
||||||
{ title: '物料编码', key: 'materialCode', width: 130, align: 'center', ellipsis: { tooltip: true } },
|
{ title: '物料编码', key: 'materialCode', width: 130, align: 'center', ellipsis: { tooltip: true } },
|
||||||
{ title: '物料名称', key: 'materialName', width: 168, ellipsis: { tooltip: true } },
|
{ title: '物料名称', key: 'materialName', width: 168, ellipsis: { tooltip: true } },
|
||||||
{ title: '生产订单编码', key: 'orderCode', width: 150, align: 'center', ellipsis: { tooltip: true } },
|
{
|
||||||
|
title: '工单号',
|
||||||
|
key: 'workOrderCode',
|
||||||
|
width: 168,
|
||||||
|
align: 'center',
|
||||||
|
ellipsis: { tooltip: true },
|
||||||
|
render: (row) => {
|
||||||
|
const codes = (row.processList ?? []).map((p) => p.workOrderCode).filter(Boolean)
|
||||||
|
return codes.length ? codes.join('、') : '-'
|
||||||
|
},
|
||||||
|
},
|
||||||
{ title: '数量', key: 'quantity', width: 72, align: 'center' },
|
{ title: '数量', key: 'quantity', width: 72, align: 'center' },
|
||||||
{ title: '工艺路线', key: 'routeCode', width: 108, align: 'center', ellipsis: { tooltip: true } },
|
{ title: '工艺路线', key: 'routeCode', width: 108, align: 'center', ellipsis: { tooltip: true } },
|
||||||
{ title: '生产车间', key: 'proWorkshop', width: 108, align: 'center', ellipsis: { tooltip: true } },
|
{ title: '生产车间', key: 'proWorkshop', width: 108, align: 'center', ellipsis: { tooltip: true } },
|
||||||
@ -614,6 +624,7 @@ const processColumns: DataTableColumns<ProcessPlanItemVO> = [
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
render: () => h(NTag, { size: 'small', type: 'warning', bordered: false }, { default: () => '工序' }),
|
render: () => h(NTag, { size: 'small', type: 'warning', bordered: false }, { default: () => '工序' }),
|
||||||
},
|
},
|
||||||
|
{ title: '工单号', key: 'workOrderCode', width: 168, align: 'center', ellipsis: { tooltip: true }, render: (row) => row.workOrderCode || '-' },
|
||||||
{ title: '工序号', key: 'operNumber', width: 72, align: 'center' },
|
{ title: '工序号', key: 'operNumber', width: 72, align: 'center' },
|
||||||
{ title: '工序编码', key: 'processCode', width: 96, align: 'center', ellipsis: { tooltip: true } },
|
{ title: '工序编码', key: 'processCode', width: 96, align: 'center', ellipsis: { tooltip: true } },
|
||||||
{ title: '工序名称', key: 'processName', width: 140, ellipsis: { tooltip: true } },
|
{ title: '工序名称', key: 'processName', width: 140, ellipsis: { tooltip: true } },
|
||||||
|
|||||||
539
src/views/biz/outsourcing/index.vue
Normal file
539
src/views/biz/outsourcing/index.vue
Normal file
@ -0,0 +1,539 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<n-card>
|
||||||
|
<div class="table-toolbar">
|
||||||
|
<n-space>
|
||||||
|
<n-button type="primary" @click="handleGoCreate">
|
||||||
|
<template #icon><n-icon><AddOutline /></n-icon></template>
|
||||||
|
新建工序委外
|
||||||
|
</n-button>
|
||||||
|
<n-button disabled title="待后端接口支持">下推</n-button>
|
||||||
|
<n-button type="error" :disabled="selectedIds.length === 0" @click="handleBatchDelete">
|
||||||
|
<template #icon><n-icon><TrashOutline /></n-icon></template>
|
||||||
|
批量删除
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
<n-space>
|
||||||
|
<n-button quaternary circle @click="loadData">
|
||||||
|
<template #icon><n-icon><RefreshOutline /></n-icon></template>
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search-form">
|
||||||
|
<n-form inline :model="searchForm" label-placement="left" size="small">
|
||||||
|
<n-form-item label="委外编号">
|
||||||
|
<n-input v-model:value="searchForm.outsourceCode" placeholder="工单委外编号" clearable style="width: 160px" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="供应商">
|
||||||
|
<n-input v-model:value="searchForm.supplierName" placeholder="委外供应商" clearable style="width: 140px" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="工序">
|
||||||
|
<n-input v-model:value="searchForm.name" placeholder="委外工序" clearable style="width: 140px" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="状态">
|
||||||
|
<n-select
|
||||||
|
v-model:value="searchForm.status"
|
||||||
|
placeholder="当前阶段"
|
||||||
|
clearable
|
||||||
|
style="width: 120px"
|
||||||
|
:options="statusOptions"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item>
|
||||||
|
<n-space>
|
||||||
|
<n-button type="primary" size="small" @click="handleSearch">
|
||||||
|
<template #icon><n-icon><SearchOutline /></n-icon></template>
|
||||||
|
搜索
|
||||||
|
</n-button>
|
||||||
|
<n-button size="small" @click="handleReset">重置</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-data-table
|
||||||
|
:columns="columns"
|
||||||
|
:data="tableData"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="pagination"
|
||||||
|
:row-key="(row) => row.id!"
|
||||||
|
:scroll-x="1520"
|
||||||
|
@update:page="handlePageChange"
|
||||||
|
@update:page-size="handlePageSizeChange"
|
||||||
|
@update:checked-row-keys="handleCheck"
|
||||||
|
/>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 详情 -->
|
||||||
|
<n-modal v-model:show="detailVisible" preset="card" title="委外工单详情" style="width: 760px">
|
||||||
|
<n-descriptions v-if="detailData" :column="2" bordered size="small" label-placement="left">
|
||||||
|
<n-descriptions-item label="工单委外编号">{{ detailData.outsourceCode || '-' }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="当前阶段">{{ statusLabel(detailData.status) }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="委外工序">{{ detailData.name || '-' }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="工序工单号">{{ detailData.businessCode || '-' }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="委外供应商">{{ detailData.supplierName || '-' }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="优先级">{{ priorityLabel(detailData.priority) }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="委外数量">{{ formatQty(detailData.quantity) }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="委外单价">{{ formatMoney(detailData.unitPrice) }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="委外总金额">{{ formatMoney(detailData.totalAmount) }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="已发料数量">{{ formatDecimal(detailData.issuedQuantity) }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="已收货数量">{{ formatDecimal(detailData.receivedQuantity) }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="合格数量">{{ formatDecimal(detailData.qualifiedQuantity) }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="预计开始">{{ detailData.beginPreparationTime || '-' }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="预计结束">{{ detailData.endPreparationTime || '-' }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="备注" :span="2">{{ detailData.remark || '-' }}</n-descriptions-item>
|
||||||
|
</n-descriptions>
|
||||||
|
</n-modal>
|
||||||
|
|
||||||
|
<!-- 编辑 -->
|
||||||
|
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 720px">
|
||||||
|
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="110px">
|
||||||
|
<n-grid :cols="2" :x-gap="16">
|
||||||
|
<n-form-item-gi label="工单委外编号">
|
||||||
|
<n-input :value="formData.outsourceCode" disabled />
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi label="委外工序">
|
||||||
|
<n-input :value="formData.name" disabled />
|
||||||
|
</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="quantity">
|
||||||
|
<n-input-number v-model:value="formData.quantity" :min="1" :precision="0" style="width: 100%" />
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi label="单价" path="unitPrice">
|
||||||
|
<n-input-number v-model:value="formData.unitPrice" :min="0" :precision="2" style="width: 100%" />
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi label="优先级" path="priority">
|
||||||
|
<n-select v-model:value="formData.priority" :options="priorityOptions" />
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi label="当前阶段" path="status">
|
||||||
|
<n-select v-model:value="formData.status" :options="statusOptions" />
|
||||||
|
</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 :span="2" label="备注" path="remark">
|
||||||
|
<n-input v-model:value="formData.remark" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" />
|
||||||
|
</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" :loading="submitLoading" @click="handleSubmit">保存</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, h, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import {
|
||||||
|
NButton, NSpace, NIcon, NTag, NModal, NForm, NFormItem, NFormItemGi, NGrid,
|
||||||
|
NInput, NInputNumber, NSelect, NDatePicker, NDescriptions, NDescriptionsItem,
|
||||||
|
useMessage, useDialog, type DataTableColumns, type FormRules,
|
||||||
|
} from 'naive-ui'
|
||||||
|
import {
|
||||||
|
SearchOutline, RefreshOutline, AddOutline, TrashOutline,
|
||||||
|
CopyOutline, EllipseOutline, TimeOutline,
|
||||||
|
} from '@vicons/ionicons5'
|
||||||
|
import {
|
||||||
|
outsourcingApi,
|
||||||
|
OUTSOURCE_STATUS_MAP,
|
||||||
|
OUTSOURCE_PRIORITY_MAP,
|
||||||
|
type Outsourcing,
|
||||||
|
} from '@/api/outsourcing'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const searchForm = reactive({
|
||||||
|
outsourceCode: '',
|
||||||
|
supplierName: '',
|
||||||
|
name: '',
|
||||||
|
status: null as number | null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusOptions = Object.entries(OUTSOURCE_STATUS_MAP).map(([value, item]) => ({
|
||||||
|
label: item.label,
|
||||||
|
value: Number(value),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const priorityOptions = Object.entries(OUTSOURCE_PRIORITY_MAP).map(([value, item]) => ({
|
||||||
|
label: item.label,
|
||||||
|
value: Number(value),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const tableData = ref<Outsourcing[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const submitLoading = ref(false)
|
||||||
|
const selectedIds = ref<number[]>([])
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
itemCount: 0,
|
||||||
|
showSizePicker: true,
|
||||||
|
pageSizes: [10, 20, 50],
|
||||||
|
})
|
||||||
|
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const detailData = ref<Outsourcing | null>(null)
|
||||||
|
const modalVisible = ref(false)
|
||||||
|
const modalTitle = ref('')
|
||||||
|
const formRef = ref()
|
||||||
|
|
||||||
|
const defaultFormData = (): Record<string, unknown> => ({
|
||||||
|
id: undefined,
|
||||||
|
outsourceCode: '',
|
||||||
|
name: '',
|
||||||
|
supplierName: '',
|
||||||
|
quantity: null,
|
||||||
|
unitPrice: null,
|
||||||
|
priority: 1,
|
||||||
|
status: 1,
|
||||||
|
beginPreparationTime: null,
|
||||||
|
endPreparationTime: null,
|
||||||
|
remark: '',
|
||||||
|
})
|
||||||
|
const formData = reactive<Record<string, unknown>>(defaultFormData())
|
||||||
|
|
||||||
|
const formRules: FormRules = {
|
||||||
|
supplierName: [{ required: true, message: '请输入供应商', trigger: 'blur' }],
|
||||||
|
quantity: [{ required: true, type: 'number', message: '请输入委外数量', trigger: ['blur', 'change'] }],
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatQty(val?: number | null) {
|
||||||
|
if (val == null) return '0.00'
|
||||||
|
return Number(val).toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDecimal(val?: number | string | null) {
|
||||||
|
if (val == null || val === '') return '0.00'
|
||||||
|
return Number(val).toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMoney(val?: number | string | null, mask = false) {
|
||||||
|
if (mask) return '¥***'
|
||||||
|
if (val == null || val === '') return '-'
|
||||||
|
return `¥${Number(val).toFixed(2)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(status?: number) {
|
||||||
|
if (status == null) return '-'
|
||||||
|
return OUTSOURCE_STATUS_MAP[status]?.label ?? String(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
function priorityLabel(priority?: number) {
|
||||||
|
if (priority == null) return '-'
|
||||||
|
return OUTSOURCE_PRIORITY_MAP[priority]?.label ?? String(priority)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyText(text: string) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
message.success('已复制')
|
||||||
|
} catch {
|
||||||
|
message.warning('复制失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: DataTableColumns<Outsourcing> = [
|
||||||
|
{ type: 'selection', width: 48, align: 'center', fixed: 'left' },
|
||||||
|
{
|
||||||
|
title: '工单委外编号',
|
||||||
|
key: 'outsourceCode',
|
||||||
|
width: 200,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'left',
|
||||||
|
ellipsis: { tooltip: true },
|
||||||
|
render(row) {
|
||||||
|
const code = row.outsourceCode || (row.id ? `WW${row.id}` : '-')
|
||||||
|
return h('div', { class: 'cell-center code-cell' }, [
|
||||||
|
h(NButton, {
|
||||||
|
text: true,
|
||||||
|
type: 'primary',
|
||||||
|
size: 'small',
|
||||||
|
onClick: () => openDetail(row),
|
||||||
|
}, { default: () => code }),
|
||||||
|
h(NButton, {
|
||||||
|
text: true,
|
||||||
|
size: 'tiny',
|
||||||
|
onClick: () => copyText(code),
|
||||||
|
}, {
|
||||||
|
default: () => h(NIcon, { size: 14 }, { default: () => h(CopyOutline) }),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '委外数量',
|
||||||
|
key: 'quantity',
|
||||||
|
width: 96,
|
||||||
|
align: 'center',
|
||||||
|
render: (row) => formatQty(row.quantity),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '委外供应商',
|
||||||
|
key: 'supplierName',
|
||||||
|
width: 140,
|
||||||
|
align: 'center',
|
||||||
|
ellipsis: { tooltip: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '委外工序',
|
||||||
|
key: 'name',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
ellipsis: { tooltip: true },
|
||||||
|
render: (row) => row.name || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '委外单价',
|
||||||
|
key: 'unitPrice',
|
||||||
|
width: 96,
|
||||||
|
align: 'center',
|
||||||
|
render: (row) => formatMoney(row.unitPrice, true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '委外总金额',
|
||||||
|
key: 'totalAmount',
|
||||||
|
width: 108,
|
||||||
|
align: 'center',
|
||||||
|
render: (row) => formatMoney(row.totalAmount, true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '优先级',
|
||||||
|
key: 'priority',
|
||||||
|
width: 88,
|
||||||
|
align: 'center',
|
||||||
|
render(row) {
|
||||||
|
const item = OUTSOURCE_PRIORITY_MAP[row.priority ?? 1] ?? OUTSOURCE_PRIORITY_MAP[1]
|
||||||
|
return h('div', { class: 'cell-center' }, [
|
||||||
|
h(NTag, { size: 'small', type: item.type, bordered: false }, { default: () => item.label }),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '已发料数量',
|
||||||
|
key: 'issuedQuantity',
|
||||||
|
width: 104,
|
||||||
|
align: 'center',
|
||||||
|
render: (row) => formatDecimal(row.issuedQuantity),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '已收货数量',
|
||||||
|
key: 'receivedQuantity',
|
||||||
|
width: 104,
|
||||||
|
align: 'center',
|
||||||
|
render: (row) => formatDecimal(row.receivedQuantity),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '合格数量',
|
||||||
|
key: 'qualifiedQuantity',
|
||||||
|
width: 96,
|
||||||
|
align: 'center',
|
||||||
|
render: (row) => formatDecimal(row.qualifiedQuantity),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '当前阶段',
|
||||||
|
key: 'status',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
render(row) {
|
||||||
|
const status = row.status ?? 1
|
||||||
|
const item = OUTSOURCE_STATUS_MAP[status] ?? OUTSOURCE_STATUS_MAP[1]
|
||||||
|
const icon = status === 1 ? TimeOutline : EllipseOutline
|
||||||
|
const color = status === 1 ? '#2080f0' : '#bfbfbf'
|
||||||
|
return h('div', { class: 'cell-center status-cell' }, [
|
||||||
|
h(NIcon, { color, size: 16 }, { default: () => h(icon) }),
|
||||||
|
h('span', item.label),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'actions',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'right',
|
||||||
|
render(row) {
|
||||||
|
return h('div', { class: 'cell-center' }, [
|
||||||
|
h(NSpace, { size: 4, justify: 'center' }, {
|
||||||
|
default: () => [
|
||||||
|
h(NButton, { text: true, type: 'primary', size: 'small', onClick: () => openDetail(row) }, { default: () => '详情' }),
|
||||||
|
h(NButton, { text: true, type: 'primary', size: 'small', onClick: () => handleEdit(row) }, { default: () => '编辑' }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await outsourcingApi.page({
|
||||||
|
page: pagination.page,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
name: searchForm.name || undefined,
|
||||||
|
outsourceCode: searchForm.outsourceCode || undefined,
|
||||||
|
supplierName: searchForm.supplierName || undefined,
|
||||||
|
status: searchForm.status ?? undefined,
|
||||||
|
})
|
||||||
|
tableData.value = res.list ?? []
|
||||||
|
pagination.itemCount = res.total ?? 0
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
pagination.page = 1
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
searchForm.outsourceCode = ''
|
||||||
|
searchForm.supplierName = ''
|
||||||
|
searchForm.name = ''
|
||||||
|
searchForm.status = null
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePageChange(page: number) {
|
||||||
|
pagination.page = page
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePageSizeChange(pageSize: number) {
|
||||||
|
pagination.pageSize = pageSize
|
||||||
|
pagination.page = 1
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCheck(keys: Array<string | number>) {
|
||||||
|
selectedIds.value = keys as number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGoCreate() {
|
||||||
|
router.push('/biz/orderProcessPlan/board')
|
||||||
|
message.info('请在看板中选择工序后创建委外')
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDateTime(val?: string | null) {
|
||||||
|
if (!val) return null
|
||||||
|
const ts = new Date(val.replace(' ', 'T')).getTime()
|
||||||
|
return Number.isNaN(ts) ? null : ts
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openDetail(row: Outsourcing) {
|
||||||
|
if (!row.id) return
|
||||||
|
detailData.value = await outsourcingApi.detail(row.id)
|
||||||
|
detailVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEdit(row: Outsourcing) {
|
||||||
|
modalTitle.value = '编辑委外工单'
|
||||||
|
Object.assign(formData, defaultFormData(), row, {
|
||||||
|
quantity: row.quantity ?? null,
|
||||||
|
unitPrice: row.unitPrice != null ? Number(row.unitPrice) : null,
|
||||||
|
beginPreparationTime: parseDateTime(row.beginPreparationTime),
|
||||||
|
endPreparationTime: parseDateTime(row.endPreparationTime),
|
||||||
|
})
|
||||||
|
modalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSubmitDateTime(ts: number | null) {
|
||||||
|
if (!ts) return undefined
|
||||||
|
return new Date(ts).toISOString().slice(0, 19).replace('T', ' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
await formRef.value?.validate()
|
||||||
|
submitLoading.value = true
|
||||||
|
try {
|
||||||
|
const qty = formData.quantity as number
|
||||||
|
const price = formData.unitPrice as number | null
|
||||||
|
const payload: Outsourcing = {
|
||||||
|
id: formData.id as number,
|
||||||
|
supplierName: formData.supplierName as string,
|
||||||
|
quantity: qty,
|
||||||
|
unitPrice: price ?? undefined,
|
||||||
|
totalAmount: price != null ? Number((qty * price).toFixed(2)) : undefined,
|
||||||
|
priority: formData.priority as number,
|
||||||
|
status: formData.status as number,
|
||||||
|
beginPreparationTime: formatSubmitDateTime(formData.beginPreparationTime as number | null),
|
||||||
|
endPreparationTime: formatSubmitDateTime(formData.endPreparationTime as number | null),
|
||||||
|
remark: (formData.remark as string) || undefined,
|
||||||
|
}
|
||||||
|
await outsourcingApi.update(payload)
|
||||||
|
message.success('保存成功')
|
||||||
|
modalVisible.value = false
|
||||||
|
loadData()
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBatchDelete() {
|
||||||
|
dialog.warning({
|
||||||
|
title: '提示',
|
||||||
|
content: `确定删除选中的 ${selectedIds.value.length} 条委外工单吗?`,
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
await outsourcingApi.delete(selectedIds.value)
|
||||||
|
message.success('删除成功')
|
||||||
|
selectedIds.value = []
|
||||||
|
loadData()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadData)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-form {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-cell {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cell {
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-data-table-th) {
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-data-table-td) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -16,14 +16,13 @@ export default defineConfig({
|
|||||||
emptyOutDir: true
|
emptyOutDir: true
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
// host: "192.168.5.231",
|
|
||||||
host: true,
|
host: true,
|
||||||
port: 3000,
|
port: 3000,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
//target:'http://192.168.6.107:8080', //'http://192.168.5.230:8888',
|
//target:'http://192.168.6.107:8080', //'http://192.168.5.230:8888',
|
||||||
target:'http://192.168.5.230:8888',
|
//target:'http://192.168.5.230:8888',
|
||||||
//target:'http://192.168.5.200:8888',
|
target:'http://localhost:8888',
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
},
|
},
|
||||||
'/druid': {
|
'/druid': {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user