first commit
This commit is contained in:
487
src/views/mes/production/BOM/index.vue
Normal file
487
src/views/mes/production/BOM/index.vue
Normal file
@@ -0,0 +1,487 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :inline="true" :model="queryParams" class="-mb-15px" label-width="84px">
|
||||
<el-form-item label="BOM编号">
|
||||
<el-input
|
||||
v-model="queryParams.code"
|
||||
placeholder="请输入BOM编号"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="BOM名称">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入BOM名称"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品条码">
|
||||
<el-input
|
||||
v-model="queryParams.productBarCode"
|
||||
placeholder="请输入产品条码"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="queryParams.status" placeholder="全部" class="!w-240px" clearable>
|
||||
<el-option :value="1" label="启用" />
|
||||
<el-option :value="0" label="禁用" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<el-button type="primary" plain @click="openForm('create')"
|
||||
><Icon icon="ep:plus" class="mr-5px" /> 新增</el-button
|
||||
>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
:disabled="selectionList.length === 0"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
<Icon icon="ep:delete" class="mr-5px" /> 删除
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<ContentWrap>
|
||||
<el-table :data="list" v-loading="loading" :stripe="true" @selection-change="onSelectionChange">
|
||||
<el-table-column type="selection" width="50" />
|
||||
<el-table-column prop="code" label="BOM编号" min-width="140" />
|
||||
<el-table-column prop="name" label="BOM名称" min-width="180" />
|
||||
<el-table-column label="产品信息" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.productName }}</div>
|
||||
<div class="text-gray-400 text-sm">{{ row.productBarCode }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="version" label="版本" width="90" align="center" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'">{{
|
||||
row.status === 1 ? '启用' : '禁用'
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="itemCount" label="组件数" width="90" align="center" />
|
||||
<el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="操作" fixed="right" width="220" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip content="编辑" placement="top">
|
||||
<el-button link type="primary" @click="openForm('update', row.id)"
|
||||
><Icon icon="ep:edit"
|
||||
/></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="明细" placement="top">
|
||||
<el-button link type="info" @click="openDetail(row)"><Icon icon="ep:list" /></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button link type="danger" @click="handleDelete(row.id)"
|
||||
><Icon icon="ep:delete"
|
||||
/></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<el-dialog v-model="formDialogVisible" :title="formDialogTitle" width="700px" append-to-body>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="BOM编号" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="请输入BOM编号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="版本" prop="version">
|
||||
<el-input v-model="formData.version" placeholder="例如:V1.0" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="BOM名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入BOM名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-switch v-model="formData.status" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="产品条码" prop="productBarCode">
|
||||
<el-input v-model="formData.productBarCode" placeholder="请输入成品条码" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="产品名称" prop="productName">
|
||||
<el-input v-model="formData.productName" placeholder="请输入成品名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="formDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="formSubmitting" @click="submitForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 明细弹窗(只读,演示用) -->
|
||||
<el-dialog v-model="detailDialogVisible" title="BOM 明细" width="800px" append-to-body>
|
||||
<div class="mb-2 text-gray-500">
|
||||
<span>成品:</span>
|
||||
<span class="mr-2">{{ currentDetail?.productName }}</span>
|
||||
<span class="text-gray-400">({{ currentDetail?.productBarCode }})</span>
|
||||
</div>
|
||||
<el-table :data="detailItems" border stripe>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="materialCode" label="物料编码" min-width="140" />
|
||||
<el-table-column prop="materialName" label="物料名称" min-width="160" />
|
||||
<el-table-column prop="unit" label="单位" width="90" align="center" />
|
||||
<el-table-column prop="quantity" label="用量" width="100" align="center" />
|
||||
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="detailDialogVisible = false">知道了</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Icon } from '@/components/Icon'
|
||||
|
||||
// 可替换的 Service 层:未来直接改为真实接口调用即可
|
||||
// 约定:列表分页、增删改查、明细查询
|
||||
interface BomItemVO {
|
||||
id: number
|
||||
materialCode: string
|
||||
materialName: string
|
||||
unit: string
|
||||
quantity: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
interface BomVO {
|
||||
id: number
|
||||
code: string
|
||||
name: string
|
||||
productBarCode: string
|
||||
productName: string
|
||||
version: string
|
||||
status: 0 | 1
|
||||
itemCount: number
|
||||
remark?: string
|
||||
updateTime: string
|
||||
}
|
||||
|
||||
interface BomQuery {
|
||||
pageNo: number
|
||||
pageSize: number
|
||||
code?: string
|
||||
name?: string
|
||||
productBarCode?: string
|
||||
status?: number | undefined
|
||||
}
|
||||
|
||||
interface BomPageResult {
|
||||
list: BomVO[]
|
||||
total: number
|
||||
}
|
||||
|
||||
interface BomService {
|
||||
getPage(params: BomQuery): Promise<BomPageResult>
|
||||
get(id: number): Promise<BomVO | null>
|
||||
getItems(id: number): Promise<BomItemVO[]>
|
||||
create(data: Partial<BomVO>): Promise<void>
|
||||
update(data: Partial<BomVO>): Promise<void>
|
||||
delete(id: number): Promise<void>
|
||||
deleteBatch(ids: number[]): Promise<void>
|
||||
}
|
||||
|
||||
// Mock 数据源(可无缝替换为真实接口)
|
||||
const mockDb = reactive({
|
||||
boms: [
|
||||
{
|
||||
id: 1,
|
||||
code: 'BOM-202501-001',
|
||||
name: 'A产品标准BOM',
|
||||
productBarCode: 'P-100001',
|
||||
productName: 'A产品',
|
||||
version: 'V1.0',
|
||||
status: 1,
|
||||
itemCount: 3,
|
||||
remark: '首版',
|
||||
updateTime: '2025-01-01 10:00:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
code: 'BOM-202501-002',
|
||||
name: 'B产品试制BOM',
|
||||
productBarCode: 'P-100002',
|
||||
productName: 'B产品',
|
||||
version: 'V0.9',
|
||||
status: 0,
|
||||
itemCount: 2,
|
||||
remark: '试制用',
|
||||
updateTime: '2025-01-03 15:20:00'
|
||||
}
|
||||
] as BomVO[],
|
||||
items: new Map<number, BomItemVO[]>([
|
||||
[
|
||||
1,
|
||||
[
|
||||
{ id: 11, materialCode: 'M-200001', materialName: '外壳', unit: '个', quantity: 1 },
|
||||
{ id: 12, materialCode: 'M-200002', materialName: '主板', unit: '块', quantity: 1 },
|
||||
{ id: 13, materialCode: 'M-200003', materialName: '螺丝', unit: '颗', quantity: 6 }
|
||||
]
|
||||
],
|
||||
[
|
||||
2,
|
||||
[
|
||||
{ id: 21, materialCode: 'M-300001', materialName: '外壳(B)', unit: '个', quantity: 1 },
|
||||
{ id: 22, materialCode: 'M-300002', materialName: '主板(B)', unit: '块', quantity: 1 }
|
||||
]
|
||||
]
|
||||
])
|
||||
})
|
||||
|
||||
const mockService: BomService = {
|
||||
async getPage(params: BomQuery): Promise<BomPageResult> {
|
||||
const { pageNo, pageSize, code, name, productBarCode, status } = params
|
||||
let data = [...mockDb.boms]
|
||||
if (code) data = data.filter((b) => b.code.includes(code))
|
||||
if (name) data = data.filter((b) => b.name.includes(name))
|
||||
if (productBarCode) data = data.filter((b) => b.productBarCode.includes(productBarCode))
|
||||
if (status !== undefined && status !== null && status !== ('' as any)) {
|
||||
data = data.filter((b) => b.status === (status as 0 | 1))
|
||||
}
|
||||
const total = data.length
|
||||
const start = (pageNo - 1) * pageSize
|
||||
const end = start + pageSize
|
||||
const pageList = data.slice(start, end)
|
||||
await sleep(200)
|
||||
return { list: pageList, total }
|
||||
},
|
||||
async get(id: number): Promise<BomVO | null> {
|
||||
await sleep(120)
|
||||
return mockDb.boms.find((b) => b.id === id) || null
|
||||
},
|
||||
async getItems(id: number): Promise<BomItemVO[]> {
|
||||
await sleep(180)
|
||||
return mockDb.items.get(id) || []
|
||||
},
|
||||
async create(data: Partial<BomVO>): Promise<void> {
|
||||
await sleep(150)
|
||||
const id = Math.max(0, ...mockDb.boms.map((b) => b.id)) + 1
|
||||
const now = new Date().toISOString().replace('T', ' ').slice(0, 19)
|
||||
mockDb.boms.unshift({
|
||||
id,
|
||||
code: data.code || `BOM-${id}`,
|
||||
name: data.name || `未命名BOM-${id}`,
|
||||
productBarCode: data.productBarCode || '',
|
||||
productName: data.productName || '',
|
||||
version: data.version || 'V1.0',
|
||||
status: (data.status as 0 | 1) ?? 1,
|
||||
itemCount: 0,
|
||||
remark: data.remark || '',
|
||||
updateTime: now
|
||||
})
|
||||
mockDb.items.set(id, [])
|
||||
},
|
||||
async update(data: Partial<BomVO>): Promise<void> {
|
||||
await sleep(150)
|
||||
if (!data.id) return
|
||||
const idx = mockDb.boms.findIndex((b) => b.id === data.id)
|
||||
if (idx >= 0) {
|
||||
const now = new Date().toISOString().replace('T', ' ').slice(0, 19)
|
||||
mockDb.boms[idx] = { ...mockDb.boms[idx], ...data, updateTime: now } as BomVO
|
||||
}
|
||||
},
|
||||
async delete(id: number): Promise<void> {
|
||||
await sleep(120)
|
||||
const idx = mockDb.boms.findIndex((b) => b.id === id)
|
||||
if (idx >= 0) mockDb.boms.splice(idx, 1)
|
||||
mockDb.items.delete(id)
|
||||
},
|
||||
async deleteBatch(ids: number[]): Promise<void> {
|
||||
await sleep(180)
|
||||
for (const id of ids) {
|
||||
const i = mockDb.boms.findIndex((b) => b.id === id)
|
||||
if (i >= 0) mockDb.boms.splice(i, 1)
|
||||
mockDb.items.delete(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
// 如果要替换真实接口,仅需在此处将 service 指向真实实现
|
||||
const service: BomService = mockService
|
||||
|
||||
// 页面状态
|
||||
const loading = ref(false)
|
||||
const list = ref<BomVO[]>([])
|
||||
const total = ref(0)
|
||||
const selectionList = ref<BomVO[]>([])
|
||||
|
||||
const queryParams = reactive<BomQuery>({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
code: undefined,
|
||||
name: undefined,
|
||||
productBarCode: undefined,
|
||||
status: undefined
|
||||
})
|
||||
|
||||
const formDialogVisible = ref(false)
|
||||
const formDialogTitle = ref('')
|
||||
const formSubmitting = ref(false)
|
||||
const formRef = ref()
|
||||
const formData = reactive<Partial<BomVO>>({
|
||||
id: undefined,
|
||||
code: '',
|
||||
name: '',
|
||||
productBarCode: '',
|
||||
productName: '',
|
||||
version: 'V1.0',
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
const formRules = reactive({
|
||||
code: [{ required: true, message: '请输入BOM编号', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入BOM名称', trigger: 'blur' }],
|
||||
productBarCode: [{ required: true, message: '请输入产品条码', trigger: 'blur' }],
|
||||
productName: [{ required: true, message: '请输入产品名称', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const detailDialogVisible = ref(false)
|
||||
const currentDetail = ref<BomVO | null>(null)
|
||||
const detailItems = ref<BomItemVO[]>([])
|
||||
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await service.getPage({ ...queryParams })
|
||||
list.value = res.list
|
||||
total.value = res.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleQuery() {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
function resetQuery() {
|
||||
queryParams.pageNo = 1
|
||||
queryParams.code = undefined
|
||||
queryParams.name = undefined
|
||||
queryParams.productBarCode = undefined
|
||||
queryParams.status = undefined
|
||||
getList()
|
||||
}
|
||||
|
||||
function onSelectionChange(rows: BomVO[]) {
|
||||
selectionList.value = rows
|
||||
}
|
||||
|
||||
function openForm(type: 'create' | 'update', id?: number) {
|
||||
formDialogVisible.value = true
|
||||
formDialogTitle.value = type === 'create' ? '新增BOM' : '编辑BOM'
|
||||
if (type === 'create') {
|
||||
Object.assign(formData, {
|
||||
id: undefined,
|
||||
code: '',
|
||||
name: '',
|
||||
productBarCode: '',
|
||||
productName: '',
|
||||
version: 'V1.0',
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
} else if (id) {
|
||||
service.get(id).then((data) => {
|
||||
if (data) Object.assign(formData, data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
;(formRef.value as any)?.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
formSubmitting.value = true
|
||||
try {
|
||||
if (formData.id) {
|
||||
await service.update(formData)
|
||||
ElMessage.success('修改成功')
|
||||
} else {
|
||||
await service.create(formData)
|
||||
ElMessage.success('新增成功')
|
||||
}
|
||||
formDialogVisible.value = false
|
||||
getList()
|
||||
} finally {
|
||||
formSubmitting.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该BOM吗?', '提示', { type: 'warning' })
|
||||
await service.delete(id)
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
if (!selectionList.value.length) return
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除选中的 ${selectionList.value.length} 条记录吗?`, '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
await service.deleteBatch(selectionList.value.map((i) => i.id))
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function openDetail(row: BomVO) {
|
||||
currentDetail.value = row
|
||||
detailDialogVisible.value = true
|
||||
detailItems.value = []
|
||||
const items = await service.getItems(row.id)
|
||||
detailItems.value = items
|
||||
}
|
||||
|
||||
getList()
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<Dialog title="查看领料单" v-model="dialogVisible" width="80%">
|
||||
<Descriptions :schema="schema" :data="detailData" />
|
||||
|
||||
<el-divider content-position="left">领料明细</el-divider>
|
||||
<el-table :data="detailData.items" border style="width: 100%" max-height="480">
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column label="物料信息" min-width="240">
|
||||
<template #default="{ row }">
|
||||
<div class="text-13px text-gray-600">编码:{{ row.materialCode }}</div>
|
||||
<div class="text-13px">名称:{{ row.materialName }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单位" prop="unit" width="80" />
|
||||
<el-table-column label="计划数量" prop="planQuantity" width="120" />
|
||||
<el-table-column label="实际数量" prop="actualQuantity" width="120" />
|
||||
<el-table-column label="仓库" prop="warehouseName" width="140" />
|
||||
<el-table-column label="过磅单号" prop="no" width="160" />
|
||||
<el-table-column label="备注" prop="remark" min-width="160" />
|
||||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip content="过磅详情" placement="top" v-if="row.purchaseId">
|
||||
<el-button link type="primary" @click="openWeighDetail(row.purchaseId)">
|
||||
<Icon icon="ep:document" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- 过磅单详情弹窗 -->
|
||||
<Dialog title="过磅单详情" v-model="weighDialogVisible" width="50%">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="过磅单号">{{ weighDetail?.no || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="入库状态">{{
|
||||
weighDetail?.inStatus === 1 ? '已入库' : '未入库'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="产品">{{
|
||||
weighDetail?.productName || '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="车牌">{{
|
||||
weighDetail?.vehicleNumber || '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="毛重">{{
|
||||
weighDetail?.grossWeight != null ? weighDetail.grossWeight + ' kg' : '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="皮重">{{
|
||||
weighDetail?.tareWeight != null ? weighDetail.tareWeight + ' kg' : '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="净重">{{
|
||||
weighDetail?.netWeight != null ? weighDetail.netWeight + ' kg' : '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="计划重量">{{
|
||||
weighDetail?.plannedWeight != null ? weighDetail.plannedWeight + ' kg' : '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="杂质率">{{
|
||||
weighDetail?.impurityRate != null ? (weighDetail.impurityRate * 100).toFixed(2) + '%' : '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="供应商">{{
|
||||
weighDetail?.supplierName || '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="供应商类型">{{
|
||||
weighDetail?.supplierName2 || '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="身份证号">{{
|
||||
weighDetail?.idCardNumber || '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="司机">{{ weighDetail?.driver || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="管理员">{{
|
||||
weighDetail?.administrator || '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="过磅员">{{ weighDetail?.weigher || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="产地证编号">{{
|
||||
weighDetail?.originCertificateNumber || '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="附件">
|
||||
<template v-if="weighDetail?.fileUrl">
|
||||
<a :href="weighDetail.fileUrl" target="_blank">查看附件</a>
|
||||
</template>
|
||||
<template v-else>-</template>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{
|
||||
weighDetail?.remark || '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">{{
|
||||
weighDetail?.createTime ? formatDate(weighDetail.createTime) : '-'
|
||||
}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 检验信息部分 - 仅当有检验数据时显示 -->
|
||||
<template v-if="hasInspectionData">
|
||||
<el-divider content-position="left">检验信息</el-divider>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="取样日期">{{
|
||||
weighDetail?.sampleDate ? formatDate(weighDetail.sampleDate) : '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="取样地点">{{
|
||||
weighDetail?.sampleLocation || '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="代表重量">{{
|
||||
weighDetail?.representativeWeight != null ? weighDetail.representativeWeight + ' kg' : '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="样品重量">{{
|
||||
weighDetail?.sampleWeight != null ? weighDetail.sampleWeight + ' kg' : '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="杂质重量">{{
|
||||
weighDetail?.impurityWeight != null ? weighDetail.impurityWeight + ' kg' : '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="沙土重量">{{
|
||||
weighDetail?.sandWeight != null ? weighDetail.sandWeight + ' kg' : '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="感官">{{
|
||||
weighDetail?.sensoryEvaluation || '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="合格率">{{
|
||||
weighDetail?.qualificationRate ? weighDetail.qualificationRate + '%' : '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="水分">{{
|
||||
weighDetail?.moistureContent ? weighDetail.moistureContent + '%' : '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="含糖">{{
|
||||
weighDetail?.sugarContent ? weighDetail.sugarContent + '%' : '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="RA值">{{
|
||||
weighDetail?.raValue || '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="STV值">{{
|
||||
weighDetail?.stvValue || '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="化验员">{{
|
||||
weighDetail?.labTechnician || '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="检验备注" :span="2">{{
|
||||
weighDetail?.inspectionRemark || '-'
|
||||
}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { getMaterialRequisition } from '@/api/mes/production/material-requisition'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import { WeighApi } from '@/api/erp/purchase/weigh'
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const detailData = ref<any>({
|
||||
items: []
|
||||
})
|
||||
|
||||
const weighDialogVisible = ref(false)
|
||||
const weighDetail = ref<any>(null)
|
||||
|
||||
// 判断是否有检验数据
|
||||
const hasInspectionData = computed(() => {
|
||||
if (!weighDetail.value) return false
|
||||
return !!(
|
||||
weighDetail.value.sampleDate ||
|
||||
weighDetail.value.sampleLocation ||
|
||||
weighDetail.value.representativeWeight ||
|
||||
weighDetail.value.sampleWeight ||
|
||||
weighDetail.value.impurityWeight ||
|
||||
weighDetail.value.sandWeight ||
|
||||
weighDetail.value.sensoryEvaluation ||
|
||||
weighDetail.value.qualificationRate ||
|
||||
weighDetail.value.moistureContent ||
|
||||
weighDetail.value.sugarContent ||
|
||||
weighDetail.value.raValue ||
|
||||
weighDetail.value.stvValue ||
|
||||
weighDetail.value.labTechnician ||
|
||||
weighDetail.value.inspectionRemark
|
||||
)
|
||||
})
|
||||
|
||||
const schema = [
|
||||
{
|
||||
label: '领料单号',
|
||||
field: 'code'
|
||||
},
|
||||
{
|
||||
label: '工单编号',
|
||||
field: 'orderCode'
|
||||
},
|
||||
{
|
||||
label: '领料时间',
|
||||
field: 'requisitionTime',
|
||||
formatter: (val: string) => (val ? formatDate(val) : '')
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
field: 'status',
|
||||
formatter: (val: number) => {
|
||||
const statusMap: Record<number, string> = {
|
||||
0: '草稿',
|
||||
1: '待审核',
|
||||
2: '已审核',
|
||||
3: '已领料',
|
||||
4: '已取消'
|
||||
}
|
||||
return statusMap[val] || '未知'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '申请人',
|
||||
field: 'applicantName'
|
||||
},
|
||||
{
|
||||
label: '审批人',
|
||||
field: 'approverName'
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
field: 'remark'
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
field: 'createTime',
|
||||
formatter: (val: string) => (val ? formatDate(val) : '')
|
||||
}
|
||||
]
|
||||
|
||||
const open = async (id: number) => {
|
||||
dialogVisible.value = true
|
||||
const res = await getMaterialRequisition(id)
|
||||
detailData.value = res
|
||||
}
|
||||
|
||||
const openWeighDetail = async (purchaseId: number) => {
|
||||
try {
|
||||
const res: any = await WeighApi.getWeigh(purchaseId)
|
||||
weighDetail.value = res?.data || res
|
||||
weighDialogVisible.value = true
|
||||
} catch (e) {
|
||||
console.error('获取过磅单详情失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open
|
||||
})
|
||||
</script>
|
||||
357
src/views/mes/production/material-requisition/index.vue
Normal file
357
src/views/mes/production/material-requisition/index.vue
Normal file
@@ -0,0 +1,357 @@
|
||||
<template>
|
||||
<doc-alert title="【MES】生产领料管理" url="https://doc.iocoder.cn/mes/material-requisition/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="领料单号" prop="code">
|
||||
<el-input
|
||||
v-model="queryParams.code"
|
||||
placeholder="请输入领料单号"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="工单编号" prop="orderCode">
|
||||
<el-input
|
||||
v-model="queryParams.orderCode"
|
||||
placeholder="请输入工单编号"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
|
||||
<el-option label="待审核" :value="1" />
|
||||
<el-option label="已审核" :value="2" />
|
||||
<el-option label="已领料" :value="3" />
|
||||
<el-option label="已取消" :value="4" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="openForm('create')"
|
||||
v-hasPermi="['mes:material-requisition:create']"
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
</el-button>
|
||||
<!-- <el-button
|
||||
type="danger"
|
||||
plain
|
||||
@click="handleDelete(selectionList.map((item) => item.id))"
|
||||
v-hasPermi="['mes:material-requisition:delete']"
|
||||
:disabled="selectionList.length === 0"
|
||||
>
|
||||
<Icon icon="ep:delete" class="mr-5px" /> 删除
|
||||
</el-button> -->
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
:stripe="true"
|
||||
:show-overflow-tooltip="true"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-click="handleRowClick"
|
||||
>
|
||||
<el-table-column type="selection" width="50" />
|
||||
<el-table-column label="领料单号" align="center" prop="code" min-width="120" />
|
||||
<el-table-column label="批次信息" align="center" prop="orderCode" min-width="120" />
|
||||
<el-table-column label="领料时间" align="center" prop="requisitionTime" min-width="150" sortable>
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.requisitionTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="status" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.status)">
|
||||
{{ getStatusText(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="申请人" align="center" prop="applicantName" min-width="100" />
|
||||
<el-table-column label="审批人" align="center" prop="approverName" min-width="100" />
|
||||
<el-table-column label="备注" align="center" prop="remark" min-width="120" />
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" min-width="150" sortable>
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" fixed="right" min-width="160">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="查看" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleView(scope.row.id)"
|
||||
v-hasPermi="['mes:material-requisition:query']"
|
||||
>
|
||||
<Icon icon="ep:view" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="编辑" placement="top">
|
||||
<el-button
|
||||
v-if="scope.row.status === 1"
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
v-hasPermi="['mes:material-requisition:update']"
|
||||
>
|
||||
<Icon icon="ep:edit" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="审核" placement="top">
|
||||
<el-button
|
||||
v-if="scope.row.status === 1"
|
||||
link
|
||||
type="success"
|
||||
@click="handleApprove(scope.row.id)"
|
||||
v-hasPermi="['mes:material-requisition:approve']"
|
||||
>
|
||||
<Icon icon="ep:check" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="确认领料" placement="top">
|
||||
<el-button
|
||||
v-if="scope.row.status === 2"
|
||||
link
|
||||
type="success"
|
||||
@click="handleComplete(scope.row.id)"
|
||||
v-hasPermi="['mes:material-requisition:complete']"
|
||||
>
|
||||
<Icon icon="ep:finished" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="反审核" placement="top">
|
||||
<el-button
|
||||
v-if="scope.row.status === 2"
|
||||
link
|
||||
type="warning"
|
||||
@click="handleReverse(scope.row.id)"
|
||||
v-hasPermi="['mes:material-requisition:reverse']"
|
||||
>
|
||||
<Icon icon="ep:refresh-left" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<!-- <el-tooltip content="删除" placement="top">
|
||||
<el-button
|
||||
v-if="[0, 4].includes(scope.row.status)"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete([scope.row.id])"
|
||||
v-hasPermi="['mes:material-requisition:delete']"
|
||||
>
|
||||
<Icon icon="ep:delete" />
|
||||
</el-button>
|
||||
</el-tooltip> -->
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<MaterialRequisitionForm ref="formRef" @success="getList" />
|
||||
<!-- 查看弹窗 -->
|
||||
<MaterialRequisitionView ref="viewRef" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import {
|
||||
getMaterialRequisitionPage,
|
||||
approveMaterialRequisition,
|
||||
reverseMaterialRequisition
|
||||
} from '@/api/mes/production/material-requisition'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import MaterialRequisitionForm from './MaterialRequisitionForm.vue'
|
||||
import MaterialRequisitionView from './MaterialRequisitionView.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const currentUser = computed(() => userStore.user)
|
||||
|
||||
const loading = ref(true)
|
||||
const list = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
code: undefined,
|
||||
orderCode: undefined,
|
||||
status: undefined,
|
||||
createTime: undefined
|
||||
})
|
||||
const queryFormRef = ref()
|
||||
const formRef = ref()
|
||||
const viewRef = ref()
|
||||
const selectionList = ref<any[]>([])
|
||||
|
||||
const getStatusType = (status: number) => {
|
||||
const types = {
|
||||
1: 'warning',
|
||||
2: 'success',
|
||||
3: 'success',
|
||||
4: 'danger'
|
||||
}
|
||||
return types[status]
|
||||
}
|
||||
|
||||
const getStatusText = (status: number) => {
|
||||
const texts = {
|
||||
1: '待审核',
|
||||
2: '已审核',
|
||||
3: '已领料',
|
||||
4: '已取消'
|
||||
}
|
||||
return texts[status]
|
||||
}
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getMaterialRequisitionPage(queryParams)
|
||||
list.value = res.list
|
||||
total.value = res.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields?.()
|
||||
queryParams.code = undefined
|
||||
queryParams.orderCode = undefined
|
||||
queryParams.status = undefined
|
||||
queryParams.createTime = undefined
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const openForm = (type: 'create' | 'update', id?: number) => {
|
||||
console.log('打开表单:', type, id)
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
const handleView = (id: number) => {
|
||||
console.log('查看详情:', id)
|
||||
viewRef.value.open(id)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const handleApprove = async (id: number) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要审核通过该领料单吗?', '提示', { type: 'warning' })
|
||||
await approveMaterialRequisition({
|
||||
id,
|
||||
approved: true,
|
||||
approverId: currentUser.value.id,
|
||||
approverName: currentUser.value.nickname,
|
||||
remark: ''
|
||||
})
|
||||
ElMessage.success('审核成功')
|
||||
getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleComplete = async (id: number) => {
|
||||
console.log('确认领料:', id)
|
||||
formRef.value.openComplete(id)
|
||||
}
|
||||
|
||||
const handleReverse = async (id: number) => {
|
||||
try {
|
||||
const { value: remark } = await ElMessageBox.prompt('请输入反审核原因', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValidator: (value) => {
|
||||
if (!value) {
|
||||
return '反审核原因不能为空'
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
await reverseMaterialRequisition({
|
||||
id,
|
||||
reverserId: currentUser.value.id,
|
||||
reverserName: currentUser.value.nickname,
|
||||
remark
|
||||
})
|
||||
ElMessage.success('反审核成功')
|
||||
getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
|
||||
const handleSelectionChange = (rows: any[]) => {
|
||||
selectionList.value = rows
|
||||
}
|
||||
|
||||
/** 行点击操作 */
|
||||
const handleRowClick = (row: any, _column: any, event: MouseEvent) => {
|
||||
// 检查是否点击了按钮、链接或其他交互元素
|
||||
const target = event.target as HTMLElement
|
||||
if (
|
||||
target.tagName === 'BUTTON' ||
|
||||
target.tagName === 'A' ||
|
||||
target.tagName === 'I' ||
|
||||
target.tagName === 'svg' ||
|
||||
target.closest('button') ||
|
||||
target.closest('a') ||
|
||||
target.closest('.el-button') ||
|
||||
target.closest('.el-checkbox')
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// 待审核状态打开编辑页面,其他状态打开查看页面
|
||||
if (row.status === 1) {
|
||||
openForm('update', row.id)
|
||||
} else {
|
||||
handleView(row.id)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
417
src/views/mes/production/plan/detail/index.vue
Normal file
417
src/views/mes/production/plan/detail/index.vue
Normal file
@@ -0,0 +1,417 @@
|
||||
<template>
|
||||
<doc-alert title="【MES】生产计划详情" url="https://doc.iocoder.cn/mes/production-plan/" />
|
||||
|
||||
<ContentWrap v-loading="loading">
|
||||
<!-- 计划基本信息 -->
|
||||
<el-descriptions title="计划基本信息" :column="3" border>
|
||||
<el-descriptions-item label="计划编号">{{ planInfo.planCode }}</el-descriptions-item>
|
||||
<el-descriptions-item label="计划名称">{{ planInfo.planName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="计划类型">
|
||||
<el-tag :type="getPlanTypeTagType(planInfo.planType)">
|
||||
{{ getPlanTypeText(planInfo.planType) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="计划周期">{{ planInfo.planPeriod }}</el-descriptions-item>
|
||||
<el-descriptions-item label="开始日期">{{ planInfo.startDate }}</el-descriptions-item>
|
||||
<el-descriptions-item label="结束日期">{{ planInfo.endDate }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="getStatusType(planInfo.status)">
|
||||
{{ getStatusText(planInfo.status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ planInfo.createTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="3">{{ planInfo.remark || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 完成情况统计 -->
|
||||
<el-card class="mt-20px" shadow="never">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-bold">完成情况统计</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleRefresh"
|
||||
>
|
||||
<Icon icon="ep:refresh" class="mr-5px" /> 刷新进度
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">总计划数量</div>
|
||||
<div class="stat-value text-blue-600">{{ planInfo.totalPlanQuantity || 0 }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">已完成数量</div>
|
||||
<div class="stat-value text-green-600">{{ planInfo.totalCompletedQuantity || 0 }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">进行中数量</div>
|
||||
<div class="stat-value text-orange-600">{{ planInfo.totalInProgressQuantity || 0 }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">整体完成率</div>
|
||||
<div class="stat-value" :style="{ color: getProgressColor(planInfo.overallCompletionRate) }">
|
||||
{{ planInfo.overallCompletionRate || 0 }}%
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-progress
|
||||
:percentage="planInfo.overallCompletionRate || 0"
|
||||
:color="getProgressColor(planInfo.overallCompletionRate)"
|
||||
class="mt-20px"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 计划明细 -->
|
||||
<el-card class="mt-20px" shadow="never">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-bold">计划明细</span>
|
||||
<el-button type="primary" size="small" @click="handleQueryOrders">
|
||||
<Icon icon="ep:search" class="mr-5px" /> 查询工单
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="planInfo.items" border>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column label="产品信息" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="font-bold">{{ row.productName }}</div>
|
||||
<div class="text-sm text-gray-500">{{ row.productCode }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="工序路线" prop="routeName" min-width="120" align="center" />
|
||||
<el-table-column label="计划数量" prop="planQuantity" width="100" align="center" />
|
||||
<el-table-column label="已完成" prop="completedQuantity" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="text-green-600 font-bold">{{ row.completedQuantity || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="进行中" prop="inProgressQuantity" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="text-orange-600 font-bold">{{ row.inProgressQuantity || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="完成率" prop="completionRate" width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-progress
|
||||
:percentage="row.completionRate || 0"
|
||||
:color="getProgressColor(row.completionRate)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="优先级" prop="priority" width="80" align="center" />
|
||||
<el-table-column label="计划时间" min-width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
<div>开始:{{ row.plannedStartDate || '-' }}</div>
|
||||
<div class="mt-5px">结束:{{ row.plannedEndDate || '-' }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="实际时间" min-width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
<div>开始:{{ row.actualStartDate || '-' }}</div>
|
||||
<div class="mt-5px">结束:{{ row.actualEndDate || '-' }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" min-width="150" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 工单列表 -->
|
||||
<el-card class="mt-20px" shadow="never" v-if="showOrders">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-bold">工单列表</span>
|
||||
<el-tag type="info">共 {{ orderList.length }} 条工单</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="orderList" border v-loading="orderLoading">
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column label="工单编号" prop="code" min-width="150" align="center" />
|
||||
<el-table-column label="工单名称" prop="name" min-width="150" align="center" />
|
||||
<el-table-column label="产品信息" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="font-bold">{{ row.productName }}</div>
|
||||
<div class="text-sm text-gray-500">{{ row.productCode }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="工艺路线" prop="routeName" min-width="120" align="center" />
|
||||
<el-table-column label="计划数量" prop="planQuantity" width="100" align="center" />
|
||||
<el-table-column label="已生产" prop="producedQuantity" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="text-blue-600 font-bold">{{ row.producedQuantity || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="合格数" prop="qualifiedQuantity" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="text-green-600 font-bold">{{ row.qualifiedQuantity || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="不合格" prop="unqualifiedQuantity" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="text-red-600 font-bold">{{ row.unqualifiedQuantity || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="优先级" prop="priority" width="80" align="center" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getOrderStatusType(row.status)">
|
||||
{{ getOrderStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="计划时间" min-width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
<div>开始:{{ row.planStartTime || '-' }}</div>
|
||||
<div class="mt-5px">结束:{{ row.planEndTime || '-' }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="实际时间" min-width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
<div>开始:{{ row.actualStartTime || '-' }}</div>
|
||||
<div class="mt-5px">结束:{{ row.actualEndTime || '-' }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" min-width="150" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mt-20px text-center">
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleEdit"
|
||||
v-if="planInfo.status === 0"
|
||||
v-hasPermi="['mes:production-plan:update']"
|
||||
>
|
||||
编辑计划
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
@click="handlePublish"
|
||||
v-if="planInfo.status === 0"
|
||||
v-hasPermi="['mes:production-plan:publish']"
|
||||
>
|
||||
发布计划
|
||||
</el-button>
|
||||
<!-- <el-button-->
|
||||
<!-- type="warning"-->
|
||||
<!-- @click="handleGenerateOrders"-->
|
||||
<!-- v-if="planInfo.status >= 1 && planInfo.status <= 2"-->
|
||||
<!-- v-hasPermi="['mes:production-plan:generate-orders']"-->
|
||||
<!-- >-->
|
||||
<!-- 生成工单-->
|
||||
<!-- </el-button>-->
|
||||
</div>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 生成工单弹窗 -->
|
||||
<GenerateOrderDialog ref="generateOrderRef" @success="getDetail" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import GenerateOrderDialog from '../list/GenerateOrderDialog.vue'
|
||||
import {
|
||||
getProductionPlan,
|
||||
publishProductionPlan,
|
||||
updatePlanProgress
|
||||
} from '@/api/mes/production/plan'
|
||||
import { YVHgetWorkOrderPage } from '@/api/mes/production/workorder'
|
||||
|
||||
defineOptions({ name: 'MesProductionPlanDetail' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const planInfo = ref<any>({})
|
||||
const generateOrderRef = ref()
|
||||
const showOrders = ref(false)
|
||||
const orderLoading = ref(false)
|
||||
const orderList = ref<any[]>([])
|
||||
|
||||
const planId = ref<number>(Number(route.query.id))
|
||||
|
||||
const getDetail = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getProductionPlan(planId.value)
|
||||
planInfo.value = res
|
||||
} catch (error) {
|
||||
console.error('获取计划详情失败:', error)
|
||||
ElMessage.error('获取计划详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = async () => {
|
||||
try {
|
||||
await updatePlanProgress(planId.value)
|
||||
ElMessage.success('进度更新成功')
|
||||
getDetail()
|
||||
} catch (error) {
|
||||
console.error('更新进度失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push({
|
||||
path: '/mes/production/plan/list',
|
||||
query: { editId: planId.value }
|
||||
})
|
||||
}
|
||||
|
||||
const handlePublish = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要发布该计划吗?发布后将不能修改。', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
await publishProductionPlan(planId.value)
|
||||
ElMessage.success('发布成功')
|
||||
getDetail()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleGenerateOrders = () => {
|
||||
generateOrderRef.value.open(planInfo.value)
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const handleQueryOrders = async () => {
|
||||
orderLoading.value = true
|
||||
showOrders.value = true
|
||||
try {
|
||||
const res = await YVHgetWorkOrderPage({
|
||||
sourceType: 2,
|
||||
sourceId: planId.value,
|
||||
pageNo: 1,
|
||||
pageSize: 100
|
||||
})
|
||||
orderList.value = res.list || []
|
||||
ElMessage.success(`查询成功,共找到 ${orderList.value.length} 条工单`)
|
||||
} catch (error) {
|
||||
console.error('查询工单失败:', error)
|
||||
ElMessage.error('查询工单失败')
|
||||
} finally {
|
||||
orderLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getOrderStatusText = (status: number) => {
|
||||
const map: Record<number, string> = {
|
||||
0: '草稿',
|
||||
1: '待审核',
|
||||
2: '已审核',
|
||||
3: '生产中',
|
||||
4: '已完成',
|
||||
5: '已关闭',
|
||||
6: '已取消',
|
||||
7: '已入库'
|
||||
}
|
||||
return map[status] || '未知'
|
||||
}
|
||||
|
||||
const getOrderStatusType = (status: number) => {
|
||||
const map: Record<number, string> = {
|
||||
0: 'info',
|
||||
1: 'warning',
|
||||
2: 'primary',
|
||||
3: 'warning',
|
||||
4: 'success',
|
||||
5: 'danger',
|
||||
6: 'info'
|
||||
}
|
||||
return map[status] || ''
|
||||
}
|
||||
|
||||
const getPlanTypeText = (type: number) => {
|
||||
const map: Record<number, string> = {
|
||||
1: '年度',
|
||||
2: '月度',
|
||||
3: '周度'
|
||||
}
|
||||
return map[type] || '未知'
|
||||
}
|
||||
|
||||
const getPlanTypeTagType = (type: number) => {
|
||||
const map: Record<number, string> = {
|
||||
1: 'danger',
|
||||
2: 'warning',
|
||||
3: 'info'
|
||||
}
|
||||
return map[type] || ''
|
||||
}
|
||||
|
||||
const getStatusText = (status: number) => {
|
||||
const map: Record<number, string> = {
|
||||
0: '草稿',
|
||||
1: '已发布',
|
||||
2: '执行中',
|
||||
3: '已完成',
|
||||
4: '已关闭'
|
||||
}
|
||||
return map[status] || '未知'
|
||||
}
|
||||
|
||||
const getStatusType = (status: number) => {
|
||||
const map: Record<number, string> = {
|
||||
0: 'info',
|
||||
1: 'primary',
|
||||
2: 'warning',
|
||||
3: 'success',
|
||||
4: 'danger'
|
||||
}
|
||||
return map[status] || ''
|
||||
}
|
||||
|
||||
const getProgressColor = (percentage: number) => {
|
||||
if (percentage < 30) return '#f56c6c'
|
||||
if (percentage < 70) return '#e6a23c'
|
||||
return '#67c23a'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (planId.value) {
|
||||
getDetail()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
150
src/views/mes/production/plan/index.vue
Normal file
150
src/views/mes/production/plan/index.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<doc-alert title="【MES】生产执行情况" url="https://doc.iocoder.cn/mes/plan/" />
|
||||
|
||||
<ContentWrap>
|
||||
<div
|
||||
class="production-board"
|
||||
:style="{
|
||||
backgroundImage: backgroundImage ? `url(${backgroundImage})` : 'none',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
minHeight: '600px',
|
||||
position: 'relative',
|
||||
border: backgroundImage ? 'none' : '2px dashed #dcdfe6',
|
||||
borderRadius: '4px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}"
|
||||
>
|
||||
<div v-if="!backgroundImage" class="upload-tip text-gray-400"> 请上传背景图片 </div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 底部上传按钮 -->
|
||||
<div class="upload-container">
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
accept="image/*"
|
||||
:on-change="handleFileChange"
|
||||
>
|
||||
<template #trigger>
|
||||
<el-button type="primary">
|
||||
<Icon icon="ep:upload" class="mr-5px" />
|
||||
{{ backgroundImage ? '更换背景图片' : '上传背景图片' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-upload>
|
||||
<el-button v-if="backgroundImage" type="danger" @click="removeBackground">
|
||||
<Icon icon="ep:delete" class="mr-5px" />
|
||||
移除背景图片
|
||||
</el-button>
|
||||
</div>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const backgroundImage = ref('')
|
||||
|
||||
// 从localStorage加载背景图片
|
||||
onMounted(() => {
|
||||
const savedBackground = localStorage.getItem('productionBackground')
|
||||
if (savedBackground) {
|
||||
backgroundImage.value = savedBackground
|
||||
}
|
||||
})
|
||||
|
||||
// 处理文件选择
|
||||
const handleFileChange = (file: any) => {
|
||||
const isImage = file.raw.type.startsWith('image/')
|
||||
const isLt2M = file.raw.size / 1024 / 1024 < 2
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件!')
|
||||
return
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error('图片大小不能超过 2MB!')
|
||||
return
|
||||
}
|
||||
|
||||
// 读取文件为 base64
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file.raw)
|
||||
reader.onload = (e) => {
|
||||
const base64 = e.target?.result as string
|
||||
backgroundImage.value = base64
|
||||
// 保存到localStorage
|
||||
localStorage.setItem('productionBackground', base64)
|
||||
ElMessage.success('背景图片设置成功')
|
||||
}
|
||||
}
|
||||
|
||||
// 移除背景图片
|
||||
const removeBackground = () => {
|
||||
backgroundImage.value = ''
|
||||
localStorage.removeItem('productionBackground')
|
||||
ElMessage.success('背景图片已移除')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.production-board {
|
||||
margin-bottom: 20px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.upload-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.production-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 8px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.data-card {
|
||||
background: white;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.data-card h3 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.data-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.data-item span {
|
||||
color: #606266;
|
||||
}
|
||||
</style>
|
||||
140
src/views/mes/production/plan/list/GenerateMonthlyPlanDialog.vue
Normal file
140
src/views/mes/production/plan/list/GenerateMonthlyPlanDialog.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<Dialog v-model="dialogVisible" title="生成月度计划" width="600px">
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||
<el-alert
|
||||
title="提示"
|
||||
type="info"
|
||||
:closable="false"
|
||||
class="mb-15px"
|
||||
>
|
||||
<template #default>
|
||||
<div>从年度计划生成月度计划,系统将根据所选月份和分配策略自动生成对应的月度计划。</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<el-form-item label="年度计划" prop="annualPlanId">
|
||||
<div class="w-full">
|
||||
<div class="font-bold">{{ annualPlan?.planCode }}</div>
|
||||
<div class="text-sm text-gray-500">{{ annualPlan?.planName }}</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="计划周期" prop="planPeriod">
|
||||
<el-tag type="danger">{{ annualPlan?.planPeriod }}</el-tag>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="选择月份" prop="months" required>
|
||||
<el-checkbox-group v-model="formData.months">
|
||||
<el-checkbox
|
||||
v-for="month in availableMonths"
|
||||
:key="month"
|
||||
:label="month"
|
||||
:disabled="existingMonths.includes(month)"
|
||||
>
|
||||
{{ month }}月
|
||||
<el-tag v-if="existingMonths.includes(month)" type="info" size="small" class="ml-5px">
|
||||
已存在
|
||||
</el-tag>
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="分配策略" prop="splitStrategy">
|
||||
<el-radio-group v-model="formData.splitStrategy">
|
||||
<el-radio label="average">平均分配</el-radio>
|
||||
<el-radio label="proportional">按比例分配</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="text-sm text-gray-500 mt-5px">
|
||||
<div v-if="formData.splitStrategy === 'average'">
|
||||
将年度计划数量平均分配到各月度计划
|
||||
</div>
|
||||
<div v-else>
|
||||
根据各月工作日比例分配计划数量
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确定生成
|
||||
</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { generateMonthlyPlans, type GenerateMonthlyPlanReqVO } from '@/api/mes/production/plan'
|
||||
|
||||
defineOptions({ name: 'GenerateMonthlyPlanDialog' })
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const formRef = ref()
|
||||
const annualPlan = ref<any>(null)
|
||||
const existingMonths = ref<number[]>([])
|
||||
|
||||
const formData = reactive<GenerateMonthlyPlanReqVO>({
|
||||
annualPlanId: 0,
|
||||
months: [],
|
||||
splitStrategy: 'average'
|
||||
})
|
||||
|
||||
const availableMonths = computed(() => {
|
||||
return Array.from({ length: 12 }, (_, i) => i + 1)
|
||||
})
|
||||
|
||||
const rules = {
|
||||
months: [{ required: true, message: '请选择要生成的月份', trigger: 'change' }],
|
||||
splitStrategy: [{ required: true, message: '请选择分配策略', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const open = (plan: any) => {
|
||||
annualPlan.value = plan
|
||||
formData.annualPlanId = plan.id
|
||||
formData.months = []
|
||||
formData.splitStrategy = 'average'
|
||||
|
||||
if (plan.children && plan.children.length > 0) {
|
||||
existingMonths.value = plan.children
|
||||
.filter((child: any) => child.planType === 2)
|
||||
.map((child: any) => {
|
||||
const match = child.planPeriod?.match(/-(\d{2})$/)
|
||||
return match ? parseInt(match[1]) : 0
|
||||
})
|
||||
.filter((month: number) => month > 0)
|
||||
} else {
|
||||
existingMonths.value = []
|
||||
}
|
||||
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
|
||||
if (formData.months.length === 0) {
|
||||
ElMessage.warning('请至少选择一个月份')
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
await generateMonthlyPlans(formData)
|
||||
ElMessage.success('月度计划生成成功')
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error('生成月度计划失败:', error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
173
src/views/mes/production/plan/list/GenerateOrderDialog.vue
Normal file
173
src/views/mes/production/plan/list/GenerateOrderDialog.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
title="从计划生成工单"
|
||||
v-model="visible"
|
||||
width="800px"
|
||||
@close="handleClose"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px" v-loading="formLoading">
|
||||
<el-alert
|
||||
title="提示"
|
||||
type="info"
|
||||
:closable="false"
|
||||
class="mb-20px"
|
||||
>
|
||||
<template #default>
|
||||
<div>计划名称:{{ planInfo.planName }}</div>
|
||||
<div>计划周期:{{ planInfo.planPeriod }}</div>
|
||||
<div>总计划数量:{{ planInfo.totalPlanQuantity }}</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<el-form-item label="选择产品" prop="itemIds">
|
||||
<el-table
|
||||
:data="planItems"
|
||||
@selection-change="handleSelectionChange"
|
||||
border
|
||||
max-height="300"
|
||||
>
|
||||
<el-table-column type="selection" width="50" />
|
||||
<el-table-column label="产品名称" prop="productName" min-width="150" />
|
||||
<el-table-column label="计划数量" prop="planQuantity" width="100" align="center" />
|
||||
<el-table-column label="已完成" prop="completedQuantity" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="text-green-600">{{ row.completedQuantity || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="进行中" prop="inProgressQuantity" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="text-orange-600">{{ row.inProgressQuantity || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="完成率" prop="completionRate" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-progress
|
||||
:percentage="row.completionRate || 0"
|
||||
:format="() => `${row.completionRate || 0}%`"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="拆分策略" prop="splitStrategy">
|
||||
<el-radio-group v-model="form.splitStrategy">
|
||||
<el-radio label="single">单个工单(不拆分)</el-radio>
|
||||
<el-radio label="weekly">按周拆分</el-radio>
|
||||
<el-radio label="daily">按天拆分</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="text-sm text-gray-500 mt-5px">
|
||||
<div v-if="form.splitStrategy === 'single'">将整个计划明细生成为一个工单</div>
|
||||
<div v-if="form.splitStrategy === 'weekly'">按周拆分,每周生成一个工单</div>
|
||||
<div v-if="form.splitStrategy === 'daily'">按天拆分,每天生成一个工单</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="自动审核" prop="autoApprove">
|
||||
<el-switch v-model="form.autoApprove" />
|
||||
<span class="text-sm text-gray-500 ml-10px">
|
||||
开启后,生成的工单将自动审核通过
|
||||
</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="submitForm">
|
||||
生成工单
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { generateWorkOrders, getProductionPlan } from '@/api/mes/production/plan'
|
||||
|
||||
const emits = defineEmits(['success'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const formLoading = ref(false)
|
||||
const formRef = ref()
|
||||
|
||||
const planInfo = ref<any>({})
|
||||
const planItems = ref<any[]>([])
|
||||
const selectedItems = ref<any[]>([])
|
||||
|
||||
const form = reactive<any>({
|
||||
planId: undefined,
|
||||
itemIds: [],
|
||||
splitStrategy: 'daily',
|
||||
batchSize: undefined,
|
||||
autoApprove: false
|
||||
})
|
||||
|
||||
const rules = {
|
||||
itemIds: [{ required: true, message: '请选择要生成工单的产品', trigger: 'change' }],
|
||||
splitStrategy: [{ required: true, message: '请选择拆分策略', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const open = async (plan: any) => {
|
||||
visible.value = true
|
||||
formLoading.value = true
|
||||
|
||||
try {
|
||||
// 重置表单
|
||||
Object.assign(form, {
|
||||
planId: plan.id,
|
||||
itemIds: [],
|
||||
splitStrategy: 'daily',
|
||||
batchSize: undefined,
|
||||
autoApprove: false
|
||||
})
|
||||
|
||||
// 获取计划详情
|
||||
const res = await getProductionPlan(plan.id)
|
||||
planInfo.value = res
|
||||
planItems.value = res.items || []
|
||||
} catch (error) {
|
||||
console.error('加载计划详情失败:', error)
|
||||
ElMessage.error('加载数据失败,请重试')
|
||||
visible.value = false
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedItems.value = selection
|
||||
form.itemIds = selection.map((item) => item.id)
|
||||
}
|
||||
|
||||
const submitForm = () => {
|
||||
if (!form.itemIds || form.itemIds.length === 0) {
|
||||
ElMessage.warning('请至少选择一个产品')
|
||||
return
|
||||
}
|
||||
|
||||
formRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await generateWorkOrders(form)
|
||||
ElMessage.success(`成功生成 ${res.length} 个工单`)
|
||||
visible.value = false
|
||||
emits('success')
|
||||
} catch (error) {
|
||||
console.error('生成工单失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
formRef.value?.resetFields()
|
||||
selectedItems.value = []
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
356
src/views/mes/production/plan/list/PlanAnalysisDialog.vue
Normal file
356
src/views/mes/production/plan/list/PlanAnalysisDialog.vue
Normal file
@@ -0,0 +1,356 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
title="生产计划分析"
|
||||
v-model="visible"
|
||||
width="1000px"
|
||||
@close="handleClose"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div v-loading="loading">
|
||||
<!-- 基本信息卡片 -->
|
||||
<el-card shadow="never" class="mb-15px">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-bold">计划概览</span>
|
||||
<el-tag type="primary">{{ analysisData?.planCode }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="4">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">计划名称</div>
|
||||
<div class="stat-value text-blue-600">{{ analysisData?.planName }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">计划总数量</div>
|
||||
<div class="stat-value text-gray-600">{{ analysisData?.totalPlanQuantity }} 件</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">已完成数量</div>
|
||||
<div class="stat-value text-green-600">{{ analysisData?.totalCompletedQuantity }} 件</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">剩余数量</div>
|
||||
<div class="stat-value text-orange-600">{{ analysisData?.totalRemainingQuantity }} 件</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">预计生产周期</div>
|
||||
<div class="stat-value text-blue-500">{{ analysisData?.estimatedProductionHours }} 小时</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">预计交货日期</div>
|
||||
<div class="stat-value text-purple-600">{{ analysisData?.estimatedDeliveryDate }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-alert type="info" :closable="false" class="mt-10px">
|
||||
<template #title>
|
||||
<span>※ 以下物料需求分析基于<strong>剩余未完成数量</strong>计算</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
</el-card>
|
||||
|
||||
<!-- 风险提示 -->
|
||||
<el-alert
|
||||
v-if="analysisData?.riskWarnings && analysisData.riskWarnings.length > 0"
|
||||
title="风险提示"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
class="mb-15px"
|
||||
>
|
||||
<template #default>
|
||||
<div v-for="(warning, index) in analysisData.riskWarnings" :key="index" class="mb-5px">
|
||||
⚠ {{ warning }}
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<!-- 采购建议 -->
|
||||
<el-card shadow="never" class="mb-15px">
|
||||
<template #header>
|
||||
<span class="font-bold">采购建议</span>
|
||||
</template>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="建议采购提前天数">
|
||||
<el-tag type="warning">{{ analysisData?.suggestedPurchaseLeadDays }} 天</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="建议采购日期">
|
||||
<el-tag type="danger">{{ analysisData?.suggestedPurchaseDate }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 辅料需求汇总 -->
|
||||
<el-card shadow="never" class="mb-15px">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-bold">辅料需求汇总</span>
|
||||
<el-tag type="info">共 {{ analysisData?.materialRequirements?.length || 0 }} 种物料</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="analysisData?.materialRequirements" border max-height="300">
|
||||
<el-table-column label="物料编码" prop="materialCode" width="100" align="center" />
|
||||
<el-table-column label="物料名称" prop="materialName" min-width="120" align="center" />
|
||||
<el-table-column label="类型" prop="materialType" width="90" align="center" />
|
||||
<el-table-column label="需求数量" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.requiredQuantity }} {{ row.unit }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="当前库存" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.currentStock }} {{ row.unit }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存缺口" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="getGapClass(row.stockGap)">
|
||||
{{ row.stockGap > 0 ? '+' : '' }}{{ row.stockGap }} {{ row.unit }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStockStatusType(row.stockStatus)" size="small">
|
||||
{{ getStockStatusText(row.stockStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="建议采购" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="text-orange-600 font-bold" v-if="row.suggestedPurchaseQuantity > 0">
|
||||
{{ row.suggestedPurchaseQuantity }} {{ row.unit }}
|
||||
</span>
|
||||
<span v-else class="text-gray-400">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="采购提前期" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.purchaseLeadTime }} 天
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预计金额" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.estimatedPurchaseAmount > 0" class="text-red-600">
|
||||
¥{{ row.estimatedPurchaseAmount }}
|
||||
</span>
|
||||
<span v-else class="text-gray-400">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="mt-10px text-right" v-if="totalPurchaseAmount > 0">
|
||||
<span class="text-gray-500">预计采购总金额:</span>
|
||||
<span class="text-red-600 font-bold text-lg">¥{{ totalPurchaseAmount.toFixed(2) }}</span>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 产品明细分析 -->
|
||||
<el-card shadow="never" class="mb-15px">
|
||||
<template #header>
|
||||
<span class="font-bold">产品明细分析</span>
|
||||
</template>
|
||||
<el-collapse v-model="activeProducts">
|
||||
<el-collapse-item
|
||||
v-for="(product, index) in analysisData?.productAnalysisList"
|
||||
:key="index"
|
||||
:name="index"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex items-center gap-10px">
|
||||
<span class="font-bold">{{ product.productName }}</span>
|
||||
<el-tag size="small">{{ product.planQuantity }} {{ product.unit || '件' }}</el-tag>
|
||||
<el-tag size="small" type="success">已完成 {{ product.completedQuantity }}</el-tag>
|
||||
<el-tag size="small" type="warning">剩余 {{ product.remainingQuantity }}</el-tag>
|
||||
<el-tag size="small" type="info">{{ product.estimatedHours }} 小时</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="pl-20px">
|
||||
<el-descriptions :column="3" border size="small" class="mb-10px">
|
||||
<el-descriptions-item label="产品编码">{{ product.productCode }}</el-descriptions-item>
|
||||
<el-descriptions-item label="工序路线">{{ product.routeName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预计周期">{{ product.estimatedHours }} 小时</el-descriptions-item>
|
||||
<el-descriptions-item label="计划开始">{{ product.plannedStartDate || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="计划结束">{{ product.plannedEndDate || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="text-sm text-gray-500 mb-5px">该产品辅料需求:</div>
|
||||
<el-table :data="product.materialRequirements" border size="small">
|
||||
<el-table-column label="物料名称" prop="materialName" min-width="100" />
|
||||
<el-table-column label="需求数量" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.requiredQuantity }} {{ row.unit }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStockStatusType(row.stockStatus)" size="small">
|
||||
{{ getStockStatusText(row.stockStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
|
||||
<!-- 描述性分析文本 -->
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-bold">分析报告</span>
|
||||
<el-button type="primary" size="small" @click="copyAnalysisText">
|
||||
<Icon icon="ep:document-copy" class="mr-5px" /> 复制报告
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<pre class="analysis-text">{{ analysisData?.analysisDescription }}</pre>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">关闭</el-button>
|
||||
<el-button type="primary" @click="handleExport">
|
||||
<Icon icon="ep:download" class="mr-5px" /> 导出报告
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import { analyzeProductionPlan, type ProductionPlanAnalysisVO } from '@/api/mes/production/plan'
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const analysisData = ref<ProductionPlanAnalysisVO | null>(null)
|
||||
const activeProducts = ref<number[]>([0])
|
||||
|
||||
const totalPurchaseAmount = computed(() => {
|
||||
if (!analysisData.value?.materialRequirements) return 0
|
||||
return analysisData.value.materialRequirements.reduce((sum, item) => {
|
||||
return sum + (item.estimatedPurchaseAmount || 0)
|
||||
}, 0)
|
||||
})
|
||||
|
||||
const open = async (planId: number) => {
|
||||
visible.value = true
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
analysisData.value = await analyzeProductionPlan(planId)
|
||||
} catch (error) {
|
||||
console.error('获取分析数据失败:', error)
|
||||
ElMessage.error('获取分析数据失败')
|
||||
visible.value = false
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
analysisData.value = null
|
||||
activeProducts.value = [0]
|
||||
}
|
||||
|
||||
const getStockStatusType = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
sufficient: 'success',
|
||||
warning: 'warning',
|
||||
shortage: 'danger'
|
||||
}
|
||||
return map[status] || 'info'
|
||||
}
|
||||
|
||||
const getStockStatusText = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
sufficient: '充足',
|
||||
warning: '预警',
|
||||
shortage: '短缺'
|
||||
}
|
||||
return map[status] || '未知'
|
||||
}
|
||||
|
||||
const getGapClass = (gap: number) => {
|
||||
if (gap <= 0) return 'text-green-600'
|
||||
return 'text-red-600'
|
||||
}
|
||||
|
||||
const copyAnalysisText = async () => {
|
||||
if (!analysisData.value?.analysisDescription) return
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(analysisData.value.analysisDescription)
|
||||
ElMessage.success('分析报告已复制到剪贴板')
|
||||
} catch (error) {
|
||||
ElMessage.error('复制失败,请手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
const handleExport = () => {
|
||||
if (!analysisData.value?.analysisDescription) return
|
||||
|
||||
const blob = new Blob([analysisData.value.analysisDescription], { type: 'text/plain;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `生产计划分析报告_${analysisData.value.planCode}_${new Date().toISOString().slice(0, 10)}.txt`
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
ElMessage.success('报告导出成功')
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.analysis-text {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: #303133;
|
||||
background: #f5f7fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin: 0;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item__header) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item__content) {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
</style>
|
||||
407
src/views/mes/production/plan/list/ProductionPlanForm.vue
Normal file
407
src/views/mes/production/plan/list/ProductionPlanForm.vue
Normal file
@@ -0,0 +1,407 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:title="formType === 'create' ? '新增生产计划' : '编辑生产计划'"
|
||||
v-model="visible"
|
||||
width="1200px"
|
||||
@close="handleClose"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px" v-loading="formLoading">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="计划编号" prop="planCode">
|
||||
<el-input v-model="form.planCode" placeholder="请输入计划编号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="计划名称" prop="planName">
|
||||
<el-input v-model="form.planName" placeholder="请输入计划名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="计划类型" prop="planType">
|
||||
<el-select v-model="form.planType" placeholder="请选择计划类型" class="!w-full">
|
||||
<el-option label="年度计划" :value="1" />
|
||||
<el-option label="月度计划" :value="2" />
|
||||
<el-option label="周度计划" :value="3" />
|
||||
<el-option label="日计划" :value="4" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="计划周期" prop="planPeriod">
|
||||
<el-input v-model="form.planPeriod" placeholder="如:2024-01" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="父计划" prop="parentPlanId">
|
||||
<el-select
|
||||
v-model="form.parentPlanId"
|
||||
placeholder="请选择父计划"
|
||||
clearable
|
||||
filterable
|
||||
class="!w-full"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in filteredParentPlans"
|
||||
:key="item.id"
|
||||
:label="`${item.planName} (${item.planCode})`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="开始日期" prop="startDate">
|
||||
<el-date-picker
|
||||
v-model="form.startDate"
|
||||
type="date"
|
||||
placeholder="选择开始日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
class="!w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="结束日期" prop="endDate">
|
||||
<el-date-picker
|
||||
v-model="form.endDate"
|
||||
type="date"
|
||||
placeholder="选择结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
class="!w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 计划明细 -->
|
||||
<el-divider content-position="left">计划明细</el-divider>
|
||||
<el-button type="primary" @click="handleAddItem" class="mb-10px">
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 添加产品
|
||||
</el-button>
|
||||
<el-table :data="form.items" border>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column label="产品" prop="productName" min-width="150" align="center">
|
||||
<template #default="{ row, $index }">
|
||||
<el-select
|
||||
v-model="row.productId"
|
||||
placeholder="请选择产品"
|
||||
filterable
|
||||
@change="handleProductChange(row, $index)"
|
||||
class="!w-full"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in productOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
>
|
||||
<span>{{ item.name }}</span>
|
||||
<span class="text-gray-400 ml-10px">({{ item.barCode }})</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="计划数量" prop="planQuantity" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.planQuantity"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
class="!w-full"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="工序路线" prop="routeName" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.routeId"
|
||||
placeholder="请选择工序路线"
|
||||
filterable
|
||||
@change="handleRouteChange(row)"
|
||||
class="!w-full"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in routeOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="优先级" prop="priority" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.priority"
|
||||
:min="1"
|
||||
:max="10"
|
||||
controls-position="right"
|
||||
class="!w-full"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="计划开始日期" prop="plannedStartDate" width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-date-picker
|
||||
v-model="row.plannedStartDate"
|
||||
type="date"
|
||||
placeholder="开始日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
class="!w-full"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="计划结束日期" prop="plannedEndDate" width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-date-picker
|
||||
v-model="row.plannedEndDate"
|
||||
type="date"
|
||||
placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
class="!w-full"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-button link type="danger" @click="handleDeleteItem($index)">
|
||||
<Icon icon="ep:delete" />
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="submitForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import {
|
||||
createProductionPlan,
|
||||
updateProductionPlan,
|
||||
getProductionPlan,
|
||||
getProductionPlanPage
|
||||
} from '@/api/mes/production/plan'
|
||||
import { ProductApi } from '@/api/erp/product/product'
|
||||
import { YVHgetProcessRouteList } from '@/api/mes/production/process-route'
|
||||
|
||||
const emits = defineEmits(['success'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const formLoading = ref(false)
|
||||
const formType = ref<'create' | 'update'>('create')
|
||||
const formRef = ref()
|
||||
|
||||
const productOptions = ref<any[]>([])
|
||||
const routeOptions = ref<any[]>([])
|
||||
const parentPlanOptions = ref<any[]>([])
|
||||
|
||||
const form = reactive<any>({
|
||||
id: undefined,
|
||||
planCode: '',
|
||||
planName: '',
|
||||
planType: 2,
|
||||
planPeriod: '',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
parentPlanId: undefined,
|
||||
remark: '',
|
||||
items: []
|
||||
})
|
||||
|
||||
// 根据计划类型过滤可选的父计划
|
||||
// 层级关系: 年度计划(1) > 月度计划(2) > 周度计划(3) > 日计划(4)
|
||||
const filteredParentPlans = computed(() => {
|
||||
if (!form.planType) return []
|
||||
|
||||
// 年度计划没有父计划
|
||||
if (form.planType === 1) {
|
||||
return []
|
||||
}
|
||||
|
||||
// 月度计划只能选择年度计划作为父计划
|
||||
if (form.planType === 2) {
|
||||
return parentPlanOptions.value.filter(plan => plan.planType === 1)
|
||||
}
|
||||
|
||||
// 周度计划只能选择月度计划作为父计划
|
||||
if (form.planType === 3) {
|
||||
return parentPlanOptions.value.filter(plan => plan.planType === 2)
|
||||
}
|
||||
|
||||
// 日计划只能选择周度计划作为父计划
|
||||
if (form.planType === 4) {
|
||||
return parentPlanOptions.value.filter(plan => plan.planType === 3)
|
||||
}
|
||||
|
||||
return []
|
||||
})
|
||||
|
||||
const rules = {
|
||||
planCode: [{ required: true, message: '请输入计划编号', trigger: 'blur' }],
|
||||
planName: [{ required: true, message: '请输入计划名称', trigger: 'blur' }],
|
||||
planType: [{ required: true, message: '请选择计划类型', trigger: 'change' }],
|
||||
planPeriod: [{ required: true, message: '请输入计划周期', trigger: 'blur' }],
|
||||
startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }],
|
||||
endDate: [{ required: true, message: '请选择结束日期', trigger: 'change' }]
|
||||
}
|
||||
|
||||
// 监听计划类型变化,自动清除不符合条件的父计划
|
||||
watch(() => form.planType, () => {
|
||||
// 如果当前选择的父计划不在过滤后的列表中,则清空
|
||||
if (form.parentPlanId && !filteredParentPlans.value.find(p => p.id === form.parentPlanId)) {
|
||||
form.parentPlanId = undefined
|
||||
}
|
||||
})
|
||||
|
||||
const open = async (type: 'create' | 'update', id?: number) => {
|
||||
formType.value = type
|
||||
visible.value = true
|
||||
formLoading.value = true
|
||||
|
||||
try {
|
||||
// 重置表单
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
planCode: type === 'create' ? generatePlanCode() : '',
|
||||
planName: '',
|
||||
planType: 2,
|
||||
planPeriod: getCurrentPeriod(),
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
parentPlanId: undefined,
|
||||
remark: '',
|
||||
items: []
|
||||
})
|
||||
|
||||
// 并行获取产品列表、工序路线列表和父计划列表
|
||||
const [products, routes, plans] = await Promise.all([
|
||||
ProductApi.getProductSimpleList().catch(() => []),
|
||||
YVHgetProcessRouteList().catch(() => []),
|
||||
getProductionPlanPage({ pageNo: 1, pageSize: 100 }).then(res => res.list || []).catch(() => [])
|
||||
])
|
||||
|
||||
productOptions.value = products ? products.filter((p) => p.categoryName?.includes('成品')) : []
|
||||
routeOptions.value = routes || []
|
||||
// 过滤掉当前编辑的计划,避免循环引用
|
||||
parentPlanOptions.value = plans.filter(p => p.id !== id)
|
||||
|
||||
// 如果是编辑模式,获取计划详情
|
||||
if (type === 'update' && id) {
|
||||
const res = await getProductionPlan(id)
|
||||
Object.assign(form, res)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('初始化表单失败:', error)
|
||||
ElMessage.error('加载数据失败,请重试')
|
||||
visible.value = false
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddItem = () => {
|
||||
form.items.push({
|
||||
productId: undefined,
|
||||
productCode: '',
|
||||
productName: '',
|
||||
planQuantity: 1,
|
||||
routeId: undefined,
|
||||
routeName: '',
|
||||
priority: 5,
|
||||
plannedStartDate: form.startDate,
|
||||
plannedEndDate: form.endDate,
|
||||
remark: ''
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteItem = (index: number) => {
|
||||
form.items.splice(index, 1)
|
||||
}
|
||||
|
||||
const handleProductChange = (row: any, index: number) => {
|
||||
const product = productOptions.value.find((item) => item.id === row.productId)
|
||||
if (product) {
|
||||
row.productCode = product.barCode
|
||||
row.productName = product.name
|
||||
|
||||
// 自动匹配工序路线
|
||||
const route = routeOptions.value.find((r) => r.name === product.name || r.name.includes(product.name))
|
||||
if (route) {
|
||||
row.routeId = route.id
|
||||
row.routeName = route.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleRouteChange = (row: any) => {
|
||||
const route = routeOptions.value.find((item) => item.id === row.routeId)
|
||||
if (route) {
|
||||
row.routeName = route.name
|
||||
}
|
||||
}
|
||||
|
||||
const submitForm = () => {
|
||||
if (!form.items || form.items.length === 0) {
|
||||
ElMessage.warning('请至少添加一个产品')
|
||||
return
|
||||
}
|
||||
|
||||
formRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
loading.value = true
|
||||
try {
|
||||
if (formType.value === 'create') {
|
||||
await createProductionPlan(form)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await updateProductionPlan(form)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
visible.value = false
|
||||
emits('success')
|
||||
} catch (error) {
|
||||
console.error('保存计划失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const generatePlanCode = () => {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
return `PLAN${year}${month}001`
|
||||
}
|
||||
|
||||
const getCurrentPeriod = () => {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
return `${year}-${month}`
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
531
src/views/mes/production/plan/list/index.vue
Normal file
531
src/views/mes/production/plan/list/index.vue
Normal file
@@ -0,0 +1,531 @@
|
||||
<template>
|
||||
<doc-alert title="【MES】生产计划管理" url="https://doc.iocoder.cn/mes/production-plan/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="84px"
|
||||
>
|
||||
<el-form-item label="计划编号" prop="planCode">
|
||||
<el-input
|
||||
v-model="queryParams.planCode"
|
||||
placeholder="请输入计划编号"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="计划名称" prop="planName">
|
||||
<el-input
|
||||
v-model="queryParams.planName"
|
||||
placeholder="请输入计划名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="计划类型" prop="planType">
|
||||
<el-select
|
||||
v-model="queryParams.planType"
|
||||
placeholder="请选择计划类型"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option label="年度计划" :value="1" />
|
||||
<el-option label="月度计划" :value="2" />
|
||||
<el-option label="周度计划" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="计划周期" prop="planPeriod">
|
||||
<el-input
|
||||
v-model="queryParams.planPeriod"
|
||||
placeholder="如:2024-01"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="计划状态" prop="status">
|
||||
<el-select
|
||||
v-model="queryParams.status"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
multiple
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option label="草稿" :value="0" />
|
||||
<el-option label="已发布" :value="1" />
|
||||
<el-option label="执行中" :value="2" />
|
||||
<el-option label="已完成" :value="3" />
|
||||
<el-option label="已关闭" :value="4" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="openForm('create')"
|
||||
v-hasPermi="['mes:production-plan:create']"
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增计划
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="treeList"
|
||||
:stripe="true"
|
||||
:show-overflow-tooltip="true"
|
||||
row-key="id"
|
||||
:tree-props="{ children: 'children' }"
|
||||
:default-expand-all="true"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="50" />
|
||||
<el-table-column label="计划信息" align="left" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div class="font-bold">{{ row.planCode }}</div>
|
||||
<div class="mt-5px text-gray-500">{{ row.planName }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="计划类型" align="center" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlanTypeTagType(row.planType)">
|
||||
{{ getPlanTypeText(row.planType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="计划周期" align="center" prop="planPeriod" width="120" />
|
||||
<el-table-column label="计划时间" align="center" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div>开始:{{ row.startDate }}</div>
|
||||
<div class="mt-5px">结束:{{ row.endDate }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="计划数量" align="center" width="120">
|
||||
<template #default="{ row }">
|
||||
<div class="font-bold text-blue-600">{{ row.totalPlanQuantity || 0 }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="完成情况" align="center" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="mb-5px">
|
||||
<span class="text-sm text-gray-500">完成:</span>
|
||||
<span class="font-bold text-green-600">{{ row.totalCompletedQuantity || 0 }}</span>
|
||||
<span class="text-sm text-gray-500 ml-10px">进行中:</span>
|
||||
<span class="font-bold text-orange-600">{{ row.totalInProgressQuantity || 0 }}</span>
|
||||
</div>
|
||||
<el-progress
|
||||
:percentage="row.overallCompletionRate || 0"
|
||||
:color="getProgressColor(row.overallCompletionRate)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)">
|
||||
{{ getStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" fixed="right" width="480">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" @click="openDetail(scope.row.id)">
|
||||
<Icon icon="ep:view" class="mr-5px" /> 详情
|
||||
</el-button>
|
||||
<el-button link type="success" @click="handleAnalyze(scope.row.id)">
|
||||
<Icon icon="ep:data-analysis" class="mr-5px" /> 分析
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
v-hasPermi="['mes:production-plan:update']"
|
||||
:disabled="scope.row.status === 0 || scope.row.hasApprovalRecords"
|
||||
>
|
||||
<Icon icon="ep:edit" class="mr-5px" /> 编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="success"
|
||||
@click="handlePublish(scope.row.id)"
|
||||
v-if="scope.row.status === 0"
|
||||
v-hasPermi="['mes:production-plan:publish']"
|
||||
>
|
||||
<Icon icon="ep:upload" class="mr-5px" /> 发布
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
link
|
||||
type="success"
|
||||
@click="handleSubmitApproval(scope.row.id)"
|
||||
v-hasPermi="['mes:production-plan:submit-approval']"
|
||||
v-if="scope.row.status === 0 && !scope.row.hasApprovalRecords"
|
||||
>
|
||||
提交审批
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="info"
|
||||
@click="handleViewApproval(scope.row.id)"
|
||||
v-hasPermi="['mes:production-plan:query-approval']"
|
||||
v-if="scope.row.hasApprovalRecords"
|
||||
>
|
||||
审批记录
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleProcessApproval(scope.row.id)"
|
||||
v-hasPermi="['mes:approval-record:process']"
|
||||
v-if="scope.row.hasApprovalRecords && scope.row.status !== 1"
|
||||
>
|
||||
处理审批
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleGenerateMonthlyPlans(scope.row)"
|
||||
v-if="scope.row.planType === 1 && scope.row.status >= 1"
|
||||
v-hasPermi="['mes:production-plan:generate-monthly-plans']"
|
||||
>
|
||||
<Icon icon="ep:calendar" class="mr-5px" /> 生成月度计划
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="warning"
|
||||
@click="handleGenerateOrders(scope.row)"
|
||||
v-if="scope.row.status >= 1 && scope.row.status <= 2 && !scope.row.hasChildPlans && !scope.row.hasGeneratedOrders"
|
||||
v-hasPermi="['mes:production-plan:generate-orders']"
|
||||
>
|
||||
<Icon icon="ep:document-add" class="mr-5px" /> 生成工单
|
||||
</el-button>
|
||||
<el-tooltip
|
||||
content="该计划有下级计划,请在下级计划中生成工单"
|
||||
placement="top"
|
||||
v-if="scope.row.status >= 1 && scope.row.status <= 2 && scope.row.hasChildPlans"
|
||||
>
|
||||
<el-button
|
||||
link
|
||||
type="info"
|
||||
disabled
|
||||
>
|
||||
<Icon icon="ep:document-add" class="mr-5px" /> 生成工单
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-button
|
||||
link
|
||||
type="info"
|
||||
@click="handleUpdateProgress(scope.row.id)"
|
||||
v-if="scope.row.status === 2"
|
||||
v-hasPermi="['mes:production-plan:update-progress']"
|
||||
>
|
||||
<Icon icon="ep:refresh" class="mr-5px" /> 更新进度
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleClose(scope.row.id)"
|
||||
v-if="scope.row.status === 2 || scope.row.status === 3"
|
||||
v-hasPermi="['mes:production-plan:close']"
|
||||
>
|
||||
<Icon icon="ep:close" class="mr-5px" /> 关闭
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-if="scope.row.status === 0"
|
||||
v-hasPermi="['mes:production-plan:delete']"
|
||||
:disabled="scope.row.hasApprovalRecords"
|
||||
>
|
||||
<Icon icon="ep:delete" class="mr-5px" /> 删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<ProductionPlanForm ref="formRef" @success="getList" />
|
||||
|
||||
<!-- 生成工单弹窗 -->
|
||||
<GenerateOrderDialog ref="generateOrderRef" @success="getList" />
|
||||
|
||||
<!-- 生成月度计划弹窗 -->
|
||||
<GenerateMonthlyPlanDialog ref="generateMonthlyPlanRef" @success="getList" />
|
||||
|
||||
<!-- 计划分析弹窗 -->
|
||||
<PlanAnalysisDialog ref="planAnalysisRef" />
|
||||
|
||||
<!-- 提交审批对话框 -->
|
||||
<SubmitApprovalDialog ref="submitApprovalDialogRef" @success="getList" />
|
||||
|
||||
<!-- 审批记录对话框 -->
|
||||
<ApprovalRecordsDialog ref="approvalDialogRef" />
|
||||
|
||||
<!-- 处理审批对话框 -->
|
||||
<ProcessApprovalDialog ref="processApprovalDialogRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import ProductionPlanForm from './ProductionPlanForm.vue'
|
||||
import GenerateOrderDialog from './GenerateOrderDialog.vue'
|
||||
import GenerateMonthlyPlanDialog from './GenerateMonthlyPlanDialog.vue'
|
||||
import PlanAnalysisDialog from './PlanAnalysisDialog.vue'
|
||||
import {
|
||||
getProductionPlanPage,
|
||||
deleteProductionPlan,
|
||||
publishProductionPlan,
|
||||
closeProductionPlan,
|
||||
updatePlanProgress
|
||||
} from '@/api/mes/production/plan'
|
||||
import { SubmitApprovalDialog, ApprovalRecordsDialog, ProcessApprovalDialog } from '@/components/Approval'
|
||||
import { ApprovalRecordApi, ApprovalRecordVO } from '@/api/erp/approval/index'
|
||||
|
||||
|
||||
defineOptions({ name: 'MesProductionPlanList' })
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(true)
|
||||
const total = ref(0)
|
||||
const list = ref<any[]>([])
|
||||
const treeList = ref<any[]>([])
|
||||
const queryFormRef = ref()
|
||||
const formRef = ref()
|
||||
const generateOrderRef = ref()
|
||||
const generateMonthlyPlanRef = ref()
|
||||
const planAnalysisRef = ref()
|
||||
const selectionList = ref<any[]>([])
|
||||
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
planCode: undefined,
|
||||
planName: undefined,
|
||||
planType: undefined,
|
||||
planPeriod: undefined,
|
||||
status: [] as number[]
|
||||
})
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getProductionPlanPage(queryParams)
|
||||
|
||||
// 递归处理树形结构,为每个节点(包括子节点)检查审批记录
|
||||
const processTreeNode = async (item: any): Promise<any> => {
|
||||
try {
|
||||
const records = await ApprovalRecordApi.getApprovalRecordListByBiz(item.id, 'mes_production_plan')
|
||||
const processedItem = {
|
||||
...item,
|
||||
hasApprovalRecords: records && records.length > 0
|
||||
}
|
||||
|
||||
// 如果有子节点,递归处理
|
||||
if (item.children && item.children.length > 0) {
|
||||
processedItem.children = await Promise.all(
|
||||
item.children.map(child => processTreeNode(child))
|
||||
)
|
||||
}
|
||||
|
||||
return processedItem
|
||||
} catch (error) {
|
||||
console.error('获取审批记录失败', error)
|
||||
const processedItem = {
|
||||
...item,
|
||||
hasApprovalRecords: false
|
||||
}
|
||||
|
||||
// 如果有子节点,递归处理
|
||||
if (item.children && item.children.length > 0) {
|
||||
processedItem.children = await Promise.all(
|
||||
item.children.map(child => processTreeNode(child))
|
||||
)
|
||||
}
|
||||
|
||||
return processedItem
|
||||
}
|
||||
}
|
||||
|
||||
// 处理整个树形结构
|
||||
const listWithApprovalStatus = await Promise.all(
|
||||
res.list.map(item => processTreeNode(item))
|
||||
)
|
||||
|
||||
list.value = listWithApprovalStatus
|
||||
total.value = res.total
|
||||
// 使用处理后的数据作为树形结构
|
||||
treeList.value = listWithApprovalStatus
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields()
|
||||
queryParams.planCode = undefined
|
||||
queryParams.planName = undefined
|
||||
queryParams.planType = undefined
|
||||
queryParams.planPeriod = undefined
|
||||
queryParams.status = []
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 提交审批 */
|
||||
const submitApprovalDialogRef = ref()
|
||||
const handleSubmitApproval = (id: number) => {
|
||||
submitApprovalDialogRef.value.open(String(id), 'mes_production_plan')
|
||||
}
|
||||
|
||||
/** 查看审批记录 */
|
||||
const approvalDialogRef = ref()
|
||||
const handleViewApproval = (id: number) => {
|
||||
approvalDialogRef.value.open(String(id), 'mes_production_plan')
|
||||
}
|
||||
|
||||
/** 处理审批 */
|
||||
const processApprovalDialogRef = ref()
|
||||
const handleProcessApproval = (id: number) => {
|
||||
processApprovalDialogRef.value.open(String(id), 'mes_production_plan')
|
||||
}
|
||||
|
||||
const openForm = (type: 'create' | 'update', id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
const openDetail = (id: number) => {
|
||||
// 跳转到详情页面
|
||||
router.push({
|
||||
path: '/mes/production/plan/detail',
|
||||
query: { id: id.toString() }
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该计划吗?', '提示', { type: 'warning' })
|
||||
await deleteProductionPlan(id)
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handlePublish = async (id: number) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要发布该计划吗?发布后将不能修改。', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
await publishProductionPlan(id)
|
||||
ElMessage.success('发布成功')
|
||||
getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleClose = async (id: number) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要关闭该计划吗?', '提示', { type: 'warning' })
|
||||
await closeProductionPlan(id)
|
||||
ElMessage.success('关闭成功')
|
||||
getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleGenerateOrders = (row: any) => {
|
||||
generateOrderRef.value.open(row)
|
||||
}
|
||||
|
||||
const handleGenerateMonthlyPlans = (row: any) => {
|
||||
generateMonthlyPlanRef.value.open(row)
|
||||
}
|
||||
|
||||
const handleAnalyze = (id: number) => {
|
||||
planAnalysisRef.value.open(id)
|
||||
}
|
||||
|
||||
const handleUpdateProgress = async (id: number) => {
|
||||
try {
|
||||
await updatePlanProgress(id)
|
||||
ElMessage.success('进度更新成功')
|
||||
getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectionList.value = selection
|
||||
}
|
||||
|
||||
const getPlanTypeText = (type: number) => {
|
||||
const map: Record<number, string> = {
|
||||
1: '年度',
|
||||
2: '月度',
|
||||
3: '周度'
|
||||
}
|
||||
return map[type] || '未知'
|
||||
}
|
||||
|
||||
const getPlanTypeTagType = (type: number) => {
|
||||
const map: Record<number, string> = {
|
||||
1: 'danger',
|
||||
2: 'warning',
|
||||
3: 'info'
|
||||
}
|
||||
return map[type] || ''
|
||||
}
|
||||
|
||||
const getStatusText = (status: number) => {
|
||||
const map: Record<number, string> = {
|
||||
0: '草稿',
|
||||
1: '已发布',
|
||||
2: '执行中',
|
||||
3: '已完成',
|
||||
4: '已关闭'
|
||||
}
|
||||
return map[status] || '未知'
|
||||
}
|
||||
|
||||
const getStatusType = (status: number) => {
|
||||
const map: Record<number, string> = {
|
||||
0: 'info',
|
||||
1: 'primary',
|
||||
2: 'warning',
|
||||
3: 'success',
|
||||
4: 'danger'
|
||||
}
|
||||
return map[status] || ''
|
||||
}
|
||||
|
||||
const getProgressColor = (percentage: number) => {
|
||||
if (percentage < 30) return '#f56c6c'
|
||||
if (percentage < 70) return '#e6a23c'
|
||||
return '#67c23a'
|
||||
}
|
||||
|
||||
getList()
|
||||
</script>
|
||||
839
src/views/mes/production/process-record/index.vue
Normal file
839
src/views/mes/production/process-record/index.vue
Normal file
@@ -0,0 +1,839 @@
|
||||
<template>
|
||||
<doc-alert title="【MES】工序记录" url="https://doc.iocoder.cn/mes/process-record/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form class="mb-4" :model="queryParams" ref="queryFormRef" :inline="true" label-width="80px">
|
||||
<el-form-item label="批次号">
|
||||
<el-input
|
||||
v-model="queryParams.batchNo"
|
||||
placeholder="请输入批次号"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="工序类型">
|
||||
<el-select
|
||||
v-model="selectedProcessType"
|
||||
placeholder="请选择工序类型"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
@change="handleProcessTypeChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in processTypes"
|
||||
:key="item.code"
|
||||
:label="`${item.name} (${item.code})`"
|
||||
:value="item.code"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-1">{{ item.name }}</span>
|
||||
<span class="text-gray-400 text-sm">({{ item.code }})</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
|
||||
<el-option label="未开始" :value="0" />
|
||||
<el-option label="进行中" :value="2" />
|
||||
<el-option label="完成" :value="1" />
|
||||
<el-option label="异常" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 数据展示部分 -->
|
||||
<el-card class="box-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>工序记录列表</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="filteredTableData" border style="width: 100%">
|
||||
<!-- 基础信息列 -->
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="batchNo" label="批次号" width="120" />
|
||||
<el-table-column label="工序信息" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<div class="process-info">
|
||||
<span class="process-name">{{ row.processName }}</span>
|
||||
<span class="process-code">{{ row.processCode }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="operator" label="操作员" width="100" />
|
||||
<el-table-column label="处理时间" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.startTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)">
|
||||
{{ getStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 动态工序数据列 -->
|
||||
<template v-if="selectedProcessType">
|
||||
<template v-for="field in getTableColumns(filteredTableData[0])" :key="field.prop">
|
||||
<el-table-column
|
||||
:prop="'processData.' + field.prop + '.value'"
|
||||
:label="field.label"
|
||||
min-width="120"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span>
|
||||
{{ formatFieldValue(row.processData[field.prop]) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
|
||||
|
||||
<!-- 操作列 -->
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" @click="handleViewProcessData(row)"> 查看/编辑 </el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 工序数据详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="currentProcess?.processName + ' - 工序数据'"
|
||||
width="600px"
|
||||
>
|
||||
<el-form
|
||||
v-if="currentProcess"
|
||||
ref="formRef"
|
||||
:model="editingProcessData"
|
||||
label-width="140px"
|
||||
:rules="formRules"
|
||||
>
|
||||
<template v-for="(field, key) in currentProcess.processData" :key="key">
|
||||
<el-form-item :label="field.label" :required="field.required" :prop="key">
|
||||
<!-- 选择器类型 -->
|
||||
<template v-if="field.type === 'select'">
|
||||
<el-select
|
||||
v-model="editingProcessData[key]"
|
||||
:disabled="!field.editable"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in field.options"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<!-- 数字类型 -->
|
||||
<template v-else-if="field.type === 'number'">
|
||||
<div class="number-input-with-unit">
|
||||
<el-input-number
|
||||
v-model="editingProcessData[key]"
|
||||
:disabled="!field.editable"
|
||||
:min="field.rules?.min"
|
||||
:max="field.rules?.max"
|
||||
:step="field.rules?.step"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<span v-if="field.unit" class="unit-label">{{ field.unit }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 字符串类型 -->
|
||||
<template v-else>
|
||||
<el-input
|
||||
v-model="editingProcessData[key]"
|
||||
:disabled="!field.editable"
|
||||
:placeholder="field.placeholder"
|
||||
/>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSaveProcessData"> 保存 </el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { Icon } from '@/components/Icon'
|
||||
|
||||
// 接口定义
|
||||
interface ProcessRecord {
|
||||
id: number
|
||||
processName: string
|
||||
processCode: string
|
||||
startTime: string
|
||||
endTime: string
|
||||
operator: string
|
||||
status: number
|
||||
remark: string
|
||||
processData: Record<string, any>
|
||||
batchNo: string // Added batchNo to the interface
|
||||
}
|
||||
|
||||
interface ProcessField {
|
||||
value: any
|
||||
label: string
|
||||
editable: boolean
|
||||
required?: boolean
|
||||
type: string
|
||||
rules?: {
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
}
|
||||
unit?: string
|
||||
showInTable?: boolean
|
||||
order?: number
|
||||
options?: Array<{
|
||||
label: string
|
||||
value: string | number
|
||||
}>
|
||||
placeholder?: string
|
||||
multiple?: boolean
|
||||
}
|
||||
|
||||
interface QueryParams {
|
||||
pageNo: number
|
||||
pageSize: number
|
||||
batchNo?: string
|
||||
status?: number
|
||||
}
|
||||
|
||||
// 响应式变量定义
|
||||
const loading = ref(true)
|
||||
const total = ref(0)
|
||||
const queryFormRef = ref<FormInstance>()
|
||||
const selectedProcessType = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const currentProcess = ref<ProcessRecord | null>(null)
|
||||
const editingProcessData = reactive<Record<string, any>>({})
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive<QueryParams>({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
batchNo: undefined,
|
||||
status: undefined
|
||||
})
|
||||
|
||||
// 工序类型列表
|
||||
const processTypes = [
|
||||
{ name: '焊接工序', code: 'WELDING' },
|
||||
{ name: '热处理工序', code: 'HEAT_TREATMENT' },
|
||||
{ name: '装配工序', code: 'ASSEMBLY' },
|
||||
{ name: '质量检验', code: 'QUALITY_CHECK' }
|
||||
]
|
||||
|
||||
// 查询操作
|
||||
const handleQuery = () => {
|
||||
// TODO: 实现查询逻辑
|
||||
console.log('查询参数:', queryParams)
|
||||
}
|
||||
|
||||
// 重置操作
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields?.()
|
||||
queryParams.batchNo = undefined
|
||||
queryParams.status = undefined
|
||||
selectedProcessType.value = ''
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
// 模拟数据,实际应该从API获取
|
||||
const mockData = {
|
||||
code: 0,
|
||||
data: [
|
||||
{
|
||||
processData: {
|
||||
weldingTemperature: {
|
||||
value: 200.5,
|
||||
label: '焊接温度',
|
||||
editable: true,
|
||||
required: true,
|
||||
type: 'number',
|
||||
rules: {
|
||||
min: 0,
|
||||
max: 1000,
|
||||
step: 0.1
|
||||
},
|
||||
unit: '°C',
|
||||
showInTable: true,
|
||||
order: 1
|
||||
},
|
||||
inspectionResult: {
|
||||
value: '合格',
|
||||
label: '检验结果',
|
||||
editable: true,
|
||||
required: true,
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '合格', value: '合格' },
|
||||
{ label: '不合格', value: '不合格' }
|
||||
],
|
||||
showInTable: true,
|
||||
order: 3
|
||||
},
|
||||
weldingMethod: {
|
||||
value: '电弧焊',
|
||||
label: '焊接方法',
|
||||
editable: false,
|
||||
type: 'string',
|
||||
showInTable: true,
|
||||
order: 2
|
||||
},
|
||||
weldingPressure: {
|
||||
value: 2.4,
|
||||
label: '焊接压力',
|
||||
editable: true,
|
||||
required: true,
|
||||
type: 'number',
|
||||
rules: {
|
||||
min: 0,
|
||||
max: 10,
|
||||
step: 0.1
|
||||
},
|
||||
unit: 'MPa',
|
||||
showInTable: false
|
||||
},
|
||||
weldingMaterial: {
|
||||
value: '不锈钢',
|
||||
label: '焊接材料',
|
||||
editable: true,
|
||||
type: 'string',
|
||||
placeholder: '请输入焊接材料',
|
||||
showInTable: false
|
||||
}
|
||||
},
|
||||
processName: '焊接工序',
|
||||
processCode: 'WELDING',
|
||||
startTime: '2024-03-19 10:00:00',
|
||||
remark: '焊接质量良好',
|
||||
id: 1,
|
||||
batchNo: 'B202403190001',
|
||||
endTime: '2024-03-19 11:30:00',
|
||||
operator: '张三',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
processData: {
|
||||
surfaceHardness: {
|
||||
value: 58,
|
||||
label: '表面硬度',
|
||||
editable: true,
|
||||
required: true,
|
||||
type: 'number',
|
||||
rules: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1
|
||||
},
|
||||
unit: 'HRC',
|
||||
showInTable: true,
|
||||
order: 2
|
||||
},
|
||||
deformation: {
|
||||
value: 0.02,
|
||||
label: '变形量',
|
||||
editable: true,
|
||||
required: true,
|
||||
type: 'number',
|
||||
rules: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.01
|
||||
},
|
||||
unit: 'mm',
|
||||
showInTable: false
|
||||
},
|
||||
holdingTime: {
|
||||
value: 120,
|
||||
label: '保温时间',
|
||||
editable: true,
|
||||
required: true,
|
||||
type: 'number',
|
||||
rules: {
|
||||
min: 0,
|
||||
max: 300,
|
||||
step: 1
|
||||
},
|
||||
unit: 'min',
|
||||
showInTable: true,
|
||||
order: 3
|
||||
},
|
||||
heatingTemperature: {
|
||||
value: 800,
|
||||
label: '加热温度',
|
||||
editable: true,
|
||||
required: true,
|
||||
type: 'number',
|
||||
rules: {
|
||||
min: 0,
|
||||
max: 1200,
|
||||
step: 1
|
||||
},
|
||||
unit: '°C',
|
||||
showInTable: true,
|
||||
order: 1
|
||||
},
|
||||
coolingMethod: {
|
||||
value: '空冷',
|
||||
label: '冷却方式',
|
||||
editable: true,
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '空冷', value: '空冷' },
|
||||
{ label: '水冷', value: '水冷' },
|
||||
{ label: '油冷', value: '油冷' }
|
||||
],
|
||||
showInTable: false
|
||||
}
|
||||
},
|
||||
processName: '热处理工序',
|
||||
processCode: 'HEAT_TREATMENT',
|
||||
startTime: '2024-03-19 13:00:00',
|
||||
remark: '热处理完成',
|
||||
id: 2,
|
||||
batchNo: 'B202403190001',
|
||||
endTime: '2024-03-19 15:00:00',
|
||||
operator: '李四',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
processData: {
|
||||
assemblyParts: {
|
||||
value: ['轴承', '轴套', '螺栓'],
|
||||
label: '装配零件',
|
||||
editable: true,
|
||||
required: true,
|
||||
type: 'select',
|
||||
multiple: true,
|
||||
options: [
|
||||
{ label: '轴承', value: '轴承' },
|
||||
{ label: '轴套', value: '轴套' },
|
||||
{ label: '螺栓', value: '螺栓' },
|
||||
{ label: '螺母', value: '螺母' },
|
||||
{ label: '垫片', value: '垫片' }
|
||||
],
|
||||
showInTable: true,
|
||||
order: 1
|
||||
},
|
||||
gapValue: {
|
||||
value: 0.03,
|
||||
label: '间隙值',
|
||||
editable: true,
|
||||
required: true,
|
||||
type: 'number',
|
||||
rules: {
|
||||
min: 0,
|
||||
max: 0.1,
|
||||
step: 0.01
|
||||
},
|
||||
unit: 'mm',
|
||||
showInTable: true,
|
||||
order: 2
|
||||
},
|
||||
torque: {
|
||||
value: 45.5,
|
||||
label: '扭矩',
|
||||
editable: true,
|
||||
required: true,
|
||||
type: 'number',
|
||||
rules: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 0.5
|
||||
},
|
||||
unit: 'N·m',
|
||||
showInTable: true,
|
||||
order: 3
|
||||
},
|
||||
testResult: {
|
||||
value: '通过',
|
||||
label: '测试结果',
|
||||
editable: true,
|
||||
required: true,
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '通过', value: '通过' },
|
||||
{ label: '不通过', value: '不通过' },
|
||||
{ label: '待复检', value: '待复检' }
|
||||
],
|
||||
showInTable: true,
|
||||
order: 4
|
||||
},
|
||||
alignmentError: {
|
||||
value: 0.02,
|
||||
label: '对准误差',
|
||||
editable: true,
|
||||
required: true,
|
||||
type: 'number',
|
||||
rules: {
|
||||
min: 0,
|
||||
max: 0.1,
|
||||
step: 0.01
|
||||
},
|
||||
unit: 'mm',
|
||||
showInTable: true,
|
||||
order: 5
|
||||
}
|
||||
},
|
||||
processName: '装配工序',
|
||||
processCode: 'ASSEMBLY',
|
||||
startTime: '2024-03-19 15:30:00',
|
||||
remark: '装配完成,测试通过',
|
||||
id: 3,
|
||||
batchNo: 'B202403190002',
|
||||
endTime: '2024-03-19 17:00:00',
|
||||
operator: '王五',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
processData: {
|
||||
inspectionMethod: {
|
||||
value: '目视检查',
|
||||
label: '检验方法',
|
||||
editable: true,
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '目视检查', value: '目视检查' },
|
||||
{ label: '尺寸测量', value: '尺寸测量' },
|
||||
{ label: '无损检测', value: '无损检测' }
|
||||
],
|
||||
showInTable: true,
|
||||
order: 1
|
||||
},
|
||||
surfaceQuality: {
|
||||
value: '良好',
|
||||
label: '表面质量',
|
||||
editable: true,
|
||||
required: true,
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '优秀', value: '优秀' },
|
||||
{ label: '良好', value: '良好' },
|
||||
{ label: '合格', value: '合格' },
|
||||
{ label: '不合格', value: '不合格' }
|
||||
],
|
||||
showInTable: true,
|
||||
order: 2
|
||||
},
|
||||
dimensionError: {
|
||||
value: 0.05,
|
||||
label: '尺寸误差',
|
||||
editable: true,
|
||||
required: true,
|
||||
type: 'number',
|
||||
rules: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.01
|
||||
},
|
||||
unit: 'mm',
|
||||
showInTable: true,
|
||||
order: 3
|
||||
},
|
||||
defectCount: {
|
||||
value: 0,
|
||||
label: '缺陷数量',
|
||||
editable: true,
|
||||
required: true,
|
||||
type: 'number',
|
||||
rules: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1
|
||||
},
|
||||
showInTable: true,
|
||||
order: 4
|
||||
}
|
||||
},
|
||||
processName: '质量检验',
|
||||
processCode: 'QUALITY_CHECK',
|
||||
startTime: '2024-03-19 17:30:00',
|
||||
remark: '产品质量符合要求',
|
||||
id: 4,
|
||||
batchNo: 'B202403190002',
|
||||
endTime: '2024-03-19 18:00:00',
|
||||
operator: '赵六',
|
||||
status: 1
|
||||
}
|
||||
],
|
||||
msg: ''
|
||||
}
|
||||
|
||||
const tableData = ref<ProcessRecord[]>(mockData.data)
|
||||
|
||||
// 根据选中的工序类型筛选数据
|
||||
const filteredTableData = computed(() => {
|
||||
if (!selectedProcessType.value) {
|
||||
return tableData.value
|
||||
}
|
||||
return tableData.value.filter((item) => item.processCode === selectedProcessType.value)
|
||||
})
|
||||
|
||||
// 生成表单验证规则
|
||||
const formRules = computed(() => {
|
||||
if (!currentProcess.value) return {}
|
||||
|
||||
const rules: Record<string, any[]> = {}
|
||||
Object.entries(currentProcess.value.processData).forEach(([key, field]) => {
|
||||
if (field.required) {
|
||||
rules[key] = [
|
||||
{
|
||||
required: true,
|
||||
message: `请输入${field.label}`,
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if (field.type === 'number' && field.rules) {
|
||||
if (!rules[key]) rules[key] = []
|
||||
rules[key].push({
|
||||
type: 'number',
|
||||
min: field.rules.min,
|
||||
max: field.rules.max,
|
||||
message: `${field.label}必须在 ${field.rules.min} 到 ${field.rules.max} 之间`,
|
||||
trigger: 'blur'
|
||||
})
|
||||
}
|
||||
})
|
||||
return rules
|
||||
})
|
||||
|
||||
// 格式化标签名称
|
||||
const formatLabel = (key: string) => {
|
||||
const fieldData = currentProcess.value?.processData[key]
|
||||
return fieldData?.label || key
|
||||
}
|
||||
|
||||
// 查看/编辑工序数据
|
||||
const handleViewProcessData = (row: ProcessRecord) => {
|
||||
currentProcess.value = row
|
||||
// 深拷贝processData以供编辑,只复制value值
|
||||
const processDataValues = Object.entries(row.processData).reduce(
|
||||
(acc, [key, field]) => {
|
||||
acc[key] = field.value
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, any>
|
||||
)
|
||||
Object.assign(editingProcessData, processDataValues)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 保存工序数据
|
||||
const handleSaveProcessData = async () => {
|
||||
if (!currentProcess.value || !formRef.value) return
|
||||
|
||||
try {
|
||||
// 表单验证
|
||||
await formRef.value.validate()
|
||||
|
||||
// 更新表格中的数据,保持label不变,只更新value
|
||||
const index = tableData.value.findIndex((item) => item.id === currentProcess.value!.id)
|
||||
if (index !== -1) {
|
||||
const updatedProcessData = Object.entries(editingProcessData).reduce(
|
||||
(acc, [key, value]) => {
|
||||
acc[key] = {
|
||||
...currentProcess.value!.processData[key],
|
||||
value
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, any>
|
||||
)
|
||||
|
||||
tableData.value[index].processData = updatedProcessData
|
||||
|
||||
// 将整个表格数据序列化并输出到控制台
|
||||
const formattedData = {
|
||||
code: 0,
|
||||
data: tableData.value,
|
||||
msg: ''
|
||||
}
|
||||
console.log('序列化后的JSON数据:')
|
||||
console.log(JSON.stringify(formattedData, null, 2))
|
||||
|
||||
ElMessage.success('保存成功')
|
||||
dialogVisible.value = false
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('请检查表单填写是否正确')
|
||||
}
|
||||
}
|
||||
|
||||
// 工序类型变化处理
|
||||
const handleProcessTypeChange = (value: string) => {
|
||||
// 可以在这里添加其他处理逻辑
|
||||
console.log('选中的工序类型:', value)
|
||||
}
|
||||
|
||||
// 获取当前工序需要显示的列
|
||||
const getProcessColumns = computed(() => {
|
||||
if (!currentProcess.value) return []
|
||||
|
||||
const processData = currentProcess.value.processData
|
||||
return Object.entries(processData)
|
||||
.filter(([_, field]) => field.showInTable)
|
||||
.sort((a, b) => (a[1].order || 0) - (b[1].order || 0))
|
||||
.map(([key, field]) => ({
|
||||
prop: key,
|
||||
label: field.label,
|
||||
formatter: (row: any) => {
|
||||
const value = row.processData[key].value
|
||||
const unit = row.processData[key].unit
|
||||
if (Array.isArray(value)) {
|
||||
return value.join('、')
|
||||
}
|
||||
return unit ? `${value}${unit}` : value
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
// 获取表格列配置
|
||||
const getTableColumns = (row: any) => {
|
||||
if (!row) return []
|
||||
return Object.entries(row.processData)
|
||||
.filter(([_, field]) => (field as ProcessField).showInTable)
|
||||
.sort((a, b) => ((a[1] as ProcessField).order || 0) - ((b[1] as ProcessField).order || 0))
|
||||
.map(([key, field]) => ({
|
||||
prop: key,
|
||||
label: (field as ProcessField).label
|
||||
}))
|
||||
}
|
||||
|
||||
// 格式化字段值
|
||||
const formatFieldValue = (field: ProcessField | undefined) => {
|
||||
if (!field) return ''
|
||||
|
||||
const { value, unit } = field
|
||||
if (Array.isArray(value)) {
|
||||
return value.join('、')
|
||||
}
|
||||
return unit ? `${value}${unit}` : value
|
||||
}
|
||||
|
||||
// 在 script 部分添加新的方法
|
||||
const formatDateTime = (time: string) => {
|
||||
if (!time) return '-'
|
||||
return time
|
||||
}
|
||||
|
||||
const getStatusType = (status: number) => {
|
||||
const statusMap: Record<number, 'success' | 'warning' | 'info' | 'danger'> = {
|
||||
0: 'info', // 未开始
|
||||
1: 'success', // 完成
|
||||
2: 'warning', // 进行中
|
||||
3: 'danger' // 异常
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
const getStatusText = (status: number) => {
|
||||
const statusMap: Record<number, string> = {
|
||||
0: '未开始',
|
||||
1: '完成',
|
||||
2: '进行中',
|
||||
3: '异常'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
}
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
.number-input-with-unit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.unit-label {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
:deep(.el-form-item__label) {
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.2;
|
||||
padding-right: 12px;
|
||||
}
|
||||
.process-code {
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.process-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.process-name {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.process-code {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 18px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__content) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.el-select) {
|
||||
width: 240px;
|
||||
}
|
||||
</style>
|
||||
201
src/views/mes/production/process-record/readme.txt
Normal file
201
src/views/mes/production/process-record/readme.txt
Normal file
@@ -0,0 +1,201 @@
|
||||
# 工序记录字段定义说明
|
||||
|
||||
## 1. 字段基本属性
|
||||
|
||||
每个字段都需要包含以下基本属性:
|
||||
|
||||
- value: 字段值
|
||||
- label: 字段中文名称
|
||||
- type: 字段类型
|
||||
- editable: 是否可编辑
|
||||
- showInTable: 是否在表格中显示
|
||||
- order: 表格中的显示顺序(数字越小越靠前)
|
||||
|
||||
## 2. 字段类型说明
|
||||
|
||||
### 2.1 字符串类型 (string)
|
||||
```json
|
||||
{
|
||||
"value": "电弧焊",
|
||||
"label": "焊接方法",
|
||||
"type": "string",
|
||||
"editable": true,
|
||||
"placeholder": "请输入焊接方法",
|
||||
"required": true
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 数字类型 (number)
|
||||
```json
|
||||
{
|
||||
"value": 200.5,
|
||||
"label": "焊接温度",
|
||||
"type": "number",
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"rules": {
|
||||
"min": 0,
|
||||
"max": 1000,
|
||||
"step": 0.1
|
||||
},
|
||||
"unit": "°C"
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 选择类型 (select)
|
||||
```json
|
||||
{
|
||||
"value": "合格",
|
||||
"label": "检验结果",
|
||||
"type": "select",
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"options": [
|
||||
{ "label": "合格", "value": "合格" },
|
||||
{ "label": "不合格", "value": "不合格" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 多选类型 (select with multiple)
|
||||
```json
|
||||
{
|
||||
"value": ["轴承", "轴套"],
|
||||
"label": "装配零件",
|
||||
"type": "select",
|
||||
"multiple": true,
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"options": [
|
||||
{ "label": "轴承", "value": "轴承" },
|
||||
{ "label": "轴套", "value": "轴套" },
|
||||
{ "label": "螺栓", "value": "螺栓" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 字段规则说明
|
||||
|
||||
### 3.1 必填规则
|
||||
- required: true/false,表示字段是否必填
|
||||
|
||||
### 3.2 数字类型规则
|
||||
- min: 最小值
|
||||
- max: 最大值
|
||||
- step: 步进值(每次增减的数值)
|
||||
|
||||
### 3.3 显示规则
|
||||
- showInTable: 是否在表格中显示该字段
|
||||
- order: 在表格中的显示顺序,从1开始
|
||||
- placeholder: 输入框提示文本
|
||||
|
||||
## 4. 完整示例
|
||||
|
||||
### 4.1 焊接工序示例
|
||||
```json
|
||||
{
|
||||
"processData": {
|
||||
"weldingTemperature": {
|
||||
"value": 200.5,
|
||||
"label": "焊接温度",
|
||||
"type": "number",
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"rules": {
|
||||
"min": 0,
|
||||
"max": 1000,
|
||||
"step": 0.1
|
||||
},
|
||||
"unit": "°C",
|
||||
"showInTable": true,
|
||||
"order": 1
|
||||
},
|
||||
"weldingMethod": {
|
||||
"value": "电弧焊",
|
||||
"label": "焊接方法",
|
||||
"type": "string",
|
||||
"editable": false,
|
||||
"showInTable": true,
|
||||
"order": 2
|
||||
},
|
||||
"inspectionResult": {
|
||||
"value": "合格",
|
||||
"label": "检验结果",
|
||||
"type": "select",
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"options": [
|
||||
{ "label": "合格", "value": "合格" },
|
||||
{ "label": "不合格", "value": "不合格" }
|
||||
],
|
||||
"showInTable": true,
|
||||
"order": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 装配工序示例
|
||||
```json
|
||||
{
|
||||
"processData": {
|
||||
"assemblyParts": {
|
||||
"value": ["轴承", "轴套"],
|
||||
"label": "装配零件",
|
||||
"type": "select",
|
||||
"multiple": true,
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"options": [
|
||||
{ "label": "轴承", "value": "轴承" },
|
||||
{ "label": "轴套", "value": "轴套" },
|
||||
{ "label": "螺栓", "value": "螺栓" }
|
||||
],
|
||||
"showInTable": true,
|
||||
"order": 1
|
||||
},
|
||||
"torque": {
|
||||
"value": 45.5,
|
||||
"label": "扭矩",
|
||||
"type": "number",
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"rules": {
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 0.5
|
||||
},
|
||||
"unit": "N·m",
|
||||
"showInTable": true,
|
||||
"order": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 注意事项
|
||||
|
||||
1. 字段命名规则:
|
||||
- 使用驼峰命名法
|
||||
- 名称应具有描述性
|
||||
- 避免使用特殊字符
|
||||
|
||||
2. 数值类型注意事项:
|
||||
- 必须设置合理的最大最小值
|
||||
- step值要考虑实际精度需求
|
||||
- 单位必须准确填写
|
||||
|
||||
3. 选择类型注意事项:
|
||||
- options中的value值要唯一
|
||||
- label要简洁明了
|
||||
- 多选时value必须是数组
|
||||
|
||||
4. 显示规则注意事项:
|
||||
- 表格显示的字段数量要适中
|
||||
- order值不要重复
|
||||
- 重要字段优先显示
|
||||
|
||||
5. 编辑权限注意事项:
|
||||
- 关键字段建议设置为不可编辑
|
||||
- 必填字段要有明确的提示
|
||||
- 考虑字段间的依赖关系
|
||||
431
src/views/mes/production/process-record/数据驱动表单说明.md
Normal file
431
src/views/mes/production/process-record/数据驱动表单说明.md
Normal file
@@ -0,0 +1,431 @@
|
||||
# 工序字段配置指南
|
||||
|
||||
## 1. 基本结构
|
||||
|
||||
工序字段配置采用 JSON 格式,每个字段都是一个键值对,其中:
|
||||
|
||||
- 键(key):字段的唯一标识符,建议使用驼峰命名法
|
||||
- 值(value):字段的配置对象,包含该字段的所有属性
|
||||
|
||||
基本结构示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"fieldName": {
|
||||
"type": "string",
|
||||
"label": "字段中文名",
|
||||
"order": 1,
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 字段通用属性
|
||||
|
||||
每个字段都支持以下通用属性:
|
||||
|
||||
| 属性名 | 类型 | 必填 | 说明 | 示例 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| type | string | 是 | 字段类型,支持 "string"、"number"、"select" | "type": "string" |
|
||||
| label | string | 是 | 字段的显示名称(中文) | "label": "焊接方法" |
|
||||
| order | number | 否 | 显示顺序,数字越小越靠前 | "order": 1 |
|
||||
| editable | boolean | 是 | 是否可编辑 | "editable": true |
|
||||
| required | boolean | 否 | 是否必填 | "required": true |
|
||||
| showInTable | boolean | 否 | 是否在主表格中显示 | "showInTable": true |
|
||||
| placeholder | string | 否 | 输入框占位文本 | "placeholder": "请输入焊接方法" |
|
||||
|
||||
## 3. 字段类型特有属性
|
||||
|
||||
### 3.1 字符串类型 (string)
|
||||
|
||||
```json
|
||||
{
|
||||
"weldingMethod": {
|
||||
"type": "string",
|
||||
"label": "焊接方法",
|
||||
"order": 1,
|
||||
"editable": true,
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 数字类型 (number)
|
||||
|
||||
数字类型特有属性:
|
||||
|
||||
| 属性名 | 类型 | 必填 | 说明 | 示例 |
|
||||
| ------ | ------ | ---- | -------- | ------------ |
|
||||
| unit | string | 否 | 单位 | "unit": "°C" |
|
||||
| rules | object | 否 | 数值规则 | 见下方示例 |
|
||||
|
||||
rules 对象包含:
|
||||
|
||||
- min: 最小值
|
||||
- max: 最大值
|
||||
- step: 步进值
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"label": "温度",
|
||||
"unit": "°C",
|
||||
"rules": {
|
||||
"min": 0,
|
||||
"max": 1000,
|
||||
"step": 0.1
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 选择器类型 (select)
|
||||
|
||||
选择器类型特有属性:
|
||||
|
||||
| 属性名 | 类型 | 必填 | 说明 | 示例 |
|
||||
| -------- | ------- | ---- | ------------ | ---------------- |
|
||||
| options | array | 是 | 选项列表 | 见下方示例 |
|
||||
| multiple | boolean | 否 | 是否支持多选 | "multiple": true |
|
||||
|
||||
options 数组中的每个选项包含:
|
||||
|
||||
- label: 选项显示文本
|
||||
- value: 选项值
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"inspectionResult": {
|
||||
"type": "select",
|
||||
"label": "检验结果",
|
||||
"options": [
|
||||
{ "label": "合格", "value": "合格" },
|
||||
{ "label": "不合格", "value": "不合格" }
|
||||
],
|
||||
"multiple": false,
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 完整示例
|
||||
|
||||
### 4.1 焊接工序配置示例
|
||||
|
||||
```json
|
||||
{
|
||||
"weldingMethod": {
|
||||
"type": "string",
|
||||
"label": "焊接方法",
|
||||
"order": 2,
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"showInTable": true
|
||||
},
|
||||
"weldingTemperature": {
|
||||
"type": "number",
|
||||
"label": "焊接温度",
|
||||
"order": 1,
|
||||
"unit": "°C",
|
||||
"rules": {
|
||||
"max": 1000,
|
||||
"min": 0,
|
||||
"step": 0.1
|
||||
},
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"showInTable": true
|
||||
},
|
||||
"inspectionResult": {
|
||||
"type": "select",
|
||||
"label": "检验结果",
|
||||
"order": 3,
|
||||
"options": [
|
||||
{ "label": "合格", "value": "合格" },
|
||||
{ "label": "不合格", "value": "不合格" }
|
||||
],
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"showInTable": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 装配工序配置示例
|
||||
|
||||
```json
|
||||
{
|
||||
"assemblyParts": {
|
||||
"type": "select",
|
||||
"label": "装配零件",
|
||||
"order": 1,
|
||||
"options": [
|
||||
{ "label": "轴承", "value": "轴承" },
|
||||
{ "label": "轴套", "value": "轴套" },
|
||||
{ "label": "螺栓", "value": "螺栓" }
|
||||
],
|
||||
"multiple": true,
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"showInTable": true
|
||||
},
|
||||
"gapValue": {
|
||||
"type": "number",
|
||||
"label": "间隙值",
|
||||
"order": 2,
|
||||
"unit": "mm",
|
||||
"rules": {
|
||||
"max": 1,
|
||||
"min": 0,
|
||||
"step": 0.01
|
||||
},
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"showInTable": true
|
||||
},
|
||||
"alignmentError": {
|
||||
"type": "number",
|
||||
"label": "对中误差",
|
||||
"order": 3,
|
||||
"unit": "mm",
|
||||
"rules": {
|
||||
"max": 0.1,
|
||||
"min": 0,
|
||||
"step": 0.001
|
||||
},
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"showInTable": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 番茄酱生产工序配置示例
|
||||
|
||||
```json
|
||||
{
|
||||
"rawMaterialBatch": {
|
||||
"type": "string",
|
||||
"label": "原料批次号",
|
||||
"order": 1,
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"showInTable": true,
|
||||
"placeholder": "请输入原料批次号"
|
||||
},
|
||||
"cookingTemperature": {
|
||||
"type": "number",
|
||||
"label": "加热温度",
|
||||
"order": 2,
|
||||
"unit": "°C",
|
||||
"rules": {
|
||||
"max": 120,
|
||||
"min": 60,
|
||||
"step": 1
|
||||
},
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"showInTable": true
|
||||
},
|
||||
"cookingTime": {
|
||||
"type": "number",
|
||||
"label": "加热时间",
|
||||
"order": 3,
|
||||
"unit": "min",
|
||||
"rules": {
|
||||
"max": 120,
|
||||
"min": 30,
|
||||
"step": 5
|
||||
},
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"showInTable": true
|
||||
},
|
||||
"seasonings": {
|
||||
"type": "select",
|
||||
"label": "调味料",
|
||||
"order": 4,
|
||||
"options": [
|
||||
{ "label": "食用盐", "value": "salt" },
|
||||
{ "label": "白砂糖", "value": "sugar" },
|
||||
{ "label": "白胡椒粉", "value": "pepper" },
|
||||
{ "label": "香辛料", "value": "spices" }
|
||||
],
|
||||
"multiple": true,
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"showInTable": true
|
||||
},
|
||||
"consistency": {
|
||||
"type": "select",
|
||||
"label": "稠度检测",
|
||||
"order": 5,
|
||||
"options": [
|
||||
{ "label": "合格", "value": "qualified" },
|
||||
{ "label": "过稀", "value": "thin" },
|
||||
{ "label": "过稠", "value": "thick" }
|
||||
],
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"showInTable": true
|
||||
},
|
||||
"ph": {
|
||||
"type": "number",
|
||||
"label": "pH值",
|
||||
"order": 6,
|
||||
"rules": {
|
||||
"max": 7,
|
||||
"min": 3,
|
||||
"step": 0.1
|
||||
},
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"showInTable": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 甜菊糖生产工序配置示例
|
||||
|
||||
```json
|
||||
{
|
||||
"extractionTemperature": {
|
||||
"type": "number",
|
||||
"label": "提取温度",
|
||||
"order": 1,
|
||||
"unit": "°C",
|
||||
"rules": {
|
||||
"max": 80,
|
||||
"min": 40,
|
||||
"step": 1
|
||||
},
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"showInTable": true
|
||||
},
|
||||
"extractionTime": {
|
||||
"type": "number",
|
||||
"label": "提取时间",
|
||||
"order": 2,
|
||||
"unit": "min",
|
||||
"rules": {
|
||||
"max": 180,
|
||||
"min": 60,
|
||||
"step": 5
|
||||
},
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"showInTable": true
|
||||
},
|
||||
"solventType": {
|
||||
"type": "select",
|
||||
"label": "溶剂类型",
|
||||
"order": 3,
|
||||
"options": [
|
||||
{ "label": "纯净水", "value": "water" },
|
||||
{ "label": "乙醇", "value": "ethanol" },
|
||||
{ "label": "混合溶剂", "value": "mixed" }
|
||||
],
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"showInTable": true
|
||||
},
|
||||
"solventConcentration": {
|
||||
"type": "number",
|
||||
"label": "溶剂浓度",
|
||||
"order": 4,
|
||||
"unit": "%",
|
||||
"rules": {
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"step": 1
|
||||
},
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"showInTable": true
|
||||
},
|
||||
"purityTest": {
|
||||
"type": "number",
|
||||
"label": "纯度检测",
|
||||
"order": 5,
|
||||
"unit": "%",
|
||||
"rules": {
|
||||
"max": 100,
|
||||
"min": 90,
|
||||
"step": 0.1
|
||||
},
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"showInTable": true
|
||||
},
|
||||
"crystalForm": {
|
||||
"type": "select",
|
||||
"label": "结晶形态",
|
||||
"order": 6,
|
||||
"options": [
|
||||
{ "label": "针状", "value": "needle" },
|
||||
{ "label": "片状", "value": "flake" },
|
||||
{ "label": "粒状", "value": "granular" }
|
||||
],
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"showInTable": true
|
||||
},
|
||||
"moistureContent": {
|
||||
"type": "number",
|
||||
"label": "水分含量",
|
||||
"order": 7,
|
||||
"unit": "%",
|
||||
"rules": {
|
||||
"max": 10,
|
||||
"min": 0,
|
||||
"step": 0.1
|
||||
},
|
||||
"editable": true,
|
||||
"required": true,
|
||||
"showInTable": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 注意事项
|
||||
|
||||
1. 字段命名规范:
|
||||
|
||||
- 使用驼峰命名法
|
||||
- 避免使用特殊字符
|
||||
- 建议使用英文
|
||||
|
||||
2. 数值类型注意事项:
|
||||
|
||||
- 必须设置合理的最大值和最小值
|
||||
- step 建议根据精度需求设置
|
||||
- 单位使用通用符号,如 °C、mm、MPa 等
|
||||
|
||||
3. 选择器类型注意事项:
|
||||
|
||||
- options 中的 value 建议使用简单的字符串或数字
|
||||
- 多选时注意设置 multiple: true
|
||||
- 选项建议不要太多,否则影响用户体验
|
||||
|
||||
4. 显示规则:
|
||||
|
||||
- order 值越小越靠前显示
|
||||
- showInTable 为 true 的字段会显示在主表格中
|
||||
- 建议重要字段设置 showInTable: true
|
||||
|
||||
5. 编辑权限:
|
||||
|
||||
- editable 为 false 的字段用户无法修改
|
||||
- 关键参数建议设置 editable: false
|
||||
- required 为 true 的字段必须填写
|
||||
|
||||
6. 字段数量:
|
||||
- 建议每个工序的字段数量控制在 10 个以内
|
||||
- 必填字段不要太多,影响操作效率
|
||||
135
src/views/mes/production/process/ProcessOperationForm.vue
Normal file
135
src/views/mes/production/process/ProcessOperationForm.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:title="formType === 'create' ? '新增工序' : '编辑工序'"
|
||||
v-model="visible"
|
||||
width="500px"
|
||||
@close="handleClose"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px" v-loading="formLoading">
|
||||
<el-form-item label="工序编码" prop="code">
|
||||
<el-input v-model="form.code" placeholder="请输入工序编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="工序名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入工序名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="工序描述" prop="description">
|
||||
<el-input v-model="form.description" placeholder="请输入工序描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="工序类型" prop="type">
|
||||
<el-input v-model.number="form.type" placeholder="请输入工序类型(数字)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="标准工时(分钟)" prop="standardTime">
|
||||
<el-input v-model.number="form.standardTime" placeholder="请输入标准工时" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="form.status" placeholder="请选择状态">
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="submitForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
YVHcreateProcessOperation,
|
||||
YVHupdateProcessOperation,
|
||||
YVHgetProcessOperation
|
||||
} from '@/api/mes/production/process-operation'
|
||||
|
||||
// 生成工序编码
|
||||
const generateOperationCode = () => {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(now.getDate()).padStart(2, '0')
|
||||
const random = Math.floor(Math.random() * 1000)
|
||||
.toString()
|
||||
.padStart(3, '0')
|
||||
return `OP${year}${month}${day}${random}`
|
||||
}
|
||||
|
||||
const emits = defineEmits(['success'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const formLoading = ref(false) // 表单加载状态
|
||||
const formType = ref<'create' | 'update'>('create')
|
||||
const form = reactive<any>({})
|
||||
const formRef = ref()
|
||||
|
||||
const rules = {
|
||||
code: [{ required: true, message: '请输入工序编码', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入工序名称', trigger: 'blur' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const open = async (type: 'create' | 'update', id?: number) => {
|
||||
formType.value = type
|
||||
visible.value = true
|
||||
formLoading.value = true // 开始加载,禁用表单
|
||||
|
||||
try {
|
||||
// 重置表单
|
||||
Object.assign(form, {
|
||||
code: type === 'create' ? generateOperationCode() : '',
|
||||
name: '',
|
||||
description: '',
|
||||
type: 1, // 默认工序类型为1
|
||||
standardTime: 30, // 默认标准工时30分钟
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
if (type === 'update' && id) {
|
||||
const res = await YVHgetProcessOperation(id)
|
||||
// 直接使用接口返回的数据,不进行解构
|
||||
Object.assign(form, res)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('初始化表单失败:', error)
|
||||
ElMessage.error('加载数据失败,请重试')
|
||||
visible.value = false
|
||||
} finally {
|
||||
formLoading.value = false // 加载完成,启用表单
|
||||
}
|
||||
}
|
||||
|
||||
const submitForm = () => {
|
||||
formRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
loading.value = true
|
||||
try {
|
||||
if (formType.value === 'create') {
|
||||
await YVHcreateProcessOperation(form)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await YVHupdateProcessOperation(form)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
visible.value = false
|
||||
emits('success')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 对外暴露 open 方法
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
211
src/views/mes/production/process/index.vue
Normal file
211
src/views/mes/production/process/index.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<doc-alert title="【MES】工序管理" url="https://doc.iocoder.cn/mes/process-operation/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="工序编码" prop="code">
|
||||
<el-input
|
||||
v-model="queryParams.code"
|
||||
placeholder="请输入工序编码"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="工序名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入工序名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="openForm('create')"
|
||||
v-hasPermi="['mes:process-operation:create']"
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
@click="handleDelete(selectionList.map((item) => item.id))"
|
||||
v-hasPermi="['mes:process-operation:delete']"
|
||||
:disabled="selectionList.length === 0"
|
||||
>
|
||||
<Icon icon="ep:delete" class="mr-5px" /> 删除
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
:stripe="true"
|
||||
:show-overflow-tooltip="true"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-click="handleRowClick"
|
||||
>
|
||||
<el-table-column type="selection" width="50" />
|
||||
<el-table-column width="80" label="工序ID" align="center" prop="id" />
|
||||
<el-table-column min-width="120" label="工序编码" align="center" prop="code" />
|
||||
<el-table-column min-width="120" label="工序名称" align="center" prop="name" />
|
||||
<el-table-column min-width="180" label="工序描述" align="center" prop="description" />
|
||||
<el-table-column min-width="120" label="工序类型" align="center" prop="type" />
|
||||
<el-table-column min-width="120" label="标准工时(分钟)" align="center" prop="standardTime" />
|
||||
<el-table-column min-width="100" label="状态" align="center" prop="status">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
||||
{{ scope.row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column min-width="120" label="备注" align="center" prop="remark" />
|
||||
<el-table-column label="操作" align="center" fixed="right" width="120">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
v-hasPermi="['mes:process-operation:update']"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete([scope.row.id])"
|
||||
v-hasPermi="['mes:process-operation:delete']"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<ProcessOperationForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
YVHgetProcessOperationPage,
|
||||
YVHdeleteProcessOperation
|
||||
} from '@/api/mes/production/process-operation'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import ProcessOperationForm from './ProcessOperationForm.vue'
|
||||
|
||||
const loading = ref(true)
|
||||
const list = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
code: undefined,
|
||||
name: undefined,
|
||||
status: undefined
|
||||
})
|
||||
const queryFormRef = ref()
|
||||
const formRef = ref()
|
||||
const selectionList = ref<any[]>([])
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await YVHgetProcessOperationPage(queryParams)
|
||||
list.value = res.list
|
||||
total.value = res.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields?.()
|
||||
queryParams.code = undefined
|
||||
queryParams.name = undefined
|
||||
queryParams.status = undefined
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const openForm = (type: 'create' | 'update', id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
const handleDelete = async (ids: number[]) => {
|
||||
if (!ids || ids.length === 0) return
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除选中的工序吗?', '提示', { type: 'warning' })
|
||||
for (const id of ids) {
|
||||
await YVHdeleteProcessOperation(id)
|
||||
}
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleSelectionChange = (rows: any[]) => {
|
||||
selectionList.value = rows
|
||||
}
|
||||
|
||||
/** 行点击操作 */
|
||||
const handleRowClick = (row: any, column: any, event: MouseEvent) => {
|
||||
// 检查是否点击了按钮、链接或其他交互元素
|
||||
const target = event.target as HTMLElement
|
||||
if (
|
||||
target.tagName === 'BUTTON' ||
|
||||
target.tagName === 'A' ||
|
||||
target.tagName === 'I' ||
|
||||
target.tagName === 'svg' ||
|
||||
target.closest('button') ||
|
||||
target.closest('a') ||
|
||||
target.closest('.el-button') ||
|
||||
target.closest('.el-checkbox')
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// 直接打开编辑页面
|
||||
openForm('update', row.id)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
302
src/views/mes/production/route/ProcessRouteForm.vue
Normal file
302
src/views/mes/production/route/ProcessRouteForm.vue
Normal file
@@ -0,0 +1,302 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:title="formType === 'create' ? '新增工序路线与工位设置' : '编辑工序路线与工位设置'"
|
||||
v-model="visible"
|
||||
width="800px"
|
||||
@close="handleClose"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px" v-loading="formLoading">
|
||||
<!-- 基本信息 -->
|
||||
<el-form-item label="路线编码" prop="code">
|
||||
<el-input v-model="form.code" placeholder="请输入工序路线编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="路线名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入工序路线名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="路线描述" prop="description">
|
||||
<el-input v-model="form.description" placeholder="请输入工序路线描述" type="textarea" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="form.status" placeholder="请选择状态">
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 工序列表 -->
|
||||
<el-divider>工序列表</el-divider>
|
||||
<div class="mb-10px">
|
||||
<el-button type="primary" @click="handleAddOperation">
|
||||
<Icon icon="ep:plus" class="mr-5px" />添加工序
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="form.operations" border>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column label="工序" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.operationId"
|
||||
placeholder="请选择工序"
|
||||
@change="handleOperationChange($event, row)"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in operationOptions"
|
||||
:key="item.id"
|
||||
:label="`${item.name} (${item.code})`"
|
||||
:value="item.id"
|
||||
>
|
||||
<span>{{ item.name }}</span>
|
||||
<span class="text-gray-400 ml-10px">({{ item.code }})</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="实际工时(分钟)" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-model.number="row.requiredTime"
|
||||
placeholder="输入分钟数"
|
||||
style="width: 100px"
|
||||
@blur="validateRequiredTime(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.remark" placeholder="请输入备注" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
:disabled="$index === 0"
|
||||
@click="moveOperation($index, 'up')"
|
||||
>
|
||||
<Icon icon="ep:arrow-up" />
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
:disabled="$index === form.operations.length - 1"
|
||||
@click="moveOperation($index, 'down')"
|
||||
>
|
||||
<Icon icon="ep:arrow-down" />
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="removeOperation($index)">
|
||||
<Icon icon="ep:delete" />
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="submitForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
YVHcreateProcessRoute,
|
||||
YVHupdateProcessRoute,
|
||||
YVHgetProcessRoute
|
||||
} from '@/api/mes/production/process-route'
|
||||
import { YVHgetProcessOperationList } from '@/api/mes/production/process-operation'
|
||||
import { Icon } from '@/components/Icon'
|
||||
|
||||
// 生成工序路线编码
|
||||
const generateRouteCode = () => {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(now.getDate()).padStart(2, '0')
|
||||
const random = Math.floor(Math.random() * 1000)
|
||||
.toString()
|
||||
.padStart(3, '0')
|
||||
return `RT${year}${month}${day}${random}`
|
||||
}
|
||||
|
||||
const emits = defineEmits(['success'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const formLoading = ref(false) // 表单加载状态
|
||||
const formType = ref<'create' | 'update'>('create')
|
||||
const operationOptions = ref<any[]>([])
|
||||
|
||||
const form = reactive<any>({
|
||||
operations: []
|
||||
})
|
||||
|
||||
const formRef = ref()
|
||||
|
||||
const rules = {
|
||||
code: [{ required: true, message: '请输入工序路线编码', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入工序路线名称', trigger: 'blur' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
|
||||
operations: [{ required: true, message: '请添加至少一个工序', trigger: 'change' }]
|
||||
}
|
||||
|
||||
// 获取工序选项
|
||||
const getOperationOptions = async () => {
|
||||
try {
|
||||
const res = await YVHgetProcessOperationList()
|
||||
operationOptions.value = res
|
||||
return res
|
||||
} catch (error) {
|
||||
console.error('获取工序列表失败:', error)
|
||||
ElMessage.error('获取工序列表失败')
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const open = async (type: 'create' | 'update', id?: number) => {
|
||||
formType.value = type
|
||||
visible.value = true
|
||||
formLoading.value = true // 开始加载,禁用表单
|
||||
|
||||
try {
|
||||
// 重置表单
|
||||
Object.assign(form, {
|
||||
code: type === 'create' ? generateRouteCode() : '',
|
||||
name: '',
|
||||
description: '',
|
||||
status: 1,
|
||||
remark: '',
|
||||
operations: []
|
||||
})
|
||||
|
||||
// 获取工序选项
|
||||
await getOperationOptions()
|
||||
|
||||
if (type === 'update' && id) {
|
||||
const res = await YVHgetProcessRoute(id)
|
||||
Object.assign(form, res)
|
||||
} else if (type === 'create' && operationOptions.value.length > 0) {
|
||||
// 创建时默认添加一个工序
|
||||
handleAddOperation()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('初始化表单失败:', error)
|
||||
ElMessage.error('加载数据失败,请重试')
|
||||
visible.value = false
|
||||
} finally {
|
||||
formLoading.value = false // 加载完成,启用表单
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddOperation = () => {
|
||||
// 找到一个尚未添加的工序
|
||||
const usedOperationIds = form.operations.map((op: any) => op.operationId)
|
||||
const availableOperation = operationOptions.value.find((op) => !usedOperationIds.includes(op.id))
|
||||
|
||||
const newOperation = {
|
||||
operationId: availableOperation?.id,
|
||||
operationCode: availableOperation?.code || '',
|
||||
operationName: availableOperation?.name || '',
|
||||
sequence: form.operations.length + 1,
|
||||
requiredTime: availableOperation?.standardTime || 30,
|
||||
remark: ''
|
||||
}
|
||||
|
||||
form.operations.push(newOperation)
|
||||
|
||||
// 如果选择了工序,自动填充相关信息
|
||||
if (availableOperation) {
|
||||
handleOperationChange(availableOperation.id, newOperation)
|
||||
}
|
||||
}
|
||||
|
||||
const handleOperationChange = (operationId: number, row: any) => {
|
||||
const operation = operationOptions.value.find((item) => item.id === operationId)
|
||||
if (operation) {
|
||||
row.operationCode = operation.code
|
||||
row.operationName = operation.name
|
||||
// 如果requiredTime为0或未设置,则使用标准工时
|
||||
if (!row.requiredTime) {
|
||||
row.requiredTime = operation.standardTime || 30
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const moveOperation = (index: number, direction: 'up' | 'down') => {
|
||||
const operations = form.operations
|
||||
if (direction === 'up' && index > 0) {
|
||||
;[operations[index], operations[index - 1]] = [operations[index - 1], operations[index]]
|
||||
} else if (direction === 'down' && index < operations.length - 1) {
|
||||
;[operations[index], operations[index + 1]] = [operations[index + 1], operations[index]]
|
||||
}
|
||||
// 更新序号
|
||||
operations.forEach((item: any, idx: number) => {
|
||||
item.sequence = idx + 1
|
||||
})
|
||||
}
|
||||
|
||||
const removeOperation = (index: number) => {
|
||||
form.operations.splice(index, 1)
|
||||
// 更新序号
|
||||
form.operations.forEach((item: any, idx: number) => {
|
||||
item.sequence = idx + 1
|
||||
})
|
||||
}
|
||||
|
||||
const submitForm = () => {
|
||||
if (form.operations.length === 0) {
|
||||
ElMessage.warning('请至少添加一个工序')
|
||||
return
|
||||
}
|
||||
|
||||
formRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
loading.value = true
|
||||
try {
|
||||
if (formType.value === 'create') {
|
||||
await YVHcreateProcessRoute(form)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await YVHupdateProcessRoute(form)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
visible.value = false
|
||||
emits('success')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 对外暴露 open 方法
|
||||
defineExpose({ open })
|
||||
|
||||
// 在script部分添加验证函数
|
||||
const validateRequiredTime = (row: any) => {
|
||||
// 确保输入的是有效的非负整数
|
||||
if (isNaN(row.requiredTime) || row.requiredTime < 0) {
|
||||
row.requiredTime = 30 // 默认值
|
||||
} else {
|
||||
// 确保是整数
|
||||
row.requiredTime = Math.floor(Number(row.requiredTime))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-divider {
|
||||
margin: 16px 0;
|
||||
}
|
||||
</style>
|
||||
206
src/views/mes/production/route/index.vue
Normal file
206
src/views/mes/production/route/index.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<doc-alert title="【MES】工序路线管理" url="https://doc.iocoder.cn/mes/process-route/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="路线编码" prop="code">
|
||||
<el-input
|
||||
v-model="queryParams.code"
|
||||
placeholder="请输入工序路线编码"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="路线名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入工序路线名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="openForm('create')"
|
||||
v-hasPermi="['mes:process-route:create']"
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
@click="handleDelete(selectionList.map((item) => item.id))"
|
||||
v-hasPermi="['mes:process-route:delete']"
|
||||
:disabled="selectionList.length === 0"
|
||||
>
|
||||
<Icon icon="ep:delete" class="mr-5px" /> 删除
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
:stripe="true"
|
||||
:show-overflow-tooltip="true"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-click="handleRowClick"
|
||||
>
|
||||
<el-table-column type="selection" width="50" />
|
||||
<el-table-column width="80" label="路线ID" align="center" prop="id" />
|
||||
<el-table-column min-width="120" label="路线编码" align="center" prop="code" />
|
||||
<el-table-column min-width="120" label="路线名称" align="center" prop="name" />
|
||||
<el-table-column min-width="180" label="路线描述" align="center" prop="description" />
|
||||
<el-table-column min-width="100" label="状态" align="center" prop="status">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
||||
{{ scope.row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column min-width="120" label="备注" align="center" prop="remark" />
|
||||
<el-table-column label="操作" align="center" fixed="right" width="120">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
v-hasPermi="['mes:process-route:update']"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete([scope.row.id])"
|
||||
v-hasPermi="['mes:process-route:delete']"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<ProcessRouteForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { YVHgetProcessRoutePage, YVHdeleteProcessRoute } from '@/api/mes/production/process-route'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import ProcessRouteForm from './ProcessRouteForm.vue'
|
||||
|
||||
const loading = ref(true)
|
||||
const list = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
code: undefined,
|
||||
name: undefined,
|
||||
status: undefined
|
||||
})
|
||||
const queryFormRef = ref()
|
||||
const formRef = ref()
|
||||
const selectionList = ref<any[]>([])
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await YVHgetProcessRoutePage(queryParams)
|
||||
list.value = res.list
|
||||
total.value = res.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields?.()
|
||||
queryParams.code = undefined
|
||||
queryParams.name = undefined
|
||||
queryParams.status = undefined
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const openForm = (type: 'create' | 'update', id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
const handleDelete = async (ids: number[]) => {
|
||||
if (!ids || ids.length === 0) return
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除选中的工序路线吗?', '提示', { type: 'warning' })
|
||||
for (const id of ids) {
|
||||
await YVHdeleteProcessRoute(id)
|
||||
}
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleSelectionChange = (rows: any[]) => {
|
||||
selectionList.value = rows
|
||||
}
|
||||
|
||||
/** 行点击操作 */
|
||||
const handleRowClick = (row: any, column: any, event: MouseEvent) => {
|
||||
// 检查是否点击了按钮、链接或其他交互元素
|
||||
const target = event.target as HTMLElement
|
||||
if (
|
||||
target.tagName === 'BUTTON' ||
|
||||
target.tagName === 'A' ||
|
||||
target.tagName === 'I' ||
|
||||
target.tagName === 'svg' ||
|
||||
target.closest('button') ||
|
||||
target.closest('a') ||
|
||||
target.closest('.el-button') ||
|
||||
target.closest('.el-checkbox')
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// 直接打开编辑页面
|
||||
openForm('update', row.id)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
409
src/views/mes/production/workorder/WorkOrderForm.vue
Normal file
409
src/views/mes/production/workorder/WorkOrderForm.vue
Normal file
@@ -0,0 +1,409 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:title="formType === 'create' ? '新增工单' : '编辑工单'"
|
||||
v-model="visible"
|
||||
width="900px"
|
||||
@close="handleClose"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px" v-loading="formLoading">
|
||||
<el-tabs v-model="activeTab">
|
||||
<!-- 基本信息 -->
|
||||
<el-tab-pane label="基本信息" name="basic">
|
||||
<el-form-item label="产品" prop="productId">
|
||||
<el-select
|
||||
v-model="form.productId"
|
||||
placeholder="请选择产品"
|
||||
class="!w-240px"
|
||||
@change="handleProductChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in productOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
>
|
||||
<span>{{ item.name }}</span>
|
||||
<span class="text-gray-400 ml-10px">({{ item.barCode }})</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="工单编号" prop="code">
|
||||
<el-input v-model="form.code" placeholder="请输入工单编号" />
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="班次" prop="shift">-->
|
||||
<!-- <el-input v-model="form.shift" placeholder="请输入班次" @change="handleShiftChange" />-->
|
||||
<!-- </el-form-item>-->
|
||||
<el-form-item label="工单名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入工单名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="工序路线" prop="routeId">
|
||||
<el-select
|
||||
v-model="form.routeId"
|
||||
placeholder="请选择工序路线"
|
||||
class="!w-240px"
|
||||
@change="handleRouteChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in routeOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
>
|
||||
<span>{{ item.name }}</span>
|
||||
<span class="text-gray-400 ml-10px">({{ item.code }})</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="计划数量" prop="planQuantity">
|
||||
<el-input-number
|
||||
v-model="form.planQuantity"
|
||||
:min="0.01"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
placeholder="请输入计划数量"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="优先级" prop="priority">-->
|
||||
<!-- <el-input-number-->
|
||||
<!-- v-model="form.priority"-->
|
||||
<!-- :min="1"-->
|
||||
<!-- :max="10"-->
|
||||
<!-- :precision="0"-->
|
||||
<!-- placeholder="请输入优先级(1-10)"-->
|
||||
<!-- />-->
|
||||
<!-- </el-form-item>-->
|
||||
<el-form-item label="计划时间" prop="planTime">
|
||||
<el-date-picker
|
||||
v-model="form.planTime"
|
||||
type="datetimerange"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
:default-time="[
|
||||
new Date(
|
||||
new Date().setHours(
|
||||
new Date().getHours(),
|
||||
new Date().getMinutes(),
|
||||
new Date().getSeconds()
|
||||
)
|
||||
),
|
||||
new Date(
|
||||
new Date().setHours(
|
||||
new Date().getHours(),
|
||||
new Date().getMinutes(),
|
||||
new Date().getSeconds()
|
||||
)
|
||||
)
|
||||
]"
|
||||
class="!w-380px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 工序信息 -->
|
||||
<el-tab-pane label="工序信息" name="process">
|
||||
<el-table :data="form.operations" border>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column label="工序编码" prop="operationCode" min-width="120" align="center" />
|
||||
<el-table-column label="工序名称" prop="operationName" min-width="120" align="center" />
|
||||
<el-table-column
|
||||
label="工时(分钟)"
|
||||
prop="requiredTime"
|
||||
min-width="120"
|
||||
align="center"
|
||||
/>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="submitForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import {
|
||||
YVHcreateWorkOrder,
|
||||
YVHupdateWorkOrder,
|
||||
YVHgetWorkOrder
|
||||
} from '@/api/mes/production/workorder'
|
||||
import { YVHgetProcessRoute, YVHgetProcessRouteList } from '@/api/mes/production/process-route'
|
||||
import { ProductApi } from '@/api/erp/product/product'
|
||||
|
||||
const emits = defineEmits(['success'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const formLoading = ref(false) // 表单加载状态
|
||||
const formType = ref<'create' | 'update'>('create')
|
||||
const activeTab = ref('basic')
|
||||
const formRef = ref()
|
||||
|
||||
// 产品选项
|
||||
const productOptions = ref<any[]>([])
|
||||
|
||||
// 工序路线选项
|
||||
const routeOptions = ref<any[]>([])
|
||||
|
||||
const form = reactive<any>({
|
||||
id: undefined,
|
||||
code: '',
|
||||
name: '',
|
||||
shift: '', // 添加班次字段
|
||||
productId: undefined,
|
||||
productCode: '',
|
||||
productName: '',
|
||||
routeId: undefined,
|
||||
routeName: '',
|
||||
planQuantity: 1,
|
||||
priority: 5,
|
||||
planTime: [],
|
||||
status: 0,
|
||||
remark: '',
|
||||
operations: []
|
||||
})
|
||||
|
||||
const rules = {
|
||||
code: [{ required: true, message: '请输入工单编号', trigger: 'blur' }],
|
||||
shift: [{ required: true, message: '请选择班次', trigger: 'change' }],
|
||||
name: [{ required: true, message: '请输入工单名称', trigger: 'blur' }],
|
||||
productId: [{ required: true, message: '请选择产品', trigger: 'change' }],
|
||||
routeId: [{ required: true, message: '请选择工序路线', trigger: 'change' }],
|
||||
planQuantity: [{ required: true, message: '请输入计划数量', trigger: 'blur' }],
|
||||
planTime: [{ required: true, message: '请选择计划时间', trigger: 'change' }]
|
||||
}
|
||||
|
||||
// 生成工单编号
|
||||
const generateOrderCode = (productCode: string = '') => {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(now.getDate()).padStart(2, '0')
|
||||
return `${productCode}${year}${month}${day}`
|
||||
}
|
||||
|
||||
const open = async (type: 'create' | 'update', id?: number) => {
|
||||
formType.value = type
|
||||
visible.value = true
|
||||
activeTab.value = 'basic'
|
||||
formLoading.value = true // 开始加载,禁用表单
|
||||
|
||||
try {
|
||||
// 重置表单
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
code: type === 'create' ? generateOrderCode() : '',
|
||||
name: '',
|
||||
shift: '', // 重置班次
|
||||
productId: undefined,
|
||||
productCode: '',
|
||||
productName: '',
|
||||
routeId: undefined,
|
||||
routeName: '',
|
||||
planQuantity: 1,
|
||||
priority: 5,
|
||||
planTime:
|
||||
type === 'create'
|
||||
? [new Date().setHours(8,0,0,0,), new Date(new Date().getTime() + 1 * 24 * 60 * 60 * 1000).setHours(8,0,0,0,)]
|
||||
: [],
|
||||
status: 0,
|
||||
remark: '',
|
||||
operations: []
|
||||
})
|
||||
|
||||
// 并行获取产品列表和工序路线列表
|
||||
const [products, routes] = await Promise.all([
|
||||
ProductApi.getProductSimpleList().catch((error) => {
|
||||
console.error('获取产品列表失败:', error)
|
||||
return []
|
||||
}),
|
||||
YVHgetProcessRouteList().catch((error) => {
|
||||
console.error('获取工序路线列表失败:', error)
|
||||
return []
|
||||
})
|
||||
])
|
||||
|
||||
// 过滤产品列表,只保留categoryName包含"成品"的产品
|
||||
productOptions.value = products
|
||||
? products.filter((product) => product.categoryName?.includes('成品'))
|
||||
: []
|
||||
routeOptions.value = routes || []
|
||||
|
||||
// 如果是编辑模式,获取工单详情
|
||||
if (type === 'update' && id) {
|
||||
const res = await YVHgetWorkOrder(id)
|
||||
Object.assign(form, {
|
||||
...res,
|
||||
id: res.id, // 确保id被正确设置
|
||||
planTime: [res.planStartTime, res.planEndTime]
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('初始化表单失败:', error)
|
||||
ElMessage.error('加载数据失败,请重试')
|
||||
visible.value = false
|
||||
} finally {
|
||||
formLoading.value = false // 加载完成,启用表单
|
||||
}
|
||||
}
|
||||
|
||||
const handleProductChange = async (productId: number) => {
|
||||
const product = productOptions.value.find((item) => item.id === productId)
|
||||
if (product) {
|
||||
form.productCode = product.barCode
|
||||
form.productName = product.name
|
||||
|
||||
// 根据产品名称设置工单编号前缀
|
||||
let productCode = 'X'
|
||||
if (product.name.includes('番茄')) {
|
||||
productCode = 'X8'
|
||||
}
|
||||
form.code = generateOrderCode(productCode)
|
||||
|
||||
// 更新工单名称
|
||||
const today = new Date()
|
||||
const dateStr = `${today.getFullYear()}${String(today.getMonth() + 1).padStart(2, '0')}${String(today.getDate()).padStart(2, '0')}`
|
||||
form.name = `${product.name}生产工单-${dateStr}`
|
||||
|
||||
// 尝试自动匹配与产品名称相关的工序路线
|
||||
if (routeOptions.value.length > 0) {
|
||||
// 首先尝试完全匹配产品名称
|
||||
const exactMatch = routeOptions.value.find((route) => route.name === product.name)
|
||||
if (exactMatch) {
|
||||
form.routeId = exactMatch.id
|
||||
handleRouteChange(exactMatch.id)
|
||||
return
|
||||
}
|
||||
|
||||
// 其次尝试包含产品名称的工序路线
|
||||
const containsMatch = routeOptions.value.find((route) => route.name.includes(product.name))
|
||||
if (containsMatch) {
|
||||
form.routeId = containsMatch.id
|
||||
handleRouteChange(containsMatch.id)
|
||||
return
|
||||
}
|
||||
|
||||
// 最后尝试产品名称包含工序路线名称的情况
|
||||
const reverseMatch = routeOptions.value.find((route) => product.name.includes(route.name))
|
||||
if (reverseMatch) {
|
||||
form.routeId = reverseMatch.id
|
||||
handleRouteChange(reverseMatch.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleRouteChange = async (routeId: number) => {
|
||||
if (!routeId) return
|
||||
|
||||
formLoading.value = true // 加载工序路线详情时禁用表单
|
||||
try {
|
||||
const route = await YVHgetProcessRoute(routeId)
|
||||
form.routeName = route.name
|
||||
form.operations = route.operations.map((item: any) => ({
|
||||
operationId: item.operationId,
|
||||
operationCode: item.operationCode,
|
||||
operationName: item.operationName,
|
||||
requiredTime: item.requiredTime,
|
||||
sequence: item.sequence
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('获取工序路线详情失败:', error)
|
||||
form.routeName = ''
|
||||
form.operations = []
|
||||
ElMessage.error('获取工序路线详情失败')
|
||||
} finally {
|
||||
formLoading.value = false // 加载完成,启用表单
|
||||
}
|
||||
}
|
||||
|
||||
const handleShiftChange = () => {
|
||||
if (form.productId && form.shift) {
|
||||
// 更新工单编号,加入班次信息
|
||||
let productCode = 'X'
|
||||
if (form.productName.includes('番茄')) {
|
||||
productCode = 'X8'
|
||||
}
|
||||
form.code = `${generateOrderCode(productCode)}${form.shift}`
|
||||
|
||||
// 更新工单名称
|
||||
const today = new Date()
|
||||
const dateStr = `${today.getFullYear()}${String(today.getMonth() + 1).padStart(2, '0')}${String(today.getDate()).padStart(2, '0')}`
|
||||
form.name = `${form.productName}生产工单-${dateStr}-${form.shift}`
|
||||
}
|
||||
}
|
||||
|
||||
const submitForm = () => {
|
||||
if (!form.operations.length) {
|
||||
ElMessage.warning('请选择工序路线')
|
||||
return
|
||||
}
|
||||
|
||||
formRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
loading.value = true
|
||||
try {
|
||||
const data = {
|
||||
id: form.id, // 编辑时需要
|
||||
code: form.code,
|
||||
name: form.name,
|
||||
shift: form.shift, // 添加班次
|
||||
productId: form.productId,
|
||||
productCode: form.productCode,
|
||||
productName: form.productName,
|
||||
planQuantity: form.planQuantity,
|
||||
priority: form.priority,
|
||||
routeId: form.routeId,
|
||||
routeName: form.routeName,
|
||||
planStartTime: form.planTime[0],
|
||||
planEndTime: form.planTime[1],
|
||||
status: form.status, // 使用表单中保存的状态
|
||||
remark: form.remark,
|
||||
operations: form.operations.map((op: any) => ({
|
||||
operationId: op.operationId,
|
||||
operationCode: op.operationCode,
|
||||
operationName: op.operationName,
|
||||
requiredTime: op.requiredTime,
|
||||
sequence: op.sequence
|
||||
// 不包含 remark 字段,因为后端 API 不支持
|
||||
}))
|
||||
}
|
||||
|
||||
if (formType.value === 'create') {
|
||||
data.status = 0 // 新建时固定为草稿状态
|
||||
await YVHcreateWorkOrder(data)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await YVHupdateWorkOrder(data) // 编辑时直接传入所有数据
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
visible.value = false
|
||||
emits('success')
|
||||
} catch (error) {
|
||||
console.error('保存工单失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-tabs :deep(.el-tabs__content) {
|
||||
padding: 20px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<el-dialog v-model="visibleProxy" title="报工单" width="800px" append-to-body>
|
||||
<el-form label-width="120px">
|
||||
<template v-if="Object.keys(detailProcessData).length > 0">
|
||||
<div class="mb-4">
|
||||
<div class="text-lg font-bold mb-2">工序参数</div>
|
||||
<template v-for="(field, key) in detailProcessData" :key="key">
|
||||
<el-form-item :label="field.label">
|
||||
<div class="text-gray-600">{{ formatDetailFieldValue(field) }}</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<el-form-item label="操作人">
|
||||
<div class="text-gray-600">{{
|
||||
currentDetailOperation?.currentExecution?.workerName || '-'
|
||||
}}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="设备">
|
||||
<div class="text-gray-600">{{
|
||||
currentDetailOperation?.currentExecution?.equipmentName || '-'
|
||||
}}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="开始时间">
|
||||
<div class="text-gray-600">{{
|
||||
currentDetailOperation?.currentExecution?.startTime || '-'
|
||||
}}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="结束时间">
|
||||
<div class="text-gray-600">{{
|
||||
currentDetailOperation?.currentExecution?.endTime || '-'
|
||||
}}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="投入数量">
|
||||
<div class="text-gray-600">{{
|
||||
currentDetailOperation?.currentExecution?.inputQuantity || '-'
|
||||
}}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="产出数量">
|
||||
<div class="text-gray-600">{{
|
||||
currentDetailOperation?.currentExecution?.outputQuantity || '-'
|
||||
}}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="合格数量">
|
||||
<div class="text-gray-600">{{
|
||||
currentDetailOperation?.currentExecution?.qualifiedQuantity || '-'
|
||||
}}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="不合格数量">
|
||||
<div class="text-gray-600">{{
|
||||
currentDetailOperation?.currentExecution?.unqualifiedQuantity || '-'
|
||||
}}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visibleProxy = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
currentDetailOperation: any
|
||||
detailProcessData: Record<string, any>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', value: boolean): void
|
||||
}>()
|
||||
|
||||
const visibleProxy = computed({
|
||||
get: () => props.visible,
|
||||
set: (val: boolean) => emit('update:visible', val)
|
||||
})
|
||||
|
||||
const formatDetailFieldValue = (field: any) => {
|
||||
if (!field || field.value === undefined) return '-'
|
||||
const { value, type, unit, options } = field
|
||||
if (type === 'select' && options) {
|
||||
const option = options.find((opt: any) => opt.value === value)
|
||||
return option ? option.label : value
|
||||
}
|
||||
if (type === 'number' && unit) {
|
||||
return `${value} ${unit}`
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.join('、')
|
||||
}
|
||||
return value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-lg {
|
||||
font-size: 16px;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.mb-2 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.text-gray-600 {
|
||||
color: #606266;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<el-card class="mb-4">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span>工序进度</span>
|
||||
<div class="text-gray-400"> 总进度:{{ progressData?.completionRate || 0 }}% </div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold">{{ progressData?.totalOperations || 0 }}</div>
|
||||
<div class="text-gray-400">总工序</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-success">{{
|
||||
progressData?.completedOperations || 0
|
||||
}}</div>
|
||||
<div class="text-gray-400">已完成</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-warning">{{
|
||||
progressData?.inProgressOperations || 0
|
||||
}}</div>
|
||||
<div class="text-gray-400">进行中</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-info">{{ progressData?.pendingOperations || 0 }}</div>
|
||||
<div class="text-gray-400">未开始</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface ProgressData {
|
||||
completionRate?: number
|
||||
totalOperations?: number
|
||||
completedOperations?: number
|
||||
inProgressOperations?: number
|
||||
pendingOperations?: number
|
||||
}
|
||||
|
||||
const props = defineProps<{ progressData: ProgressData | null }>()
|
||||
</script>
|
||||
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<el-dialog v-model="visibleProxy" title="进度记录历史" width="1000px" append-to-body>
|
||||
<el-table :data="progressHistoryList" border stripe>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
|
||||
<!-- 动态工序参数列 -->
|
||||
<template v-for="column in processDataColumns" :key="column.key">
|
||||
<el-table-column :label="column.label" :min-width="column.minWidth" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.processData">
|
||||
{{ formatProcessDataValue(JSON.parse(row.processData)[column.key]) }}
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
|
||||
<el-table-column label="备注" prop="remark" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="记录时间" prop="createTime" width="180" align="center" />
|
||||
|
||||
<!-- 操作列:编辑进度记录 -->
|
||||
<el-table-column label="操作" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="openEdit(row)"
|
||||
v-hasPermi="['mes:production-order:operations']"
|
||||
>编辑</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="visibleProxy = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 编辑进度记录弹窗 -->
|
||||
<el-dialog v-model="editDialogVisible" title="编辑进度记录" width="600px" append-to-body>
|
||||
<el-form ref="editFormRef" :model="editForm" label-width="120px">
|
||||
<template v-if="Object.keys(editFieldsConfig).length > 0">
|
||||
<div class="mb-6 bg-gray-50 p-4 rounded">
|
||||
<div class="text-base font-bold mb-4 text-gray-700">工序参数</div>
|
||||
<template v-for="(field, key) in editFieldsConfig" :key="key">
|
||||
<el-form-item :label="field.label" class="mb-4">
|
||||
<!-- 选择器类型 -->
|
||||
<template v-if="field.type === 'select'">
|
||||
<el-select
|
||||
v-model="editForm.processData[key]"
|
||||
:disabled="field.editable === false"
|
||||
:multiple="field.multiple"
|
||||
:placeholder="'请选择' + field.label"
|
||||
class="!w-[280px]"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in field.options || []"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<!-- 数字类型 -->
|
||||
<template v-else-if="field.type === 'number'">
|
||||
<div class="flex items-center">
|
||||
<el-input-number
|
||||
v-model="editForm.processData[key]"
|
||||
:disabled="field.editable === false"
|
||||
:min="field.rules?.min ?? 0"
|
||||
:max="field.rules?.max ?? Infinity"
|
||||
:step="field.rules?.step ?? 1"
|
||||
controls-position="right"
|
||||
class="!w-[180px]"
|
||||
/>
|
||||
<span v-if="field.unit" class="ml-2 text-gray-600">{{ field.unit }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 字符串类型 -->
|
||||
<template v-else>
|
||||
<el-input
|
||||
v-model="editForm.processData[key]"
|
||||
:disabled="field.editable === false"
|
||||
:placeholder="field.placeholder || '请输入' + field.label"
|
||||
class="!w-[280px]"
|
||||
/>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="editForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
class="!w-[380px]"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitEdit" :loading="editSubmitting">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { YVHupdateOperationProgress } from '@/api/mes/production/order-operation'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
progressHistoryList: any[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'updated'): void
|
||||
}>()
|
||||
|
||||
const visibleProxy = computed({
|
||||
get: () => props.visible,
|
||||
set: (val: boolean) => emit('update:visible', val)
|
||||
})
|
||||
|
||||
const processDataColumns = computed(() => {
|
||||
if (!props.progressHistoryList?.length)
|
||||
return [] as Array<{ label: string; key: string; minWidth: string }>
|
||||
const firstRecord = props.progressHistoryList[0]
|
||||
if (!firstRecord?.processData) return []
|
||||
const processData = JSON.parse(firstRecord.processData) as Record<
|
||||
string,
|
||||
{ label: string; order?: number; [k: string]: any }
|
||||
>
|
||||
const entries = Object.entries(processData) as Array<
|
||||
[string, { label: string; order?: number; [k: string]: any }]
|
||||
>
|
||||
return entries
|
||||
.sort((a, b) => (a[1].order ?? 0) - (b[1].order ?? 0))
|
||||
.map(([key, field]) => ({ label: field.label, key, minWidth: '120' }))
|
||||
})
|
||||
|
||||
const formatProcessDataValue = (field: any) => {
|
||||
if (!field) return ''
|
||||
if (field.type === 'select' && field.options) {
|
||||
if (Array.isArray(field.value)) {
|
||||
return field.value
|
||||
.map((v: any) => field.options.find((opt: any) => opt.value === v)?.label || v)
|
||||
.join('、')
|
||||
} else {
|
||||
const option = field.options.find((opt: any) => opt.value === field.value)
|
||||
return option ? option.label : field.value
|
||||
}
|
||||
}
|
||||
if (field.type === 'number' && field.unit) {
|
||||
return `${field.value}${field.unit}`
|
||||
}
|
||||
return field.value
|
||||
}
|
||||
|
||||
// 编辑相关
|
||||
const editDialogVisible = ref(false)
|
||||
const editSubmitting = ref(false)
|
||||
const editFormRef = ref()
|
||||
const editingRecordId = ref<number | null>(null)
|
||||
const editFieldsConfig = ref<Record<string, any>>({})
|
||||
const editForm = reactive({
|
||||
processData: {} as Record<string, any>,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const openEdit = (row: any) => {
|
||||
editingRecordId.value = row.id
|
||||
editForm.remark = row.remark || ''
|
||||
const parsed = row.processData ? JSON.parse(row.processData) : {}
|
||||
editFieldsConfig.value = parsed
|
||||
editForm.processData = Object.entries(parsed).reduce(
|
||||
(acc: Record<string, any>, [key, field]: [string, any]) => {
|
||||
acc[key] = field?.value ?? ''
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
const submitEdit = async () => {
|
||||
if (!editingRecordId.value) return
|
||||
try {
|
||||
editSubmitting.value = true
|
||||
const processDataWithConfig = Object.entries(editForm.processData).reduce(
|
||||
(acc, [key, value]) => {
|
||||
const fieldConfig = editFieldsConfig.value[key]
|
||||
acc[key] = {
|
||||
...fieldConfig,
|
||||
value
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, any>
|
||||
)
|
||||
|
||||
await YVHupdateOperationProgress({
|
||||
id: editingRecordId.value,
|
||||
processData: JSON.stringify(processDataWithConfig),
|
||||
remark: editForm.remark
|
||||
})
|
||||
|
||||
ElMessage.success('更新成功')
|
||||
editDialogVisible.value = false
|
||||
emit('updated')
|
||||
} catch (error) {
|
||||
console.error('更新进度记录失败:', error)
|
||||
ElMessage.error('更新失败')
|
||||
} finally {
|
||||
editSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<el-dialog v-model="visibleProxy" title="开始工序" width="500px" append-to-body>
|
||||
<el-form ref="startFormRef" :model="startForm" :rules="startRules" label-width="100px">
|
||||
<el-form-item label="操作工人" prop="workerName">
|
||||
<el-input
|
||||
v-model="startForm.workerName"
|
||||
placeholder="请输入操作工人姓名"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="设备" prop="equipmentId">
|
||||
<el-select v-model="startForm.equipmentId" placeholder="请选择设备" class="!w-240px">
|
||||
<el-option
|
||||
v-for="item in equipmentOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="生产数量" prop="inputQuantity">
|
||||
<el-input-number
|
||||
v-model="startForm.inputQuantity"
|
||||
:min="0.01"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="startForm.remark" type="textarea" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visibleProxy = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
interface EquipmentOption {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
equipmentOptions: EquipmentOption[]
|
||||
maxQuantity?: number
|
||||
defaultInputQuantity?: number
|
||||
submitting?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(
|
||||
e: 'submit',
|
||||
payload: {
|
||||
workerName: string
|
||||
equipmentId?: number
|
||||
equipmentName?: string
|
||||
inputQuantity: number
|
||||
remark: string
|
||||
}
|
||||
): void
|
||||
}>()
|
||||
|
||||
const visibleProxy = computed({
|
||||
get: () => props.visible,
|
||||
set: (val: boolean) => emit('update:visible', val)
|
||||
})
|
||||
|
||||
const startFormRef = ref()
|
||||
const startForm = reactive({
|
||||
workerName: '',
|
||||
equipmentId: undefined as number | undefined,
|
||||
equipmentName: '',
|
||||
inputQuantity: 1,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const startRules = {
|
||||
workerName: [{ required: true, message: '请输入操作工人姓名', trigger: 'blur' }],
|
||||
inputQuantity: [{ required: true, message: '请输入投入数量', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// 当前登录用户,用于默认填充工人姓名
|
||||
const userStore = useUserStore()
|
||||
const currentNickname = computed(() => userStore.user?.nickname || '')
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
// reset form when open
|
||||
startForm.workerName = currentNickname.value || ''
|
||||
startForm.equipmentId = undefined
|
||||
startForm.equipmentName = ''
|
||||
startForm.inputQuantity = props.defaultInputQuantity ?? 1
|
||||
startForm.remark = ''
|
||||
// 清理校验
|
||||
startFormRef.value?.clearValidate?.()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const handleSubmit = () => {
|
||||
startFormRef.value?.validate((valid: boolean) => {
|
||||
if (!valid) return
|
||||
// derive equipmentName
|
||||
const eq = props.equipmentOptions?.find((e) => e.id === startForm.equipmentId)
|
||||
const equipmentName = eq ? eq.name : ''
|
||||
emit('submit', {
|
||||
workerName: startForm.workerName,
|
||||
equipmentId: startForm.equipmentId,
|
||||
equipmentName,
|
||||
inputQuantity: startForm.inputQuantity,
|
||||
remark: startForm.remark
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
700
src/views/mes/production/workorder/index.vue
Normal file
700
src/views/mes/production/workorder/index.vue
Normal file
@@ -0,0 +1,700 @@
|
||||
<template>
|
||||
<doc-alert title="【MES】生产工单管理" url="https://doc.iocoder.cn/mes/workorder/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="84px"
|
||||
>
|
||||
<el-form-item label="工单编号" prop="code">
|
||||
<el-input
|
||||
v-model="queryParams.code"
|
||||
placeholder="请输入工单编号"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品名称" prop="productName">
|
||||
<el-input
|
||||
v-model="queryParams.productName"
|
||||
placeholder="请输入产品名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="工单状态" prop="status">
|
||||
<el-select
|
||||
v-model="queryParams.status"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
multiple
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option label="草稿" :value="0" />
|
||||
<el-option label="待审核" :value="1" />
|
||||
<el-option label="已审核" :value="2" />
|
||||
<el-option label="生产中" :value="3" />
|
||||
<el-option label="已完成" :value="4" />
|
||||
<el-option label="已关闭" :value="5" />
|
||||
<el-option label="已取消" :value="6" />
|
||||
<el-option label="已入库" :value="7" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="计划日期" prop="planTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.planTime"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="openForm('create')"
|
||||
v-hasPermi="['mes:production-order:create']"
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
@click="handleDelete(selectionList.map((item) => item.id))"
|
||||
v-hasPermi="['mes:production-order:delete']"
|
||||
:disabled="selectionList.length === 0"
|
||||
>
|
||||
<Icon icon="ep:delete" class="mr-5px" /> 删除
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
:stripe="true"
|
||||
:show-overflow-tooltip="true"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-click="handleRowClick"
|
||||
>
|
||||
<el-table-column type="selection" width="50" />
|
||||
<el-table-column label="批次信息" align="center" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.code }}</div>
|
||||
<div class="mt-5px">
|
||||
<el-tag :type="getStatusType(row.status)">{{ getStatusText(row.status) }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="产品信息" align="center" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.productName }}</div>
|
||||
<div class="text-gray-400 text-sm">{{ row.productCode }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="生产数量" align="center" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<div>计划:{{ row.planQuantity }}</div>
|
||||
<div class="mt-5px text-gray-400">完成:{{ row.producedQuantity || 0 }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="计划时间" align="center" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<el-tooltip
|
||||
:content="formatFullDateTime(row.planStartTime)"
|
||||
placement="top"
|
||||
:disabled="!row.planStartTime"
|
||||
>
|
||||
<span>开始:{{ formatDateTime(row.planStartTime) }}</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="mt-5px">
|
||||
<el-tooltip
|
||||
:content="formatFullDateTime(row.planEndTime)"
|
||||
placement="top"
|
||||
:disabled="!row.planEndTime"
|
||||
>
|
||||
<span>结束:{{ formatDateTime(row.planEndTime) }}</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="实际时间" align="center" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<el-tooltip
|
||||
:content="formatFullDateTime(row.actualStartTime)"
|
||||
placement="top"
|
||||
:disabled="!row.actualStartTime"
|
||||
>
|
||||
<span
|
||||
>开始:{{
|
||||
row.actualStartTime ? formatDateTime(row.actualStartTime) : '未开始'
|
||||
}}</span
|
||||
>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="mt-5px">
|
||||
<el-tooltip
|
||||
:content="formatFullDateTime(row.actualEndTime)"
|
||||
placement="top"
|
||||
:disabled="!row.actualEndTime"
|
||||
>
|
||||
<span
|
||||
>结束:{{ row.actualEndTime ? formatDateTime(row.actualEndTime) : '未结束' }}</span
|
||||
>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="备注"
|
||||
align="center"
|
||||
prop="remark"
|
||||
min-width="150"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="操作" align="center" fixed="right" width="200">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="编辑" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
v-hasPermi="['mes:production-order:update']"
|
||||
v-if="scope.row.status === 0"
|
||||
>
|
||||
<!-- <Icon icon="ep:edit" />-->
|
||||
编辑
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="开始生产" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleStart(scope.row.id)"
|
||||
v-if="scope.row.status === 2"
|
||||
v-hasPermi="['mes:production-order:start']"
|
||||
>
|
||||
<!-- <Icon icon="ep:video-play" />-->
|
||||
开始生产
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="完成" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="success"
|
||||
@click="handleComplete(scope.row)"
|
||||
v-if="scope.row.status === 3"
|
||||
v-hasPermi="['mes:production-order:complete']"
|
||||
>
|
||||
<!-- <Icon icon="ep:check" />-->
|
||||
完成
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="取消" placement="top">
|
||||
<!-- <el-button-->
|
||||
<!-- link-->
|
||||
<!-- type="danger"-->
|
||||
<!-- @click="handleCancel(scope.row.id)"-->
|
||||
<!-- v-if="[0, 1, 2].includes(scope.row.status)"-->
|
||||
<!-- v-hasPermi="['mes:production-order:cancel']"-->
|
||||
<!-- >-->
|
||||
<!--<!– <Icon icon="ep:close" />–>-->
|
||||
<!-- 取消-->
|
||||
<!-- </el-button>-->
|
||||
</el-tooltip>
|
||||
<el-tooltip content="提交审核" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleSubmit(scope.row.id)"
|
||||
v-if="scope.row.status === 0"
|
||||
v-hasPermi="['mes:production-order:submit']"
|
||||
>
|
||||
<!-- <Icon icon="ep:upload" />-->
|
||||
提交
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<!-- <el-tooltip content="审核通过" placement="top">-->
|
||||
<!-- <el-button-->
|
||||
<!-- link-->
|
||||
<!-- type="success"-->
|
||||
<!-- @click="handleApprove(scope.row.id)"-->
|
||||
<!-- v-if="scope.row.status === 1"-->
|
||||
<!-- v-hasPermi="['mes:production-order:approve']"-->
|
||||
<!-- >-->
|
||||
<!--<!– <Icon icon="ep:circle-check" />–>-->
|
||||
<!-- 审核-->
|
||||
<!-- </el-button>-->
|
||||
<!-- </el-tooltip>-->
|
||||
<!-- <el-tooltip content="拒绝" placement="top">-->
|
||||
<!-- <el-button-->
|
||||
<!-- link-->
|
||||
<!-- type="danger"-->
|
||||
<!-- @click="handleReject(scope.row.id)"-->
|
||||
<!-- v-if="scope.row.status === 1"-->
|
||||
<!-- v-hasPermi="['mes:production-order:reject']"-->
|
||||
<!-- >-->
|
||||
<!--<!– <Icon icon="ep:circle-close" />–>-->
|
||||
<!-- 拒绝-->
|
||||
<!-- </el-button>-->
|
||||
<!-- </el-tooltip>-->
|
||||
<!-- <el-tooltip content="关闭" placement="top">-->
|
||||
<!-- <el-button-->
|
||||
<!-- link-->
|
||||
<!-- type="danger"-->
|
||||
<!-- @click="handleClose(scope.row.id)"-->
|
||||
<!-- v-if="scope.row.status === 4"-->
|
||||
<!-- v-hasPermi="['mes:production-order:close']"-->
|
||||
<!-- >-->
|
||||
<!-- <Icon icon="ep:switch" />-->
|
||||
<!-- </el-button>-->
|
||||
<!-- </el-tooltip>-->
|
||||
<!-- 入库功能已停用,按钮隐藏 -->
|
||||
<!-- <el-tooltip content="入库" placement="top">-->
|
||||
<!-- <el-button-->
|
||||
<!-- link-->
|
||||
<!-- type="success"-->
|
||||
<!-- @click="handleProductionIn(scope.row)"-->
|
||||
<!-- v-if="false && scope.row.status === 4"-->
|
||||
<!-- v-hasPermi="['erp:production-in:create']"-->
|
||||
<!-- >-->
|
||||
<!-- <Icon icon="ep:box" />-->
|
||||
<!-- </el-button>-->
|
||||
<!-- </el-tooltip>-->
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete([scope.row.id])"
|
||||
v-if="[0, 6].includes(scope.row.status)"
|
||||
v-hasPermi="['mes:production-order:delete']"
|
||||
>
|
||||
<!-- <Icon icon="ep:delete" />-->
|
||||
删除
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="工序执行" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleShowOperations(scope.row.id, scope.row.status)"
|
||||
v-hasPermi="['mes:production-order:operations']"
|
||||
>
|
||||
<!-- <Icon icon="ep:list" />-->
|
||||
详情
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<WorkOrderForm ref="formRef" @success="getList" />
|
||||
|
||||
<!-- 工序执行弹窗 -->
|
||||
<OperationExecutionDialog ref="operationExecutionRef" />
|
||||
|
||||
<!-- 生产入库弹窗 -->
|
||||
<el-dialog v-model="inboundDialogVisible" title="生产入库" width="520px" append-to-body>
|
||||
<el-form label-width="120px">
|
||||
<el-form-item label="选择仓库">
|
||||
<el-select
|
||||
v-model="selectedWarehouseId"
|
||||
placeholder="请选择或输入仓库编号"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
clearable
|
||||
class="!w-320px"
|
||||
>
|
||||
<el-option
|
||||
v-for="w in warehouseOptions"
|
||||
:key="w.id"
|
||||
:label="w.name + '(' + w.id + ')'"
|
||||
:value="w.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品条码">
|
||||
<span>{{
|
||||
(currentInboundRow &&
|
||||
(currentInboundRow.productCode ||
|
||||
currentInboundRow.productBarCode ||
|
||||
currentInboundRow.barCode)) ||
|
||||
'-'
|
||||
}}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="入库数量">
|
||||
<span>{{
|
||||
(currentInboundRow &&
|
||||
(currentInboundRow.producedQuantity || currentInboundRow.planQuantity)) ||
|
||||
0
|
||||
}}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="inboundDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="inboundSubmitting" @click="confirmInbound"
|
||||
>确定</el-button
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import WorkOrderForm from './WorkOrderForm.vue'
|
||||
import OperationExecutionDialog from './components/OperationExecutionDialog.vue'
|
||||
import { WarehouseApi } from '@/api/erp/stock/warehouse'
|
||||
import {
|
||||
YVHgetWorkOrderPage,
|
||||
YVHdeleteWorkOrder,
|
||||
YVHstartWorkOrder,
|
||||
YVHcompleteWorkOrder,
|
||||
YVHcancelWorkOrder,
|
||||
YVHsubmitWorkOrder,
|
||||
YVHapproveWorkOrder,
|
||||
YVHrejectWorkOrder,
|
||||
YVHcloseWorkOrder
|
||||
} from '@/api/mes/production/workorder'
|
||||
import { YVHgetWorkOrderOperationList } from '@/api/mes/production/order-operation'
|
||||
import request from '@/config/axios'
|
||||
|
||||
// 隐藏旧的 ERP 入库逻辑,改为调用 MES 入库(按产品条码)接口
|
||||
|
||||
const loading = ref(true)
|
||||
const total = ref(0)
|
||||
const list = ref<any[]>([])
|
||||
const queryFormRef = ref()
|
||||
const formRef = ref()
|
||||
const operationExecutionRef = ref()
|
||||
const selectionList = ref<any[]>([])
|
||||
// 入库弹窗相关
|
||||
const inboundDialogVisible = ref(false)
|
||||
const inboundSubmitting = ref(false)
|
||||
const warehouseOptions = ref<Array<{ id: number; name: string }>>([])
|
||||
const selectedWarehouseId = ref<string | number | undefined>(undefined)
|
||||
const currentInboundRow = ref<any>(null)
|
||||
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
code: undefined,
|
||||
productName: undefined,
|
||||
status: [] as number[],
|
||||
planTime: [] as string[]
|
||||
})
|
||||
|
||||
const getStatusType = (status: number) => {
|
||||
switch (status) {
|
||||
case 0: // 草稿
|
||||
return 'warning' // 橙色
|
||||
case 1: // 待审核
|
||||
return 'primary' // 蓝色
|
||||
case 2: // 已审核
|
||||
return 'success' // 绿色
|
||||
case 3: // 生产中
|
||||
return 'warning' // 浅蓝色
|
||||
case 4: // 已完成
|
||||
return 'success' // 绿色
|
||||
case 5: // 已关闭
|
||||
return 'danger' // 红色
|
||||
case 6: // 已取消
|
||||
return 'warning' // 橙色
|
||||
case 7: // 已入库
|
||||
return 'success' // 绿色
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusText = (status: number) => {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return '草稿'
|
||||
case 1:
|
||||
return '待审核'
|
||||
case 2:
|
||||
return '已审核'
|
||||
case 3:
|
||||
return '生产中'
|
||||
case 4:
|
||||
return '已完成'
|
||||
case 5:
|
||||
return '已关闭'
|
||||
case 6:
|
||||
return '已取消'
|
||||
case 7:
|
||||
return '已入库'
|
||||
default:
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 处理时间范围参数
|
||||
const params = {
|
||||
...queryParams,
|
||||
planTime: queryParams.planTime
|
||||
? queryParams.planTime.map((date: string, index: number) => {
|
||||
// 开始时间设置为当天 00:00:00,结束时间设置为当天 23:59:59
|
||||
return index === 0 ? `${date} 00:00:00` : `${date} 23:59:59`
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
const res = await YVHgetWorkOrderPage(params)
|
||||
list.value = res.list
|
||||
total.value = res.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields()
|
||||
queryParams.code = undefined
|
||||
queryParams.productName = undefined
|
||||
queryParams.status = []
|
||||
queryParams.planTime = []
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const openForm = (type: 'create' | 'update', id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
const handleDelete = async (ids: number[]) => {
|
||||
if (!ids || ids.length === 0) return
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除选中的工单吗?', '提示', { type: 'warning' })
|
||||
for (const id of ids) {
|
||||
await YVHdeleteWorkOrder(id)
|
||||
}
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleStart = async (id: number) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要开始生产该工单吗?', '提示', { type: 'warning' })
|
||||
await YVHstartWorkOrder({ id })
|
||||
ElMessage.success('操作成功')
|
||||
getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleComplete = async (row: any) => {
|
||||
try {
|
||||
const result = await ElMessageBox.prompt('请输入实际完成数量', '完成工单', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputType: 'number',
|
||||
inputValue: '',
|
||||
inputValidator: (value) => {
|
||||
if (value === undefined || value === null || String(value).trim() === '') {
|
||||
return '实际完成数量不能为空'
|
||||
}
|
||||
const num = Number(value)
|
||||
// if (!Number.isFinite(num) || !Number.isInteger(num)) {
|
||||
// return '请输入整数'
|
||||
// }
|
||||
if (num < 0) {
|
||||
return '实际完成数量不能小于 0'
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
const producedQuantity = Number(result.value)
|
||||
await YVHcompleteWorkOrder({ id: row.id, producedQuantity })
|
||||
ElMessage.success('操作成功')
|
||||
getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleCancel = async (id: number) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要取消该工单吗?', '提示', { type: 'warning' })
|
||||
await YVHcancelWorkOrder({ id })
|
||||
ElMessage.success('操作成功')
|
||||
getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleSubmit = async (id: number) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要提交该工单审核吗?', '提示', { type: 'warning' })
|
||||
await YVHsubmitWorkOrder({ id })
|
||||
ElMessage.success('提交成功')
|
||||
getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleApprove = async (id: number) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要审核通过该工单吗?', '提示', { type: 'warning' })
|
||||
await YVHapproveWorkOrder({ id })
|
||||
ElMessage.success('审核通过')
|
||||
getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleReject = async (id: number) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要拒绝该工单吗?', '提示', { type: 'warning' })
|
||||
await YVHrejectWorkOrder({ id })
|
||||
ElMessage.success('已拒绝')
|
||||
getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleClose = async (id: number) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要关闭该工单吗?', '提示', { type: 'warning' })
|
||||
await YVHcloseWorkOrder({ id })
|
||||
ElMessage.success('已关闭')
|
||||
getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleProductionIn = async (row: any) => {
|
||||
try {
|
||||
currentInboundRow.value = row
|
||||
selectedWarehouseId.value = undefined
|
||||
// 加载仓库下拉
|
||||
const list = await WarehouseApi.getWarehouseSimpleList()
|
||||
warehouseOptions.value = Array.isArray(list) ? list : list?.data || []
|
||||
inboundDialogVisible.value = true
|
||||
} catch (error) {
|
||||
console.error('加载仓库列表失败:', error)
|
||||
// 即使加载失败,也允许用户手动输入
|
||||
inboundDialogVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const confirmInbound = async () => {
|
||||
if (!currentInboundRow.value) return
|
||||
// 优先选择下拉的仓库ID,否则使用手动输入
|
||||
const chosenId = selectedWarehouseId.value
|
||||
const warehouseId = Number(chosenId)
|
||||
if (!Number.isInteger(warehouseId) || warehouseId <= 0) {
|
||||
ElMessage.error('请选择或输入合法的仓库编号')
|
||||
return
|
||||
}
|
||||
const barCode =
|
||||
currentInboundRow.value.productCode ||
|
||||
currentInboundRow.value.productBarCode ||
|
||||
currentInboundRow.value.barCode
|
||||
if (!barCode) {
|
||||
ElMessage.error('缺少产品条码,无法入库')
|
||||
return
|
||||
}
|
||||
const count = currentInboundRow.value.producedQuantity || currentInboundRow.value.planQuantity
|
||||
inboundSubmitting.value = true
|
||||
try {
|
||||
await request.post({
|
||||
url: '/mes/stock/inbound',
|
||||
data: {
|
||||
barCode,
|
||||
warehouseId,
|
||||
count,
|
||||
workOrderId: currentInboundRow.value.id
|
||||
}
|
||||
})
|
||||
ElMessage.success('入库成功')
|
||||
inboundDialogVisible.value = false
|
||||
} catch (error) {
|
||||
console.error('入库失败:', error)
|
||||
ElMessage.error('入库失败')
|
||||
} finally {
|
||||
inboundSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleShowOperations = async (id: number, status: number) => {
|
||||
console.log('显示工序执行弹窗,工单ID:', id, '工单状态:', status)
|
||||
if (operationExecutionRef.value) {
|
||||
operationExecutionRef.value.open(id, status)
|
||||
} else {
|
||||
console.error('operationExecutionRef 未定义')
|
||||
ElMessage.error('无法打开工序执行弹窗')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectionList.value = selection
|
||||
}
|
||||
|
||||
/** 行点击操作 */
|
||||
const handleRowClick = (row: any, column: any, event: MouseEvent) => {
|
||||
// 检查是否点击了按钮、链接或其他交互元素
|
||||
const target = event.target as HTMLElement
|
||||
if (
|
||||
target.tagName === 'BUTTON' ||
|
||||
target.tagName === 'A' ||
|
||||
target.tagName === 'I' ||
|
||||
target.tagName === 'svg' ||
|
||||
target.closest('button') ||
|
||||
target.closest('a') ||
|
||||
target.closest('.el-button') ||
|
||||
target.closest('.el-checkbox')
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// 草稿状态打开编辑页面,其他状态打开工序执行页面
|
||||
if (row.status === 0) {
|
||||
openForm('update', row.id)
|
||||
} else {
|
||||
handleShowOperations(row.id, row.status)
|
||||
}
|
||||
}
|
||||
|
||||
const formatDateTime = (time: string) => {
|
||||
if (!time) return ''
|
||||
return time.split('T')[0]
|
||||
}
|
||||
|
||||
const formatFullDateTime = (time: string) => {
|
||||
if (!time) return ''
|
||||
return time.replace('T', ' ')
|
||||
}
|
||||
|
||||
getList()
|
||||
</script>
|
||||
185
src/views/mes/production/workstation/WorkstationBindingForm.vue
Normal file
185
src/views/mes/production/workstation/WorkstationBindingForm.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:title="formType === 'create' ? '新增工位绑定' : '编辑工位绑定'"
|
||||
v-model="visible"
|
||||
width="500px"
|
||||
@close="handleClose"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px" v-loading="formLoading">
|
||||
<el-form-item label="工位编号" prop="code">
|
||||
<el-input v-model="form.code" placeholder="请输入工位编号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="工位名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入工位名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="绑定工序" prop="processId">
|
||||
<el-select
|
||||
v-model="form.processId"
|
||||
placeholder="请选择绑定工序"
|
||||
filterable
|
||||
clearable
|
||||
class="!w-full"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in processList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="报工人" prop="reporterId">
|
||||
<el-select
|
||||
v-model="form.reporterId"
|
||||
placeholder="请选择报工人"
|
||||
filterable
|
||||
clearable
|
||||
class="!w-full"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in userList"
|
||||
:key="item.id"
|
||||
:label="item.nickname"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="form.status" placeholder="请选择状态">
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" :rows="3" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="submitForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
YVHcreateWorkstationBinding,
|
||||
YVHupdateWorkstationBinding,
|
||||
YVHgetWorkstationBinding
|
||||
} from '@/api/mes/production/workstation'
|
||||
import { YVHgetProcessOperationList } from '@/api/mes/production/process-operation'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
|
||||
// 生成工位编号
|
||||
const generateWorkstationCode = () => {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(now.getDate()).padStart(2, '0')
|
||||
const random = Math.floor(Math.random() * 1000)
|
||||
.toString()
|
||||
.padStart(3, '0')
|
||||
return `WS${year}${month}${day}${random}`
|
||||
}
|
||||
|
||||
const emits = defineEmits(['success'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const formLoading = ref(false)
|
||||
const formType = ref<'create' | 'update'>('create')
|
||||
const form = reactive<any>({})
|
||||
const formRef = ref()
|
||||
const processList = ref<any[]>([])
|
||||
const userList = ref<any[]>([])
|
||||
|
||||
const rules = {
|
||||
code: [{ required: true, message: '请输入工位编号', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入工位名称', trigger: 'blur' }],
|
||||
processId: [{ required: true, message: '请选择绑定工序', trigger: 'change' }],
|
||||
reporterId: [{ required: true, message: '请选择报工人', trigger: 'change' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
|
||||
}
|
||||
|
||||
// 获取工序列表
|
||||
const getProcessList = async () => {
|
||||
try {
|
||||
processList.value = await YVHgetProcessOperationList()
|
||||
} catch (error) {
|
||||
console.error('获取工序列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
const getUserList = async () => {
|
||||
try {
|
||||
const res = await UserApi.getSimpleUserList()
|
||||
userList.value = res
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const open = async (type: 'create' | 'update', id?: number) => {
|
||||
formType.value = type
|
||||
visible.value = true
|
||||
formLoading.value = true
|
||||
|
||||
try {
|
||||
// 获取下拉列表数据
|
||||
await Promise.all([getProcessList(), getUserList()])
|
||||
|
||||
// 重置表单
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
code: type === 'create' ? generateWorkstationCode() : '',
|
||||
name: '',
|
||||
processId: undefined,
|
||||
reporterId: undefined,
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
if (type === 'update' && id) {
|
||||
const res = await YVHgetWorkstationBinding(id)
|
||||
Object.assign(form, res)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('初始化表单失败:', error)
|
||||
ElMessage.error('加载数据失败,请重试')
|
||||
visible.value = false
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const submitForm = () => {
|
||||
formRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
loading.value = true
|
||||
try {
|
||||
if (formType.value === 'create') {
|
||||
await YVHcreateWorkstationBinding(form)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await YVHupdateWorkstationBinding(form)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
visible.value = false
|
||||
emits('success')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 对外暴露 open 方法
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
210
src/views/mes/production/workstation/index.vue
Normal file
210
src/views/mes/production/workstation/index.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<doc-alert title="【MES】工位绑定" url="https://doc.iocoder.cn/mes/workstation-binding/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="工位编号" prop="code">
|
||||
<el-input
|
||||
v-model="queryParams.code"
|
||||
placeholder="请输入工位编号"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="工位名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入工位名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="openForm('create')"
|
||||
v-hasPermi="['mes:workstation-binding:create']"
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
@click="handleDelete(selectionList.map((item) => item.id))"
|
||||
v-hasPermi="['mes:workstation-binding:delete']"
|
||||
:disabled="selectionList.length === 0"
|
||||
>
|
||||
<Icon icon="ep:delete" class="mr-5px" /> 删除
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
:stripe="true"
|
||||
:show-overflow-tooltip="true"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-click="handleRowClick"
|
||||
>
|
||||
<el-table-column type="selection" width="50" />
|
||||
<el-table-column width="80" label="ID" align="center" prop="id" />
|
||||
<el-table-column min-width="120" label="工位编号" align="center" prop="code" />
|
||||
<el-table-column min-width="120" label="工位名称" align="center" prop="name" />
|
||||
<el-table-column min-width="150" label="绑定工序" align="center" prop="processName" />
|
||||
<el-table-column min-width="120" label="报工人" align="center" prop="reporterName" />
|
||||
<el-table-column min-width="100" label="状态" align="center" prop="status">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
||||
{{ scope.row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column min-width="150" label="备注" align="center" prop="remark" />
|
||||
<el-table-column label="操作" align="center" fixed="right" width="120">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
v-hasPermi="['mes:workstation-binding:update']"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete([scope.row.id])"
|
||||
v-hasPermi="['mes:workstation-binding:delete']"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<WorkstationBindingForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
YVHgetWorkstationBindingPage,
|
||||
YVHdeleteWorkstationBinding
|
||||
} from '@/api/mes/production/workstation'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import WorkstationBindingForm from './WorkstationBindingForm.vue'
|
||||
|
||||
const loading = ref(true)
|
||||
const list = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
code: undefined,
|
||||
name: undefined,
|
||||
status: undefined
|
||||
})
|
||||
const queryFormRef = ref()
|
||||
const formRef = ref()
|
||||
const selectionList = ref<any[]>([])
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await YVHgetWorkstationBindingPage(queryParams)
|
||||
list.value = res.list
|
||||
total.value = res.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields?.()
|
||||
queryParams.code = undefined
|
||||
queryParams.name = undefined
|
||||
queryParams.status = undefined
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const openForm = (type: 'create' | 'update', id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
const handleDelete = async (ids: number[]) => {
|
||||
if (!ids || ids.length === 0) return
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除选中的工位绑定吗?', '提示', { type: 'warning' })
|
||||
for (const id of ids) {
|
||||
await YVHdeleteWorkstationBinding(id)
|
||||
}
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleSelectionChange = (rows: any[]) => {
|
||||
selectionList.value = rows
|
||||
}
|
||||
|
||||
/** 行点击操作 */
|
||||
const handleRowClick = (row: any, column: any, event: MouseEvent) => {
|
||||
// 检查是否点击了按钮、链接或其他交互元素
|
||||
const target = event.target as HTMLElement
|
||||
if (
|
||||
target.tagName === 'BUTTON' ||
|
||||
target.tagName === 'A' ||
|
||||
target.tagName === 'I' ||
|
||||
target.tagName === 'svg' ||
|
||||
target.closest('button') ||
|
||||
target.closest('a') ||
|
||||
target.closest('.el-button') ||
|
||||
target.closest('.el-checkbox')
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// 直接打开编辑页面
|
||||
openForm('update', row.id)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user