| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <PageHeader content="工艺路线项目" /> |
| | | <PageHeader content="产品部件" /> |
| | | |
| | | <!-- 工艺路线信息展示 --> |
| | | <el-card v-if="routeInfo.processRouteCode" class="route-info-card" shadow="hover"> |
| | | <!-- <el-card v-if="routeInfo.processRouteCode" class="route-info-card" shadow="hover"> |
| | | <div class="route-info"> |
| | | <div class="info-item"> |
| | | <div class="info-label-wrapper"> |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </el-card> --> |
| | | |
| | | <!-- 表格视图 --> |
| | | <div v-if="viewMode === 'table'" class="section-header"> |
| | | <div class="section-title">工艺路线项目列表</div> |
| | | <div class="section-title">产品部件列表</div> |
| | | <div class="section-actions"> |
| | | <el-button |
| | | icon="Grid" |
| | |
| | | class="lims-table" |
| | | > |
| | | <el-table-column align="center" label="序号" width="60" type="index" /> |
| | | <el-table-column label="工序名称" prop="processId" width="200"> |
| | | <el-table-column label="产品名称" prop="name" min-width="140" show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ getProcessName(scope.row.processId) || '-' }} |
| | | {{ getProcessField(scope.row, 'name') }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="产品名称" prop="productName" min-width="160" /> |
| | | <el-table-column label="规格名称" prop="model" min-width="140" /> |
| | | <el-table-column label="单位" prop="unit" width="100" /> |
| | | <el-table-column label="产品规格" prop="productModel" min-width="120" show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ getProcessField(scope.row, 'productModel') }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="部件编号" prop="no" width="120" show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ getProcessField(scope.row, 'no') }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="部件类型" prop="typeText" width="120" show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ getProcessTypeText(getProcessRaw(scope.row)?.type) || '-' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="计划工时(小时)" prop="salaryQuota" width="130" align="center"> |
| | | <template #default="scope"> |
| | | {{ getProcessField(scope.row, 'salaryQuota') }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="计划人员" prop="plannerName" width="100" show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ getProcessField(scope.row, 'plannerName') }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="是否质检" prop="isQuality" width="90" align="center"> |
| | | <template #default="scope"> |
| | | {{ scope.row.isQuality ? '是' : '否' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="备注" prop="remark" min-width="100" show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ getProcessField(scope.row, 'remark') }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="更新时间" prop="updateTime" width="160" align="center"> |
| | | <template #default="scope"> |
| | | {{ getProcessField(scope.row, 'updateTime') }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" align="center" fixed="right" width="150"> |
| | | <template #default="scope"> |
| | | <el-button type="primary" link size="small" @click="handleEdit(scope.row)">编辑</el-button> |
| | | <el-button type="danger" link size="small" @click="handleDelete(scope.row)">删除</el-button> |
| | | <el-button type="primary" link size="small" @click="handleEdit(scope.row)" :disabled="scope.row.isComplete">编辑</el-button> |
| | | <el-button type="danger" link size="small" @click="handleDelete(scope.row)" :disabled="scope.row.isComplete">删除</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | |
| | | <!-- 序号圆圈 --> |
| | | <div class="card-header"> |
| | | <div class="card-number">{{ index + 1 }}</div> |
| | | <div class="card-process-name">{{ getProcessName(item.processId) || '-' }}</div> |
| | | <div class="card-process-name">{{ getProcessField(item, 'name') }}</div> |
| | | </div> |
| | | |
| | | <!-- 产品信息 --> |
| | | <!-- 与工序主表一致的简要信息 --> |
| | | <div class="card-content"> |
| | | <div v-if="item.productName" class="product-info"> |
| | | <div class="product-name">{{ item.productName }}</div> |
| | | <div v-if="item.model" class="product-model"> |
| | | {{ item.model }} |
| | | <!-- <span v-if="item.unit" class="product-unit">{{ item.unit }}</span> --> |
| | | <div class="product-info"> |
| | | <div class="product-name">{{ getProcessField(item, 'productModel') }}</div> |
| | | <div v-if="getProcessRaw(item)?.no" class="product-model">编号 {{ getProcessRaw(item)?.no }}</div> |
| | | <div v-if="getProcessTypeText(getProcessRaw(item)?.type)" class="product-model"> |
| | | {{ getProcessTypeText(getProcessRaw(item)?.type) }} |
| | | </div> |
| | | <el-tag type="primary" class="product-tag" v-if="item.isQuality">质检</el-tag> |
| | | </div> |
| | | <div v-else class="product-info empty">暂无产品信息</div> |
| | | </div> |
| | | |
| | | <!-- 操作按钮 --> |
| | | <div class="card-footer"> |
| | | <el-button type="primary" link size="small" @click="handleEdit(item)">编辑</el-button> |
| | | <el-button type="danger" link size="small" @click="handleDelete(item)">删除</el-button> |
| | | <el-button type="primary" link size="small" @click="handleEdit(item)" :disabled="item.isComplete">编辑</el-button> |
| | | <el-button type="danger" link size="small" @click="handleDelete(item)" :disabled="item.isComplete">删除</el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <!-- 新增/编辑弹窗 --> |
| | | <!-- 新增/编辑弹窗(布局、字段与工序/部件页 New、Edit 一致;提交参数仍为原接口字段) --> |
| | | <el-dialog |
| | | v-model="dialogVisible" |
| | | :title="operationType === 'add' ? '新增工艺路线项目' : '编辑工艺路线项目'" |
| | | width="500px" |
| | | width="760" |
| | | @close="closeDialog" |
| | | > |
| | | <el-form |
| | | ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="120px" |
| | | label-width="140px" |
| | | label-position="top" |
| | | > |
| | | <el-form-item label="工序" prop="processId"> |
| | | <el-select |
| | | v-model="form.processId" |
| | | placeholder="请选择工序" |
| | | clearable |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="process in processOptions" |
| | | :key="process.id" |
| | | :label="process.name" |
| | | :value="process.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="产品名称" prop="productModelId"> |
| | | <el-button type="primary" @click="showProductSelectDialog = true"> |
| | | {{ form.productName && form.model |
| | | ? `${form.productName} - ${form.model}` |
| | | : '选择产品' }} |
| | | </el-button> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="单位" prop="unit"> |
| | | <el-input |
| | | v-model="form.unit" |
| | | :placeholder="form.productModelId ? '根据选择的产品自动带出' : '请先选择产品'" |
| | | clearable |
| | | :disabled="true" |
| | | /> |
| | | </el-form-item> |
| | | <el-row :gutter="16"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="部件" prop="processId"> |
| | | <el-select |
| | | v-model="form.processId" |
| | | placeholder="请选择部件" |
| | | clearable |
| | | filterable |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="process in processOptions" |
| | | :key="process.id" |
| | | :label="formatProcessOptionLabel(process)" |
| | | :value="process.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item |
| | | label="产品名称:" |
| | | prop="productId" |
| | | > |
| | | <el-tree-select |
| | | v-model="form.productId" |
| | | placeholder="请选择产品名称" |
| | | clearable |
| | | filterable |
| | | check-strictly |
| | | :data="productCategoryOptions" |
| | | :render-after-expand="false" |
| | | style="width: 100%" |
| | | @change="handleProductChange" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item |
| | | label="产品规格:" |
| | | prop="productModelId" |
| | | > |
| | | <el-select |
| | | v-model="form.productModelId" |
| | | placeholder="请选择产品规格" |
| | | clearable |
| | | filterable |
| | | :disabled="!form.productId" |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="item in modelOptions" |
| | | :key="item.id" |
| | | :label="item.model" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="部件编号"> |
| | | <el-input :model-value="selectedProcess?.no ?? ''" readonly /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="部件类型"> |
| | | <el-input :model-value="getProcessTypeText(selectedProcess?.type) || '-'" readonly /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="计划工时(小时)"> |
| | | <el-input :model-value="selectedProcess?.salaryQuota != null && selectedProcess?.salaryQuota !== '' ? String(selectedProcess.salaryQuota) : ''" readonly> |
| | | <template #append>小时</template> |
| | | </el-input> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="计划人员"> |
| | | <el-input :model-value="selectedProcess?.plannerName ?? ''" readonly /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="是否质检" prop="isQuality"> |
| | | <el-switch v-model="form.isQuality" :active-value="true" inactive-value="false"/> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <el-form-item label="备注"> |
| | | <el-input :model-value="selectedProcess?.remark ?? ''" type="textarea" readonly /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | |
| | | <template #footer> |
| | | <el-button @click="closeDialog">取消</el-button> |
| | | <el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button> |
| | | <el-button @click="closeDialog">取消</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- 产品选择对话框 --> |
| | | <ProductSelectDialog |
| | | v-model="showProductSelectDialog" |
| | | @confirm="handleProductSelect" |
| | | single |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, getCurrentInstance, onMounted, onUnmounted, nextTick } from "vue"; |
| | | import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue"; |
| | | import { findProcessRouteItemList, addOrUpdateProcessRouteItem, sortProcessRouteItem, batchDeleteProcessRouteItem } from "@/api/productionManagement/processRouteItem.js"; |
| | | import { findProductProcessRouteItemList, deleteRouteItem, addRouteItem, addOrUpdateProductProcessRouteItem, sortRouteItem } from "@/api/productionManagement/productProcessRoute.js"; |
| | | import { processList } from "@/api/productionManagement/productionProcess.js"; |
| | | import { modelListPage, productTreeList } from "@/api/basicData/product"; |
| | | import { useRoute } from 'vue-router' |
| | | import { ElMessageBox } from 'element-plus' |
| | | import Sortable from 'sortablejs' |
| | |
| | | }); |
| | | |
| | | const processOptions = ref([]); |
| | | const showProductSelectDialog = ref(false); |
| | | const productCategoryOptions = ref([]); |
| | | const modelOptions = ref([]); |
| | | let tableSortable = null; |
| | | let cardSortable = null; |
| | | |
| | |
| | | id: undefined, |
| | | routeId: routeId.value, |
| | | processId: undefined, |
| | | productId: undefined, |
| | | productModelId: undefined, |
| | | productName: "", |
| | | model: "", |
| | | unit: "", |
| | | isQuality: false, |
| | | }); |
| | | |
| | | const rules = { |
| | | processId: [{ required: true, message: '请选择工序', trigger: 'change' }], |
| | | productModelId: [{ required: true, message: '请选择产品', trigger: 'change' }], |
| | | processId: [{ required: true, message: '请选择部件', trigger: 'change' }], |
| | | productId: [{ required: true, message: '请选择产品名称', trigger: 'change' }], |
| | | productModelId: [{ required: true, message: '请选择产品规格', trigger: 'change' }], |
| | | }; |
| | | |
| | | // 根据工序ID获取工序名称 |
| | | const getProcessName = (processId) => { |
| | | if (!processId) return ''; |
| | | const process = processOptions.value.find(p => p.id === processId); |
| | | return process ? process.name : ''; |
| | | const selectedProcess = computed(() => { |
| | | if (form.value.processId === undefined || form.value.processId === null || form.value.processId === '') return null; |
| | | return processOptions.value.find(p => String(p.id) === String(form.value.processId)) || null; |
| | | }); |
| | | |
| | | const getProcessTypeText = (type) => { |
| | | if (type === undefined || type === null) return ''; |
| | | const map = { |
| | | 1: '加工', |
| | | 2: '刮板冷芯制作', |
| | | 3: '管路组对', |
| | | 4: '罐体连接及调试', |
| | | 5: '测试打压', |
| | | 6: '其他', |
| | | }; |
| | | return map[type] || ''; |
| | | }; |
| | | |
| | | const getProcessRaw = (row) => { |
| | | if (!row?.processId) return null; |
| | | return processOptions.value.find(p => String(p.id) === String(row.processId)) || null; |
| | | }; |
| | | |
| | | const getProcessField = (row, key) => { |
| | | const p = getProcessRaw(row); |
| | | const fromProcess = p ? p[key] : undefined; |
| | | if (fromProcess !== undefined && fromProcess !== null && fromProcess !== '') return fromProcess; |
| | | if (key === 'name' && row.productName) return row.productName; |
| | | if (key === 'productModel' && row.model) return row.model; |
| | | const fromRow = row[key]; |
| | | if (fromRow !== undefined && fromRow !== null && fromRow !== '') return fromRow; |
| | | return '-'; |
| | | }; |
| | | |
| | | const formatProcessOptionLabel = (process) => { |
| | | if (!process) return ''; |
| | | const no = process.no ? String(process.no).trim() : ''; |
| | | const name = process.name || ''; |
| | | if (no && name) return `${no} ${name}`; |
| | | return name || no || ''; |
| | | }; |
| | | |
| | | const convertProductTree = (list) => { |
| | | return (list || []).map(item => { |
| | | const children = convertProductTree(item.children || item.childList || []); |
| | | return { |
| | | ...item, |
| | | value: item.id, |
| | | label: item.name || item.label, |
| | | children, |
| | | disabled: children.length > 0, |
| | | }; |
| | | }); |
| | | }; |
| | | |
| | | const findNodeById = (nodes, targetId) => { |
| | | for (const node of nodes || []) { |
| | | if (String(node.value) === String(targetId)) { |
| | | return node; |
| | | } |
| | | if (node.children && node.children.length > 0) { |
| | | const found = findNodeById(node.children, targetId); |
| | | if (found) return found; |
| | | } |
| | | } |
| | | return null; |
| | | }; |
| | | |
| | | const findNodeIdByLabel = (nodes, targetLabel) => { |
| | | for (const node of nodes || []) { |
| | | if (node.label === targetLabel) { |
| | | return node.value; |
| | | } |
| | | if (node.children && node.children.length > 0) { |
| | | const found = findNodeIdByLabel(node.children, targetLabel); |
| | | if (found !== null && found !== undefined) return found; |
| | | } |
| | | } |
| | | return undefined; |
| | | }; |
| | | |
| | | const getProductCategoryOptions = async () => { |
| | | try { |
| | | const res = await productTreeList(); |
| | | const list = Array.isArray(res) ? res : res?.data || []; |
| | | productCategoryOptions.value = convertProductTree(list); |
| | | } catch (e) { |
| | | productCategoryOptions.value = []; |
| | | } |
| | | }; |
| | | |
| | | const getModelOptions = async (productId) => { |
| | | if (!productId) { |
| | | modelOptions.value = []; |
| | | return; |
| | | } |
| | | try { |
| | | const res = await modelListPage({ |
| | | id: productId, |
| | | current: 1, |
| | | size: 999, |
| | | }); |
| | | const records = res?.records || res?.data?.records || []; |
| | | modelOptions.value = records; |
| | | } catch (e) { |
| | | modelOptions.value = []; |
| | | } |
| | | }; |
| | | |
| | | const handleProductChange = async (value) => { |
| | | const selectedNode = findNodeById(productCategoryOptions.value, value); |
| | | form.value.productName = selectedNode?.label || ''; |
| | | form.value.productModelId = undefined; |
| | | await getModelOptions(value); |
| | | }; |
| | | |
| | | // 获取列表 |
| | |
| | | operationType.value = 'add'; |
| | | resetForm(); |
| | | dialogVisible.value = true; |
| | | getProductCategoryOptions(); |
| | | }; |
| | | |
| | | // 编辑 |
| | | const handleEdit = (row) => { |
| | | const handleEdit = async (row) => { |
| | | operationType.value = 'edit'; |
| | | form.value = { |
| | | id: row.id, |
| | | routeId: routeId.value, |
| | | processId: row.processId, |
| | | productId: row.productId, |
| | | productModelId: row.productModelId, |
| | | productName: row.productName || "", |
| | | model: row.model || "", |
| | | unit: row.unit || "", |
| | | isQuality: row.isQuality, |
| | | }; |
| | | dialogVisible.value = true; |
| | | await getProductCategoryOptions(); |
| | | if (!form.value.productId && form.value.productName) { |
| | | form.value.productId = findNodeIdByLabel(productCategoryOptions.value, form.value.productName); |
| | | } |
| | | await getModelOptions(form.value.productId); |
| | | }; |
| | | |
| | | // 删除 |
| | |
| | | .catch(() => {}); |
| | | }; |
| | | |
| | | // 产品选择 |
| | | const handleProductSelect = (products) => { |
| | | if (products && products.length > 0) { |
| | | const product = products[0]; |
| | | form.value.productModelId = product.id; |
| | | form.value.productName = product.productName; |
| | | form.value.model = product.model; |
| | | form.value.unit = product.unit || ""; |
| | | showProductSelectDialog.value = false; |
| | | // 触发表单验证 |
| | | formRef.value?.validateField('productModelId'); |
| | | } |
| | | }; |
| | | |
| | | // 提交 |
| | | const handleSubmit = () => { |
| | | formRef.value.validate((valid) => { |
| | |
| | | productRouteId: routeId.value, |
| | | processId: form.value.processId, |
| | | productModelId: form.value.productModelId, |
| | | isQuality: form.value.isQuality, |
| | | dragSort, |
| | | }) |
| | | : addOrUpdateProcessRouteItem({ |
| | | routeId: routeId.value, |
| | | processId: form.value.processId, |
| | | productModelId: form.value.productModelId, |
| | | isQuality: form.value.isQuality, |
| | | dragSort, |
| | | }); |
| | | |
| | |
| | | id: form.value.id, |
| | | processId: form.value.processId, |
| | | productModelId: form.value.productModelId, |
| | | isQuality: form.value.isQuality, |
| | | }) |
| | | : addOrUpdateProcessRouteItem({ |
| | | routeId: routeId.value, |
| | | processId: form.value.processId, |
| | | productModelId: form.value.productModelId, |
| | | id: form.value.id, |
| | | isQuality: form.value.isQuality, |
| | | }); |
| | | |
| | | updatePromise |
| | |
| | | id: undefined, |
| | | routeId: routeId.value, |
| | | processId: undefined, |
| | | productId: undefined, |
| | | productModelId: undefined, |
| | | productName: "", |
| | | model: "", |
| | | unit: "", |
| | | isQuality: false, |
| | | }; |
| | | formRef.value?.resetFields(); |
| | | modelOptions.value = []; |
| | | nextTick(() => formRef.value?.clearValidate()); |
| | | }; |
| | | |
| | | // 关闭弹窗 |
| | |
| | | getRouteInfo(); |
| | | getList(); |
| | | getProcessList(); |
| | | getProductCategoryOptions(); |
| | | }); |
| | | |
| | | onUnmounted(() => { |
| | |
| | | color: #409eff; |
| | | } |
| | | |
| | | .product-tag { |
| | | margin: 10px 0; |
| | | } |
| | | |
| | | .card-footer { |
| | | display: flex; |
| | | justify-content: space-around; |