huang
5 天以前 f13ba9e05f653bc3083c4d17fe8658e67054131e
添加导入Excel表数据功能
4个文件已修改
2个文件已添加
2个文件已删除
1225 ■■■■■ 已修改文件
mes-processes/mes-plcSend/src/main/java/com/mes/device/controller/GlassInfoImportController.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/request/LoadVehicleRequest.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/request/VerticalCarData.java 426 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/GlassInfoService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/GlassInfoServiceImpl.java 309 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/resources/application-dev.yml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/api/engineering.js 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue 322 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/controller/GlassInfoImportController.java
New file
@@ -0,0 +1,67 @@
package com.mes.device.controller;
import com.mes.device.service.GlassInfoService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Map;
/**
 * @author :huang
 * @date :2025-12-08
 * 工程导入转发接口(接收前端 Excel 行数据,后端组装后转发 MES)
 */
@Slf4j
@RestController
@RequestMapping("excel")
@RequiredArgsConstructor
public class GlassInfoImportController {
    private final GlassInfoService glassInfoService;
    private final RestTemplate restTemplate = new RestTemplate();
    @Value("${mes.engineering.import-url}")
    private String mesEngineeringImportUrl;
    /**
     * 导入工程
     * 前端入参示例:
     * {
     *   "excelRows": [
     *     {"glassId":"GL001","width":"1000","height":"2000","thickness":"5","quantity":"2","orderNumber":"NG25082101","filmsId":"白玻"}
     *   ]
     * }
     */
    @PostMapping("/importExcel")
    public ResponseEntity<?> importEngineer(@RequestBody Map<String, Object> body) {
        Object rowsObj = body.get("excelRows");
        if (!(rowsObj instanceof List)) {
            return ResponseEntity.badRequest().body("excelRows 必须是数组");
        }
        @SuppressWarnings("unchecked")
        List<Map<String, Object>> excelRows = (List<Map<String, Object>>) rowsObj;
        if (CollectionUtils.isEmpty(excelRows)) {
            return ResponseEntity.badRequest().body("excelRows 不能为空");
        }
        Map<String, Object> payload = glassInfoService.buildEngineerImportPayload(excelRows);
        log.info("构建的 MES 导入数据: {}", payload);
        try {
            ResponseEntity<Map> mesResp = restTemplate.postForEntity(mesEngineeringImportUrl, payload, Map.class);
            return ResponseEntity.status(mesResp.getStatusCode()).body(mesResp.getBody());
        } catch (Exception e) {
            log.error("转发 MES 导入接口失败 url={}, error={}", mesEngineeringImportUrl, e.getMessage(), e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("转发 MES 失败: " + e.getMessage());
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/request/LoadVehicleRequest.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/request/VerticalCarData.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/GlassInfoService.java
@@ -75,5 +75,14 @@
     * 批量更新玻璃状态
     */
    boolean updateGlassStatus(List<String> glassIds, String status);
    /**
     * 将前端上传的 Excel 行数据转换为 MES 导入工程所需的 JSON 结构
     *
     * @param excelRows 前端解析后的行数据,字段示例:
     *                  glassId,width,height,thickness,quantity,orderNumber,filmsId,flowCardId,productName,customerName
     * @return 符合 MES 接口要求的请求体 Map
     */
    Map<String, Object> buildEngineerImportPayload(List<Map<String, Object>> excelRows);
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/GlassInfoServiceImpl.java
@@ -10,12 +10,15 @@
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static java.util.stream.IntStream.range;
/**
 * 玻璃信息服务实现类
@@ -208,5 +211,301 @@
            return false;
        }
    }
    @Override
    public Map<String, Object> buildEngineerImportPayload(List<Map<String, Object>> excelRows) {
        Map<String, Object> result = new HashMap<>();
        if (excelRows == null || excelRows.isEmpty()) {
            return result;
        }
        // 工程号生成:P + yyMMdd + 序号(2位)
        AtomicInteger seq = new AtomicInteger(1);
        final String engineerId = generateEngineerId(firstValue(excelRows, "glassId"), seq.getAndIncrement());
        final String filmsIdDefault = firstValue(excelRows, "filmsId", "白玻");
        final double thicknessDefault = parseDouble(firstValue(excelRows, "thickness"), 0d);
        // glassInfolList
        final String engineerIdFinal = engineerId;
        final String filmsIdDefaultFinal = filmsIdDefault;
        final double thicknessDefaultFinal = thicknessDefault;
        List<Map<String, Object>> glassInfolList = excelRows.stream()
                .flatMap(row -> {
                    int qty = (int) parseDouble(row.getOrDefault("quantity", 1), 1);
                    if (qty <= 0) qty = 1;
                    String glassId = str(row.get("glassId"));
                    Integer orderNumber = Integer.parseInt(str(row.get("orderNumber")));
                    String filmsId = strOrDefault(row.get("filmsId"), filmsIdDefaultFinal);
                    String flowCardId = str(row.get("flowCardId"));
                    String productName = str(row.get("productName"));
                    String customerName = str(row.get("customerName"));
                    double width = parseDouble(row.get("width"), 0d);
                    double height = parseDouble(row.get("height"), 0d);
                    double thickness = parseDouble(row.get("thickness"), thicknessDefaultFinal);
                    int finalQty = qty;
                    return range(0, qty).mapToObj(idx -> {
                        String finalGlassId = finalQty > 1 ? glassId + "_" + (idx + 1) : glassId;
                        String finalFlowCardId = flowCardId.isEmpty() ? finalGlassId : flowCardId;
                        Map<String, Object> m = new HashMap<>();
                        m.put("xAxis", 0);
                        m.put("xCoordinate", 0);
                        m.put("yAxis", 0);
                        m.put("yCoordinate", 0);
                        m.put("glassId", finalGlassId);
                        m.put("engineerId", engineerIdFinal);
                        m.put("flowCardId", finalFlowCardId);
                        m.put("orderNumber", orderNumber);
                        m.put("productSortNumber", idx + 1);
                        m.put("hollowCombineDirection", "0");
                        m.put("width", width);
                        m.put("height", height);
                        m.put("thickness", thickness);
                        m.put("filmsId", filmsId);
                        m.put("layer", 0);
                        m.put("totalLayer", 0);
                        m.put("edgWidth", width);
                        m.put("edgHeight", height);
                        m.put("isMultiple", 0);
                        m.put("maxWidth", width);
                        m.put("maxHeight", height);
                        m.put("isHorizontal", 0);
                        m.put("rawSequence", 0);
                        m.put("temperingLayoutId", 0);
                        m.put("temperingFeedSequence", 0);
                        m.put("angle", 0);
                        m.put("ruleId", 0);
                        m.put("combine", 0);
                        m.put("markIcon", "");
                        m.put("filmRemove", 0);
                        m.put("flowCardSequence", String.valueOf(idx + 1));
                        m.put("process", "");
                        m.put("rawAngle", 0);
                        m.put("graphNo", 0);
                        m.put("processParam", "");
                        return m;
                    });
                })
                .collect(Collectors.toList());
        // 原片信息去重
        Map<String, Map<String, Object>> rawGlassMap = new HashMap<>();
        for (Map<String, Object> row : excelRows) {
            double width = parseDouble(row.get("width"), 0d);
            double height = parseDouble(row.get("height"), 0d);
            double thickness = parseDouble(row.get("thickness"), thicknessDefaultFinal);
            String filmsId = strOrDefault(row.get("filmsId"), filmsIdDefaultFinal);
            String key = width + "_" + height + "_" + thickness + "_" + filmsId;
            if (!rawGlassMap.containsKey(key)) {
                Map<String, Object> m = new HashMap<>();
                m.put("engineeringId", engineerIdFinal);
                m.put("filmsId", filmsId);
                m.put("rawGlassWidth", width);
                m.put("rawGlassHeight", height);
                m.put("rawGlassThickness", thickness);
                m.put("rawSequence", rawGlassMap.size() + 1);
                m.put("usageRate", "0.95");
                rawGlassMap.put(key, m);
            }
        }
        List<Map<String, Object>> engineeringRawQueueList = rawGlassMap.values().stream().collect(Collectors.toList());
        // 流程卡信息
        Map<String, Map<String, Object>> flowCardMap = new HashMap<>();
        for (Map<String, Object> row : excelRows) {
            String glassId = str(row.get("glassId"));
            String flowCardId = str(row.get("flowCardId"));
            if (flowCardId.isEmpty()) {
                flowCardId = glassId;
            }
            double width = parseDouble(row.get("width"), 0d);
            double height = parseDouble(row.get("height"), 0d);
            double thickness = parseDouble(row.get("thickness"), thicknessDefaultFinal);
            String filmsId = strOrDefault(row.get("filmsId"), filmsIdDefaultFinal);
            Integer orderNumber = Integer.parseInt(str(row.get("orderNumber")));
            String productName = str(row.get("productName"));
            String customerName = str(row.get("customerName"));
            Map<String, Object> exist = flowCardMap.get(flowCardId);
            if (exist == null) {
                Map<String, Object> m = new HashMap<>();
                m.put("flowCardId", flowCardId);
                m.put("width", width);
                m.put("height", height);
                m.put("thickness", thickness);
                m.put("filmsId", filmsId);
                m.put("totalLayer", 0);
                m.put("layer", 0);
                m.put("glassTotal", 1);
                m.put("orderNumber", orderNumber);
                m.put("productName", productName);
                m.put("customerName", customerName);
                flowCardMap.put(flowCardId, m);
            } else {
                int count = (int) exist.getOrDefault("glassTotal", 1);
                exist.put("glassTotal", count + 1);
            }
        }
        List<Map<String, Object>> flowCardInfoList = flowCardMap.values().stream().collect(Collectors.toList());
        // 汇总
        int glassTotal = glassInfolList.size();
        double glassTotalArea = glassInfolList.stream()
                .mapToDouble(m -> parseDouble(m.get("width"), 0d) * parseDouble(m.get("height"), 0d) / 1_000_000d)
                .sum();
        double patternArea = engineeringRawQueueList.stream()
                .mapToDouble(m -> parseDouble(m.get("rawGlassWidth"), 0d) * parseDouble(m.get("rawGlassHeight"), 0d) / 1_000_000d)
                .sum();
        result.put("engineerId", engineerIdFinal);
        result.put("engineerName", "工程_" + engineerIdFinal);
        result.put("avgAvailability", "90");
        result.put("validAvailability", "90");
        result.put("lastAvailability", "90");
        result.put("glassTotal", glassTotal);
        result.put("glassTotalArea", round2(glassTotalArea));
        result.put("planPatternTotal", engineeringRawQueueList.size());
        result.put("planPatternTotalArea", round2(patternArea));
        result.put("realityPatternTotal", engineeringRawQueueList.size());
        result.put("realityPatternTotalArea", round2(patternArea));
        result.put("filmsId", filmsIdDefaultFinal);
        result.put("thickness", thicknessDefaultFinal);
        result.put("engineeringRawQueueList", engineeringRawQueueList);
        result.put("glassInfolList", glassInfolList);
        result.put("flowCardInfoList", flowCardInfoList);
        result.put("hollowFormulaDetailsList", null);
        result.put("temperingParameterList", null);
        return result;
    }
    // 日期格式化器(线程不安全,使用ThreadLocal保证线程安全)
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyMMdd");
    // 数字匹配正则(预编译提升性能)
    private static final Pattern DIGIT_PATTERN = Pattern.compile("\\d+");
    /**
     * 生成工程师ID
     * 格式规则:P + 年月日(yyMMdd) + 两位序号
     * 序号优先从glassId中提取末尾两位数字,否则使用传入的index补零
     *
     * @param glassId 玻璃ID(可为null,用于提取数字序号)
     * @param index   备用序号(当glassId无有效数字时使用)
     * @return 格式化的工程师ID(如:P25010801)
     */
    private String generateEngineerId(Object glassId, int index) {
        // 1. 生成日期前缀(yyMMdd)
        String base = LocalDate.now().format(DATE_FORMATTER);
        // 2. 初始化序号(两位补零)
        String seq = String.format("%02d", index);
        // 3. 从glassId中提取末尾两位数字(覆盖默认序号)
        if (glassId != null) {
            String glassIdStr = glassId.toString();
            Matcher matcher = DIGIT_PATTERN.matcher(glassIdStr);
            String lastDigitStr = null;
            // 遍历匹配所有数字段,取最后一个
            while (matcher.find()) {
                lastDigitStr = matcher.group();
            }
            // 若数字段长度≥2,取最后两位;否则保留原序号
            if (lastDigitStr != null && lastDigitStr.length() >= 2) {
                seq = lastDigitStr.substring(lastDigitStr.length() - 2);
            }
        }
        return "P" + base + seq;
    }
    /**
     * 提取List中第一个Map的指定key值(默认空字符串)
     *
     * @param rows 数据行列表(可为null/空)
     * @param key  要提取的键
     * @return 第一个Map的key对应值(空则返回"")
     */
    private String firstValue(List<Map<String, Object>> rows, String key) {
        return firstValue(rows, key, "");
    }
    /**
     * 提取List中第一个Map的指定key值(自定义默认值)
     *
     * @param rows       数据行列表(可为null/空)
     * @param key        要提取的键
     * @param defaultVal 空值时的默认返回值
     * @return 第一个Map的key对应值(空则返回defaultVal)
     */
    private String firstValue(List<Map<String, Object>> rows, String key, String defaultVal) {
        if (rows == null || rows.isEmpty() || key == null) {
            return defaultVal;
        }
        Map<String, Object> firstRow = rows.get(0);
        Object value = firstRow.get(key);
        return value == null ? defaultVal : value.toString();
    }
    /**
     * 对象转字符串(null转空串,自动去除首尾空格)
     *
     * @param v 待转换对象
     * @return 处理后的字符串
     */
    private String str(Object v) {
        return v == null ? "" : v.toString().trim();
    }
    /**
     * 对象转字符串(空串时返回默认值,自动去除首尾空格)
     *
     * @param v   待转换对象
     * @param def 空值默认值
     * @return 处理后的字符串
     */
    private String strOrDefault(Object v, String def) {
        String result = str(v);
        return result.isEmpty() ? def : result;
    }
    /**
     * 解析对象为double(失败/空值返回默认值)
     *
     * @param v   待解析对象(支持数字/字符串类型)
     * @param def 解析失败时的默认值
     * @return 解析后的double值
     */
    private double parseDouble(Object v, double def) {
        if (v == null) {
            return def;
        }
        try {
            if (v instanceof Number) {
                return ((Number) v).doubleValue();
            }
            return Double.parseDouble(v.toString().trim());
        } catch (NumberFormatException e) {
            // 仅捕获数字格式化异常,避免吞掉其他异常
            return def;
        }
    }
    /**
     * 保留两位小数(四舍五入)
     *
     * @param v 原始数值
     * @return 保留两位小数后的数值
     */
    private double round2(double v) {
        return Math.round(v * 100.0) / 100.0;
    }
}
mes-processes/mes-plcSend/src/main/resources/application-dev.yml
@@ -30,6 +30,9 @@
    port: 6379
    password: 123456
mes:
  engineering:
    import-url: http://10.153.19.208:10015/engineering/importEngineer
# PLC自动测试配置
plc:
  auto:
mes-web/src/api/engineering.js
New file
@@ -0,0 +1,17 @@
import request from '@/utils/request'
const BASE_URL = '/api/plcSend/excel'
export const engineeringApi = {
  /**
   * 导入工程列表
   */
  importEngineer(data) {
    return request({
      url: `${BASE_URL}/importExcel`,
      method: 'post',
      data
    })
  }
}
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue
@@ -18,8 +18,17 @@
          <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>
@@ -43,20 +52,18 @@
    </el-form>
    <!-- 设备组拓扑图 -->
    <GroupTopology
      v-if="group"
      :group="group"
      class="topology-section"
    />
    <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 { 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({
@@ -102,10 +109,12 @@
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)
watch(
  () => props.group,
@@ -142,10 +151,10 @@
    const deviceList = Array.isArray(rawList)
      ? rawList
      : Array.isArray(rawList?.records)
      ? rawList.records
      : Array.isArray(rawList?.data)
      ? rawList.data
      : []
        ? rawList.records
        : Array.isArray(rawList?.data)
          ? rawList.data
          : []
    const scannerDevice = deviceList.find((item) => {
      const type = normalizeType(item.deviceType)
      return type.includes('SCANNER') || type.includes('扫码')
@@ -172,7 +181,7 @@
    ElMessage.warning('请先选择设备组')
    return
  }
  // 表单验证
  if (!formRef.value) return
  try {
@@ -181,35 +190,35 @@
    ElMessage.warning('请检查表单输入')
    return
  }
  try {
    loading.value = true
    // 构建任务参数
    // 如果输入了玻璃ID,使用输入的;如果没有输入,glassIds为空数组,后端会从数据库读取
    const parameters = {
      glassIds: glassIds.value.length > 0 ? glassIds.value : []
    }
    // 异步启动任务,立即返回,不阻塞
    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 = ''
      // 提示用户可以继续启动其他设备组
      ElMessage.info('可以继续选择其他设备组启动测试,多个设备组将并行执行')
    } else {
@@ -253,6 +262,280 @@
    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
        }
        // 发送数据到 MES 接口
        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('order') ||
      headerStr.includes('orderno') || headerStr === '订单号') {
      headerMap.orderNumber = 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 orderNumber = parseInt(row[headerMap.orderNumber]) || ''
    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
        orderNumber: orderNumber,
        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)
    if (response?.code === 200 || response?.code === 0 || response?.data) {
      ElMessage.success(`成功导入 ${glassDataList.length} 条玻璃数据,工程号:${requestData.engineerId}`)
      // 将导入的玻璃ID填充到输入框,方便用户查看和编辑
      const glassIds = glassDataList.map(item => item.glassId).filter(id => id)
      if (glassIds.length > 0) {
        glassIdsInput.value = glassIds.join('\n')
      }
    } else {
      throw new Error(response?.message || '导入失败')
    }
  } catch (error) {
    console.error('提交玻璃数据失败:', error)
    // 显示错误信息
    const errorMsg = error?.response?.data?.message || 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>
@@ -309,4 +592,3 @@
  margin-top: 24px;
}
</style>