| | |
| | | <p v-if="group && loadDeviceName" class="sub-info">当前设备:{{ loadDeviceName }}</p> |
| | | </div> |
| | | <div class="action-buttons"> |
| | | <el-button |
| | | <!-- <el-button |
| | | type="danger" |
| | | plain |
| | | :disabled="!group || !loadDeviceId || loadDeviceLoading" |
| | |
| | | > |
| | | <el-icon><Delete /></el-icon> |
| | | 清空PLC |
| | | </el-button> --> |
| | | <el-button type="success" :disabled="!group" :loading="importLoading" @click="handleImportExcel"> |
| | | <el-icon> |
| | | <Upload /> |
| | | </el-icon> |
| | | 导入Excel数据 |
| | | </el-button> |
| | | <input ref="fileInputRef" type="file" accept=".xlsx,.xls" style="display: none" @change="handleFileChange" /> |
| | | <el-button type="primary" :disabled="!group" :loading="loading" @click="handleSubmit"> |
| | | <el-icon><Promotion /></el-icon> |
| | | <el-icon> |
| | | <Promotion /> |
| | | </el-icon> |
| | | 启动测试 |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <el-form :model="form" label-width="120px" :rules="rules" ref="formRef"> |
| | | <el-form-item label="玻璃ID列表" prop="glassIds" required> |
| | | <div style="width: 350px; margin-bottom: 12px; margin-left: 120px;"> |
| | | <el-select |
| | | v-model="selectedEngineeringId" |
| | | placeholder="选择工程号(选择后自动填充玻璃ID)" |
| | | clearable |
| | | filterable |
| | | :disabled="!group" |
| | | :loading="engineeringListLoading" |
| | | @change="handleEngineeringChange" |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="item in engineeringList" |
| | | :key="item.engineeringId" |
| | | :label="item.engineeringId" |
| | | :value="item.engineeringId" |
| | | > |
| | | <div style="display: flex; justify-content: space-between; align-items: center; width: 100%;"> |
| | | <div style="flex: 1;"> |
| | | <span>{{ item.engineeringId }}</span> |
| | | <span style="margin-left: 8px; color: #8492a6; font-size: 12px"> |
| | | {{ item.date ? new Date(item.date).toLocaleDateString() : '' }} |
| | | </span> |
| | | </div> |
| | | <el-button |
| | | type="danger" |
| | | link |
| | | size="small" |
| | | :loading="deletingEngineeringId === item.engineeringId" |
| | | @click.stop="handleDeleteEngineering(item.engineeringId)" |
| | | style="margin-left: 8px; padding: 0 4px;" |
| | | > |
| | | <el-icon><Delete /></el-icon> |
| | | </el-button> |
| | | </div> |
| | | </el-option> |
| | | </el-select> |
| | | </div> |
| | | |
| | | <el-form-item label="玻璃ID列表" prop="glassIds"> |
| | | <el-input |
| | | v-model="glassIdsInput" |
| | | type="textarea" |
| | | :rows="4" |
| | | placeholder="请输入玻璃条码,支持多行或逗号分隔,每行一个或逗号分隔" |
| | | placeholder="可选:输入玻璃ID,将使用输入的ID进行测试(或通过上方选择工程号自动填充)" |
| | | show-word-limit |
| | | :maxlength="5000" |
| | | /> |
| | | <div class="form-tip"> |
| | | 已输入 {{ glassIds.length }} 个玻璃ID |
| | | <span v-if="glassIds.length > 0">已输入 {{ glassIds.length }} 个玻璃ID(测试模式:使用输入的ID)</span> |
| | | <span v-else>未输入玻璃ID(正常模式:将从数据库读取最近扫码的玻璃ID)</span> |
| | | </div> |
| | | </el-form-item> |
| | | |
| | | <el-divider content-position="left">设备特定配置</el-divider> |
| | | |
| | | <el-form-item label="位置编码"> |
| | | <el-input |
| | | v-model="form.positionCode" |
| | | placeholder="例如:POS1" |
| | | clearable |
| | | /> |
| | | <div class="form-tip">上大车设备的位置编码</div> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="位置值"> |
| | | <el-input-number |
| | | v-model="form.positionValue" |
| | | :min="0" |
| | | :max="9999" |
| | | placeholder="位置数值" |
| | | /> |
| | | <div class="form-tip">上大车设备的位置数值</div> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="存储位置"> |
| | | <el-input-number |
| | | v-model="form.storagePosition" |
| | | :min="1" |
| | | :max="200" |
| | | placeholder="存储位置编号" |
| | | /> |
| | | <div class="form-tip">玻璃存储设备的存储位置</div> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="处理类型"> |
| | | <el-select v-model="form.processType" placeholder="选择处理类型" clearable> |
| | | <el-option label="标准处理" :value="1" /> |
| | | <el-option label="快速处理" :value="2" /> |
| | | <el-option label="慢速处理" :value="3" /> |
| | | </el-select> |
| | | <div class="form-tip">大理片设备的处理类型</div> |
| | | </el-form-item> |
| | | |
| | | <el-divider content-position="left">执行配置</el-divider> |
| | | |
| | | <el-form-item label="执行间隔 (ms)"> |
| | | <el-input-number |
| | | v-model="form.executionInterval" |
| | | :min="100" |
| | | :max="10000" |
| | | :step="100" |
| | | placeholder="设备操作间隔时间" |
| | | /> |
| | | <div class="form-tip">每个设备操作之间的间隔时间(毫秒)</div> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="超时时间 (分钟)"> |
| | | <el-input-number |
| | | v-model="form.timeoutMinutes" |
| | | :min="1" |
| | | :max="60" |
| | | :step="1" |
| | | placeholder="任务超时时间" |
| | | /> |
| | | <div class="form-tip">任务执行的最大超时时间</div> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="重试次数"> |
| | | <el-input-number |
| | | v-model="form.retryCount" |
| | | :min="0" |
| | | :max="10" |
| | | :step="1" |
| | | placeholder="失败重试次数" |
| | | /> |
| | | <div class="form-tip">设备操作失败时的最大重试次数</div> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <!-- 设备组拓扑图 --> |
| | | <GroupTopology v-if="group" :group="group" class="topology-section" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, reactive, ref, watch } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { Delete, Promotion } from '@element-plus/icons-vue' |
| | | import { computed, reactive, ref, watch, onMounted } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { Delete, Promotion, Upload } from '@element-plus/icons-vue' |
| | | import * as XLSX from 'xlsx' |
| | | import { multiDeviceTaskApi } from '@/api/device/multiDeviceTask' |
| | | import { deviceGroupApi, deviceInteractionApi } from '@/api/device/deviceManagement' |
| | | import { engineeringApi } from '@/api/engineering' |
| | | import GroupTopology from '../DeviceGroup/GroupTopology.vue' |
| | | |
| | | const props = defineProps({ |
| | | group: { |
| | |
| | | }) |
| | | |
| | | const emit = defineEmits(['task-started']) |
| | | |
| | | const form = reactive({ |
| | | positionCode: '', |
| | | positionValue: null, |
| | | storagePosition: null, |
| | | processType: null, |
| | | executionInterval: 1000, |
| | | timeoutMinutes: 30, |
| | | retryCount: 3 |
| | | }) |
| | | //配置默认值 |
| | | const form = reactive({}) |
| | | |
| | | const formRef = ref(null) |
| | | |
| | |
| | | glassIds: [ |
| | | { |
| | | validator: (rule, value, callback) => { |
| | | // 如果输入了玻璃ID,则进行验证;如果没有输入,则允许(将从数据库读取) |
| | | if (glassIds.value.length === 0) { |
| | | callback(new Error('请至少输入一个玻璃ID')) |
| | | // 允许为空,将从数据库读取最近扫码的玻璃ID |
| | | callback() |
| | | } else if (glassIds.value.length > 100) { |
| | | callback(new Error('玻璃ID数量不能超过100个')) |
| | | } else { |
| | |
| | | |
| | | const glassIdsInput = ref('') |
| | | const loading = ref(false) |
| | | const importLoading = ref(false) |
| | | const clearLoading = ref(false) |
| | | const loadDeviceId = ref(null) |
| | | const loadDeviceName = ref('') |
| | | const loadDeviceLoading = ref(false) |
| | | const fileInputRef = ref(null) |
| | | |
| | | // 工程号相关 |
| | | const selectedEngineeringId = ref('') |
| | | const engineeringList = ref([]) |
| | | const engineeringListLoading = ref(false) |
| | | const glassIdsLoading = ref(false) |
| | | const deletingEngineeringId = ref('') |
| | | |
| | | watch( |
| | | () => props.group, |
| | | () => { |
| | | glassIdsInput.value = '' |
| | | selectedEngineeringId.value = '' |
| | | fetchLoadDevice() |
| | | fetchEngineeringList() |
| | | } |
| | | ) |
| | | |
| | | // 组件挂载时加载工程号列表 |
| | | onMounted(() => { |
| | | fetchEngineeringList() |
| | | }) |
| | | |
| | | const glassIds = computed(() => { |
| | | if (!glassIdsInput.value) return [] |
| | |
| | | .map((item) => item.trim()) |
| | | .filter((item) => item.length > 0) |
| | | }) |
| | | |
| | | // 获取工程号列表 |
| | | const fetchEngineeringList = async () => { |
| | | try { |
| | | engineeringListLoading.value = true |
| | | const response = await engineeringApi.getEngineeringList() |
| | | |
| | | if (Array.isArray(response)) { |
| | | engineeringList.value = response |
| | | } else if (Array.isArray(response?.data)) { |
| | | engineeringList.value = response.data |
| | | } else { |
| | | engineeringList.value = [] |
| | | } |
| | | // 按日期倒序排列 |
| | | engineeringList.value.sort((a, b) => { |
| | | const dateA = a.date ? new Date(a.date).getTime() : 0 |
| | | const dateB = b.date ? new Date(b.date).getTime() : 0 |
| | | return dateB - dateA |
| | | }) |
| | | } catch (error) { |
| | | console.error('获取工程号列表失败:', error) |
| | | ElMessage.error(error?.message || '获取工程号列表失败') |
| | | engineeringList.value = [] |
| | | } finally { |
| | | engineeringListLoading.value = false |
| | | } |
| | | } |
| | | |
| | | // 处理工程号选择变化 |
| | | const handleEngineeringChange = async (engineeringId) => { |
| | | if (!engineeringId) { |
| | | // 清空选择时,不清空已输入的玻璃ID,让用户保留 |
| | | return |
| | | } |
| | | |
| | | try { |
| | | glassIdsLoading.value = true |
| | | const response = await engineeringApi.getGlassIdsByEngineeringId(engineeringId) |
| | | |
| | | const glassIds = response?.glassIds || response?.data?.glassIds || [] |
| | | |
| | | if (glassIds.length > 0) { |
| | | glassIdsInput.value = glassIds.join('\n') |
| | | ElMessage.success(`已加载工程号 ${engineeringId} 的 ${glassIds.length} 个玻璃ID`) |
| | | } else { |
| | | ElMessage.warning(`工程号 ${engineeringId} 下没有找到玻璃ID`) |
| | | } |
| | | } catch (error) { |
| | | console.error('获取玻璃ID列表失败:', error) |
| | | ElMessage.error(error?.message || '获取玻璃ID列表失败') |
| | | } finally { |
| | | glassIdsLoading.value = false |
| | | } |
| | | } |
| | | |
| | | // 处理删除工程号 |
| | | const handleDeleteEngineering = async (engineeringId) => { |
| | | if (!engineeringId) { |
| | | return |
| | | } |
| | | |
| | | try { |
| | | await ElMessageBox.confirm( |
| | | `确定要删除工程号 "${engineeringId}" 及其关联的所有玻璃信息吗?此操作不可恢复!`, |
| | | '确认删除', |
| | | { |
| | | confirmButtonText: '确定删除', |
| | | cancelButtonText: '取消', |
| | | type: 'warning', |
| | | dangerouslyUseHTMLString: false |
| | | } |
| | | ) |
| | | |
| | | deletingEngineeringId.value = engineeringId |
| | | const response = await engineeringApi.deleteEngineering(engineeringId) |
| | | |
| | | const result = response?.data || response |
| | | if (result?.success !== false) { |
| | | const deletedCount = result?.deletedGlassCount || 0 |
| | | ElMessage.success(`已删除工程号 ${engineeringId},共删除 ${deletedCount} 条玻璃信息`) |
| | | |
| | | // 如果删除的是当前选中的工程号,清空选择 |
| | | if (selectedEngineeringId.value === engineeringId) { |
| | | selectedEngineeringId.value = '' |
| | | glassIdsInput.value = '' |
| | | } |
| | | |
| | | // 刷新工程号列表 |
| | | await fetchEngineeringList() |
| | | } else { |
| | | throw new Error(result?.message || '删除失败') |
| | | } |
| | | } catch (error) { |
| | | if (error !== 'cancel') { |
| | | console.error('删除工程号失败:', error) |
| | | ElMessage.error(error?.message || '删除工程号失败') |
| | | } |
| | | } finally { |
| | | deletingEngineeringId.value = '' |
| | | } |
| | | } |
| | | |
| | | const normalizeType = (type) => (type || '').trim().toUpperCase() |
| | | |
| | | const fetchLoadDevice = async () => { |
| | | loadDeviceId.value = null |
| | |
| | | const deviceList = Array.isArray(rawList) |
| | | ? rawList |
| | | : Array.isArray(rawList?.records) |
| | | ? rawList.records |
| | | : Array.isArray(rawList?.data) |
| | | ? rawList.data |
| | | : [] |
| | | const targetDevice = |
| | | deviceList.find((item) => (item.deviceType || '').toUpperCase() === 'LOAD_VEHICLE') || |
| | | deviceList[0] |
| | | ? rawList.records |
| | | : Array.isArray(rawList?.data) |
| | | ? rawList.data |
| | | : [] |
| | | const scannerDevice = deviceList.find((item) => { |
| | | const type = normalizeType(item.deviceType) |
| | | return type.includes('SCANNER') || type.includes('扫码') |
| | | }) |
| | | const loadVehicleDevice = deviceList.find((item) => { |
| | | const type = normalizeType(item.deviceType) |
| | | return type.includes('LOAD_VEHICLE') || type.includes('大车') |
| | | }) |
| | | const targetDevice = scannerDevice || loadVehicleDevice || deviceList[0] |
| | | if (targetDevice && targetDevice.id) { |
| | | loadDeviceId.value = targetDevice.id |
| | | loadDeviceName.value = targetDevice.deviceName || targetDevice.deviceCode || `ID: ${targetDevice.id}` |
| | |
| | | ElMessage.warning('请先选择设备组') |
| | | return |
| | | } |
| | | |
| | | |
| | | // 表单验证 |
| | | if (!formRef.value) return |
| | | try { |
| | |
| | | ElMessage.warning('请检查表单输入') |
| | | return |
| | | } |
| | | |
| | | if (glassIds.value.length === 0) { |
| | | ElMessage.warning('请至少输入一个玻璃ID') |
| | | return |
| | | } |
| | | |
| | | |
| | | try { |
| | | loading.value = true |
| | | |
| | | |
| | | // 构建任务参数 |
| | | // 如果输入了玻璃ID,使用输入的;如果没有输入,glassIds为空数组,后端会从数据库读取 |
| | | const parameters = { |
| | | glassIds: glassIds.value, |
| | | executionInterval: form.executionInterval || 1000 |
| | | glassIds: glassIds.value.length > 0 ? glassIds.value : [] |
| | | } |
| | | |
| | | // 添加可选参数 |
| | | if (form.positionCode) { |
| | | parameters.positionCode = form.positionCode |
| | | } |
| | | if (form.positionValue !== null) { |
| | | parameters.positionValue = form.positionValue |
| | | } |
| | | if (form.storagePosition !== null) { |
| | | parameters.storagePosition = form.storagePosition |
| | | } |
| | | if (form.processType !== null) { |
| | | parameters.processType = form.processType |
| | | } |
| | | if (form.timeoutMinutes) { |
| | | parameters.timeoutMinutes = form.timeoutMinutes |
| | | } |
| | | if (form.retryCount !== null) { |
| | | parameters.retryCount = form.retryCount |
| | | } |
| | | |
| | | |
| | | // 异步启动任务,立即返回,不阻塞 |
| | | const response = await multiDeviceTaskApi.startTask({ |
| | | groupId: props.group.id || props.group.groupId, |
| | | parameters |
| | | }) |
| | | |
| | | |
| | | const task = response?.data |
| | | if (task && task.taskId) { |
| | | ElMessage.success(`任务已启动(异步执行): ${task.taskId}`) |
| | | emit('task-started', task) |
| | | |
| | | |
| | | // 立即刷新监控列表,显示新启动的任务 |
| | | setTimeout(() => { |
| | | emit('task-started') |
| | | }, 500) |
| | | |
| | | // 重置表单(保留部分配置),方便继续启动其他设备组 |
| | | |
| | | // 重置表单(保留执行配置),方便继续启动其他设备组 |
| | | glassIdsInput.value = '' |
| | | form.positionCode = '' |
| | | form.positionValue = null |
| | | form.storagePosition = null |
| | | form.processType = null |
| | | |
| | | |
| | | // 提示用户可以继续启动其他设备组 |
| | | ElMessage.info('可以继续选择其他设备组启动测试,多个设备组将并行执行') |
| | | } else { |
| | |
| | | return |
| | | } |
| | | if (!loadDeviceId.value) { |
| | | ElMessage.warning('未找到上大车设备,无法清空PLC') |
| | | ElMessage.warning('未找到对应设备,无法清空PLC') |
| | | return |
| | | } |
| | | try { |
| | | clearLoading.value = true |
| | | const response = await deviceInteractionApi.executeOperation({ |
| | | deviceId: loadDeviceId.value, |
| | | operation: 'clearGlass', |
| | | params: { |
| | | positionCode: form.positionCode || null |
| | | } |
| | | operation: 'clearPlc', |
| | | params: {} |
| | | }) |
| | | if (response?.code !== 200) { |
| | | throw new Error(response?.message || 'PLC清空失败') |
| | |
| | | ElMessage.error(error?.message || 'PLC清空失败') |
| | | } finally { |
| | | clearLoading.value = false |
| | | } |
| | | } |
| | | |
| | | // 处理导入Excel按钮点击 |
| | | const handleImportExcel = () => { |
| | | if (!props.group) { |
| | | ElMessage.warning('请先选择设备组') |
| | | return |
| | | } |
| | | if (fileInputRef.value) { |
| | | fileInputRef.value.click() |
| | | } |
| | | } |
| | | |
| | | // 处理文件选择 |
| | | const handleFileChange = async (event) => { |
| | | const file = event.target.files?.[0] |
| | | if (!file) { |
| | | return |
| | | } |
| | | |
| | | // 验证文件类型 |
| | | const fileName = file.name.toLowerCase() |
| | | if (!fileName.endsWith('.xlsx') && !fileName.endsWith('.xls')) { |
| | | ElMessage.error('请选择 Excel 文件(.xlsx 或 .xls)') |
| | | event.target.value = '' |
| | | return |
| | | } |
| | | |
| | | try { |
| | | importLoading.value = true |
| | | |
| | | // 读取文件 |
| | | const fileReader = new FileReader() |
| | | fileReader.onload = (e) => { |
| | | try { |
| | | const data = new Uint8Array(e.target.result) |
| | | const workbook = XLSX.read(data, { type: 'array' }) |
| | | |
| | | // 读取第一个工作表 |
| | | const firstSheetName = workbook.SheetNames[0] |
| | | const worksheet = workbook.Sheets[firstSheetName] |
| | | |
| | | // 转换为 JSON 数组 |
| | | const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) |
| | | |
| | | if (!jsonData || jsonData.length === 0) { |
| | | ElMessage.error('Excel 文件为空') |
| | | event.target.value = '' |
| | | return |
| | | } |
| | | |
| | | // 解析数据(假设第一行是表头) |
| | | const parsedData = parseExcelData(jsonData) |
| | | |
| | | if (parsedData.length === 0) { |
| | | ElMessage.error('未能解析到有效数据,请检查 Excel 格式') |
| | | event.target.value = '' |
| | | return |
| | | } |
| | | |
| | | // 发送数据 |
| | | submitGlassData(parsedData) |
| | | |
| | | } catch (error) { |
| | | console.error('解析 Excel 失败:', error) |
| | | ElMessage.error('解析 Excel 文件失败: ' + (error.message || '未知错误')) |
| | | } finally { |
| | | event.target.value = '' |
| | | importLoading.value = false |
| | | } |
| | | } |
| | | |
| | | fileReader.onerror = () => { |
| | | ElMessage.error('读取文件失败') |
| | | event.target.value = '' |
| | | importLoading.value = false |
| | | } |
| | | |
| | | fileReader.readAsArrayBuffer(file) |
| | | |
| | | } catch (error) { |
| | | console.error('导入 Excel 失败:', error) |
| | | ElMessage.error('导入 Excel 失败: ' + (error.message || '未知错误')) |
| | | importLoading.value = false |
| | | event.target.value = '' |
| | | } |
| | | } |
| | | |
| | | // 解析 Excel 数据 |
| | | const parseExcelData = (jsonData) => { |
| | | if (!jsonData || jsonData.length < 2) { |
| | | return [] |
| | | } |
| | | |
| | | // 尝试识别表头(支持中英文) |
| | | const headerRow = jsonData[0] |
| | | const headerMap = {} |
| | | |
| | | headerRow.forEach((header, index) => { |
| | | if (!header) return |
| | | const headerStr = String(header).trim().toLowerCase() |
| | | |
| | | // 玻璃ID |
| | | if (headerStr.includes('玻璃id') || headerStr.includes('glassid') || |
| | | (headerStr.includes('玻璃') && headerStr.includes('id')) || |
| | | headerStr === 'id' || headerStr === 'glass_id') { |
| | | headerMap.glassId = index |
| | | } |
| | | // 宽度 |
| | | else if (headerStr.includes('宽') || headerStr.includes('width') || |
| | | headerStr === 'w' || headerStr === '宽度') { |
| | | headerMap.width = index |
| | | } |
| | | // 高度 |
| | | else if (headerStr.includes('高') || headerStr.includes('height') || |
| | | headerStr === 'h' || headerStr === '高度') { |
| | | headerMap.height = index |
| | | } |
| | | // 厚度 |
| | | else if (headerStr.includes('厚') || headerStr.includes('thickness') || |
| | | headerStr === 't' || headerStr === '厚度') { |
| | | headerMap.thickness = index |
| | | } |
| | | // 数量 |
| | | else if (headerStr.includes('数量') || headerStr.includes('quantity') || |
| | | headerStr.includes('qty') || headerStr === '数量') { |
| | | headerMap.quantity = index |
| | | } |
| | | // 膜系 |
| | | else if (headerStr.includes('膜系') || headerStr.includes('films') || |
| | | headerStr.includes('film') || headerStr === '膜系id') { |
| | | headerMap.filmsId = index |
| | | } |
| | | // 流程卡ID |
| | | else if (headerStr.includes('流程卡') || headerStr.includes('flowcard') || |
| | | headerStr.includes('flow') || headerStr === '流程卡id') { |
| | | headerMap.flowCardId = index |
| | | } |
| | | // 产品名称 |
| | | else if (headerStr.includes('产品') || headerStr.includes('product') || |
| | | headerStr === '产品名称') { |
| | | headerMap.productName = index |
| | | } |
| | | // 客户名称 |
| | | else if (headerStr.includes('客户') || headerStr.includes('customer') || |
| | | headerStr === '客户名称') { |
| | | headerMap.customerName = index |
| | | } |
| | | }) |
| | | |
| | | // 如果没有找到表头,尝试使用第一行作为表头(索引方式) |
| | | if (Object.keys(headerMap).length === 0 && jsonData.length > 1) { |
| | | // 默认格式:玻璃ID, 宽, 高, 厚, 数量(按列顺序) |
| | | headerMap.glassId = 0 |
| | | headerMap.width = 1 |
| | | headerMap.height = 2 |
| | | headerMap.thickness = 3 |
| | | headerMap.quantity = 4 |
| | | } |
| | | |
| | | // 解析数据行 |
| | | const result = [] |
| | | for (let i = 1; i < jsonData.length; i++) { |
| | | const row = jsonData[i] |
| | | if (!row || row.length === 0) continue |
| | | |
| | | const glassId = row[headerMap.glassId] ? String(row[headerMap.glassId]).trim() : '' |
| | | const width = row[headerMap.width] ? String(row[headerMap.width]).trim() : '' |
| | | const height = row[headerMap.height] ? String(row[headerMap.height]).trim() : '' |
| | | const thickness = row[headerMap.thickness] ? String(row[headerMap.thickness]).trim() : '' |
| | | const quantity = row[headerMap.quantity] ? String(row[headerMap.quantity]).trim() : '' |
| | | const filmsId = row[headerMap.filmsId] ? String(row[headerMap.filmsId]).trim() : '' |
| | | const flowCardId = row[headerMap.flowCardId] ? String(row[headerMap.flowCardId]).trim() : '' |
| | | const productName = row[headerMap.productName] ? String(row[headerMap.productName]).trim() : '' |
| | | const customerName = row[headerMap.customerName] ? String(row[headerMap.customerName]).trim() : '' |
| | | |
| | | // 跳过空行 |
| | | if (!glassId && !width && !height && !thickness && !quantity) { |
| | | continue |
| | | } |
| | | |
| | | // 验证必填字段 |
| | | if (!glassId) { |
| | | ElMessage.warning(`第 ${i + 1} 行:玻璃ID为空,已跳过`) |
| | | continue |
| | | } |
| | | |
| | | // 转换数值类型,确保格式正确 |
| | | const parseNumber = (value) => { |
| | | if (!value) return '0' |
| | | const num = parseFloat(value) |
| | | return isNaN(num) ? '0' : String(num) |
| | | } |
| | | |
| | | // 处理数量:如果数量大于1,需要生成多条记录 |
| | | const qty = parseInt(quantity) || 1 |
| | | for (let j = 0; j < qty; j++) { |
| | | // 如果数量大于1,为每条记录生成唯一的玻璃ID(追加序号) |
| | | const finalGlassId = qty > 1 ? `${glassId}${j + 1}` : glassId |
| | | |
| | | result.push({ |
| | | glassId: finalGlassId, |
| | | width: parseNumber(width), |
| | | height: parseNumber(height), |
| | | thickness: parseNumber(thickness), |
| | | quantity: '1', // 每条记录数量为1 |
| | | filmsId: filmsId, |
| | | flowCardId: flowCardId || finalGlassId, |
| | | productName: productName, |
| | | customerName: customerName |
| | | }) |
| | | } |
| | | } |
| | | |
| | | return result |
| | | } |
| | | |
| | | // 提交玻璃数据到后端,由后端完成 JSON 转换并调用 MES 接口 |
| | | const submitGlassData = async (glassDataList) => { |
| | | if (!props.group) { |
| | | ElMessage.warning('请先选择设备组') |
| | | return |
| | | } |
| | | |
| | | try { |
| | | importLoading.value = true |
| | | |
| | | // 传递原始解析数据给后端,后端完成转换与 MES 调用 |
| | | const requestData = { excelRows: glassDataList } |
| | | |
| | | // 打印原始数据供调试 |
| | | console.log('上传到后端的原始 Excel 数据:', JSON.stringify(requestData, null, 2)) |
| | | |
| | | const response = await engineeringApi.importEngineer(requestData) |
| | | |
| | | // 检查 MES 接口是否调用成功 |
| | | // MES 接口成功时返回格式:{ code: 200/0, data: true/false, message: "..." } |
| | | if (response?.code === 200 || response?.code === 0) { |
| | | // MES 接口调用成功 |
| | | const engineerId = response?.data?.engineerId || response?.engineerId || '' |
| | | const successMsg = engineerId |
| | | ? `成功导入 ${glassDataList.length} 条玻璃数据,工程号:${engineerId}` |
| | | : `成功导入 ${glassDataList.length} 条玻璃数据` |
| | | ElMessage.success(successMsg) |
| | | |
| | | // 成功后刷新工程号下拉列表,并选中最新工程号 |
| | | try { |
| | | await fetchEngineeringList() |
| | | if (engineerId) { |
| | | selectedEngineeringId.value = engineerId |
| | | // 刷新并回填后端保存的 glassId(带工程号前缀),避免使用前端原始值 |
| | | await handleEngineeringChange(engineerId) |
| | | } |
| | | } catch (refreshErr) { |
| | | console.error('刷新工程号列表失败:', refreshErr) |
| | | } |
| | | } else { |
| | | // MES 接口返回失败 |
| | | throw new Error(response?.message || 'MES 接口返回失败') |
| | | } |
| | | } catch (error) { |
| | | console.error('提交玻璃数据失败:', error) |
| | | |
| | | // 判断错误类型,给出更友好的提示 |
| | | let errorMsg = '未知错误' |
| | | |
| | | // 检查是否是后端返回的错误响应(后端转发 MES 失败) |
| | | if (error?.response?.status === 500 && error?.response?.data) { |
| | | // 后端返回的统一错误格式 |
| | | errorMsg = error.response.data.message || error.response.data || '转发 MES 接口失败' |
| | | } else if (error?.response?.data?.message) { |
| | | // MES 接口返回的错误 |
| | | errorMsg = error.response.data.message |
| | | } else if (error?.message) { |
| | | errorMsg = error.message |
| | | } |
| | | |
| | | ElMessage.error('提交数据失败: ' + errorMsg) |
| | | |
| | | // 即使失败,也尝试填充玻璃ID到输入框 |
| | | try { |
| | | const glassIds = glassDataList.map(item => item.glassId).filter(id => id) |
| | | if (glassIds.length > 0) { |
| | | glassIdsInput.value = glassIds.join('\n') |
| | | ElMessage.info('已将玻璃ID填充到输入框,您可以手动提交') |
| | | } |
| | | } catch (e) { |
| | | console.error('填充数据失败:', e) |
| | | } |
| | | } finally { |
| | | importLoading.value = false |
| | | } |
| | | } |
| | | </script> |
| | |
| | | margin-top: 4px; |
| | | line-height: 1.4; |
| | | } |
| | | </style> |
| | | |
| | | .topology-section { |
| | | margin-top: 24px; |
| | | } |
| | | </style> |