已添加9个文件
已修改6个文件
12884 ■■■■ 文件已修改
src/components/ProcessParamListDialog.vue 631 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/dict.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/product/ProductSelectDialog.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/costAccounting/energyCosts/index.vue 2207 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/Edit.vue 257 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/ItemsForm.vue 531 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/New.vue 194 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/index.vue 2567 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/index2.vue 2417 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/processRouteItem/index.vue 1024 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionPlan/productionPlan/index.vue 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/components/formDia.vue 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionStatistics/index.vue 1003 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/salesStatistics/index.vue 1304 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/solidWasteConsumption/index1.vue 623 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ProcessParamListDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,631 @@
<template>
  <el-dialog v-model="visible"
             :title="title"
             width="800px"
             destroy-on-close>
    <div class="param-list-container">
      <div class="params-header">
        <span>参数列表</span>
        <el-button v-if="editable"
                   type="primary"
                   link
                   size="small"
                   @click="handleAddParam">
          <el-icon>
            <Plus />
          </el-icon>新增
        </el-button>
      </div>
      <div class="params-list">
        <div v-for="param in paramList"
             :key="param.id"
             class="param-item">
          <div class="param-info">
            <span class="param-code">{{ param.paramName }}</span>
            <span v-if="param.valueMode == 1"
                  class="param-value">
              æ ‡å‡†å€¼ï¼š{{ param.standardValue || "-" }} {{ param.unit }}
            </span>
            <span v-else
                  class="param-value">
              æ ‡å‡†å€¼ï¼š{{ param.minValue || "-" }}-{{ param.maxValue || "-" }} {{ param.unit }}
            </span>
          </div>
          <div class="param-actions">
            <el-button v-if="editable"
                       link
                       type="primary"
                       size="small"
                       @click="handleEditParam(param)">
              ç¼–辑
            </el-button>
            <el-button v-if="editable"
                       link
                       type="danger"
                       size="small"
                       @click="handleDeleteParam(param)">
              åˆ é™¤
            </el-button>
          </div>
        </div>
        <el-empty v-if="!paramList || paramList.length === 0"
                  description="暂无参数"
                  :image-size="50" />
      </div>
    </div>
    <!-- é€‰æ‹©å‚数对话框 -->
    <el-dialog v-model="selectParamDialogVisible"
               title="选择参数"
               width="1000px">
      <div class="param-select-container">
        <!-- å·¦ä¾§å‚数列表 -->
        <div class="param-list-area">
          <div class="area-title">可选参数</div>
          <div class="search-box">
            <el-input v-model="paramSearchKeyword"
                      placeholder="请输入参数名称搜索"
                      clearable
                      size="small"
                      @input="getBaseParamListData">
              <template #prefix>
                <el-icon>
                  <Search />
                </el-icon>
              </template>
            </el-input>
          </div>
          <el-table :data="filteredParamList"
                    height="400"
                    border
                    highlight-current-row
                    @current-change="handleSelectParam">
            <el-table-column prop="paramName"
                             label="参数名称" />
            <el-table-column prop="paramType"
                             label="参数类型">
              <template #default="scope">
                <el-tag size="small"
                        :type="getParamTypeTag(scope.row.paramType)">{{ getParamTypeText(scope.row.paramType) }}</el-tag>
              </template>
            </el-table-column>
          </el-table>
          <!-- åˆ†é¡µæŽ§ä»¶ -->
          <div class="pagination-container"
               style="margin-top: 10px;">
            <el-pagination v-model:current-page="paramPage.current"
                           v-model:page-size="paramPage.size"
                           :page-sizes="[10, 20, 50, 100]"
                           layout="total, sizes, prev, pager, next, jumper"
                           :total="paramPage.total"
                           @size-change="getBaseParamListData"
                           @current-change="getBaseParamListData"
                           size="small" />
          </div>
        </div>
        <!-- å³ä¾§å‚数详情 -->
        <div class="param-detail-area">
          <div class="area-title">参数详情</div>
          <el-form v-if="selectedParam"
                   :model="selectedParam"
                   label-width="100px"
                   class="param-detail-form">
            <el-form-item label="参数名称">
              <span class="detail-text">{{ selectedParam.paramName }}</span>
            </el-form-item>
            <el-form-item label="参数模式">
              <el-tag size="small"
                      :type="selectedParam.valueMode == '1' ? 'success' : 'warning'">
                {{ selectedParam.valueMode == '1' ? '单值' : '区间' }}
              </el-tag>
            </el-form-item>
            <el-form-item label="参数类型">
              <el-tag size="small"
                      :type="getParamTypeTag(selectedParam.paramType)">{{ getParamTypeText(selectedParam.paramType) }}</el-tag>
            </el-form-item>
            <el-form-item label="参数格式">
              <span class="detail-text">{{ selectedParam.paramFormat || '-' }}</span>
            </el-form-item>
            <el-form-item label="单位">
              <span class="detail-text">{{ selectedParam.unit || '-' }}</span>
            </el-form-item>
            <el-form-item label="标准值"
                          v-if="selectedParam.valueMode == '1' && selectedParam.paramType == '1'">
              <el-input v-model="selectedParam.standardValue"
                        type="number"
                        placeholder="请输入默认值" />
            </el-form-item>
            <el-form-item label="最小值"
                          v-if="selectedParam.valueMode == '2' && selectedParam.paramType == '1'">
              <el-input v-model="selectedParam.minValue"
                        type="number"
                        placeholder="请输入最小值" />
            </el-form-item>
            <el-form-item label="最大值"
                          v-if="selectedParam.valueMode == '2' && selectedParam.paramType == '1'">
              <el-input v-model="selectedParam.maxValue"
                        type="number"
                        placeholder="请输入最大值" />
            </el-form-item>
            <el-form-item label="排序">
              <el-input v-model="selectedParam.sort"
                        type="number"
                        placeholder="请输入排序" />
            </el-form-item>
            <el-form-item label="是否必填">
              <el-switch v-model="selectedParam.isRequired" />
            </el-form-item>
          </el-form>
          <el-empty v-else
                    description="请从左侧选择参数"
                    :image-size="100" />
        </div>
      </div>
      <template #footer>
        <el-button @click="selectParamDialogVisible = false">取消</el-button>
        <el-button type="primary"
                   @click="handleParamSelectSubmit">确定</el-button>
      </template>
    </el-dialog>
    <!-- ç¼–辑参数对话框 -->
    <el-dialog v-model="editParamDialogVisible"
               title="编辑参数"
               width="600px">
      <el-form :model="editParamForm"
               :rules="editParamRules"
               ref="editParamFormRef"
               label-width="120px">
        <el-form-item label="参数名称">
          <span class="detail-text">{{ editParamForm.paramName }}</span>
        </el-form-item>
        <el-form-item label="参数模式">
          <el-tag size="small"
                  :type="editParamForm.valueMode == '1' ? 'success' : 'warning'">
            {{ editParamForm.valueMode == '1' ? '单值' : '区间' }}
          </el-tag>
        </el-form-item>
        <el-form-item label="参数类型">
          <el-tag size="small"
                  :type="getParamTypeTag(editParamForm.paramType)">
            {{ getParamTypeText(editParamForm.paramType) }}
          </el-tag>
        </el-form-item>
        <el-form-item label="参数格式">
          <span class="detail-text">{{ editParamForm.paramFormat || '-' }}</span>
        </el-form-item>
        <el-form-item label="单位">
          <span class="detail-text">{{ editParamForm.unit || '-' }}</span>
        </el-form-item>
        <el-form-item label="标准值"
                      v-if="editParamForm.valueMode == '1' && editParamForm.paramType == '1'"
                      prop="standardValue">
          <el-input v-model="editParamForm.standardValue"
                    type="number"
                    placeholder="请输入标准值" />
        </el-form-item>
        <el-form-item label="最小值"
                      v-if="editParamForm.valueMode == '2' && editParamForm.paramType == '1'"
                      prop="minValue">
          <el-input v-model="editParamForm.minValue"
                    type="number"
                    placeholder="请输入最小值" />
        </el-form-item>
        <el-form-item label="最大值"
                      v-if="editParamForm.valueMode == '2' && editParamForm.paramType == '1'"
                      prop="maxValue">
          <el-input v-model="editParamForm.maxValue"
                    type="number"
                    placeholder="请输入最大值" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="editParamDialogVisible = false">取消</el-button>
        <el-button type="primary"
                   @click="handleEditParamSubmit">确定</el-button>
      </template>
    </el-dialog>
  </el-dialog>
</template>
<script setup>
  import { ref, computed, watch } from "vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import { Plus, Search } from "@element-plus/icons-vue";
  import {
    delProcessRouteItemParam,
    editProcessRouteItemParam,
    addProcessRouteItemParam,
  } from "@/api/productionManagement/processRouteItem.js";
  import { getBaseParamList } from "@/api/basicData/parameterMaintenance.js";
  const props = defineProps({
    modelValue: {
      type: Boolean,
      default: false,
    },
    title: {
      type: String,
      default: "参数列表",
    },
    routeId: {
      type: Number,
      default: 0,
    },
    process: {
      type: Object,
      default: () => ({}),
    },
    paramList: {
      type: Array,
      default: () => [],
    },
    editable: {
      type: Boolean,
      default: true,
    },
  });
  const emit = defineEmits(["update:modelValue", "refresh"]);
  const visible = computed({
    get: () => props.modelValue,
    set: value => emit("update:modelValue", value),
  });
  // å“åº”式数据
  const selectParamDialogVisible = ref(false);
  const editParamDialogVisible = ref(false);
  const paramSearchKeyword = ref("");
  const selectedParam = ref(null);
  const filteredParamList = ref([]);
  const paramPage = ref({
    current: 1,
    size: 10,
    total: 0,
  });
  const editParamForm = ref({
    id: null,
    processId: null,
    paramId: null,
    paramName: "",
    valueMode: "1",
    standardValue: null,
    minValue: null,
    maxValue: null,
    sort: 1,
    isRequired: 0,
    paramType: null,
    paramFormat: "",
    unit: "",
  });
  const editParamRules = ref({
    standardValue: [{ required: true, message: "请输入标准值", trigger: "blur" }],
    minValue: [{ required: true, message: "请输入最小值", trigger: "blur" }],
    maxValue: [{ required: true, message: "请输入最大值", trigger: "blur" }],
  });
  const editParamFormRef = ref(null);
  // æ–°å¢žå‚æ•°
  const handleAddParam = () => {
    selectedParam.value = null;
    paramSearchKeyword.value = "";
    paramPage.current = 1;
    // èŽ·å–å¯é€‰å‚æ•°åˆ—è¡¨
    getBaseParamListData();
    selectParamDialogVisible.value = true;
  };
  // ç¼–辑参数
  const handleEditParam = param => {
    editParamForm.value = {
      id: param.id,
      processId: props.process.id,
      paramId: param.paramId,
      paramName: param.parameterName || param.paramName,
      valueMode: param.parameterType2 || param.valueMode || "1",
      standardValue: param.standardValue,
      minValue: param.minValue,
      maxValue: param.maxValue,
      sort: param.sort || 1,
      isRequired: param.isRequired || 0,
      paramType: param.parameterType || param.paramType,
      paramFormat: param.parameterFormat || param.paramFormat,
      unit: param.unit || param.unit,
    };
    editParamDialogVisible.value = true;
  };
  // åˆ é™¤å‚æ•°
  const handleDeleteParam = param => {
    ElMessageBox.confirm("确定要删除该参数吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        // è°ƒç”¨API删除参数
        delProcessRouteItemParam(param.id)
          .then(res => {
            ElMessage.success("删除成功");
            emit("refresh");
          })
          .catch(err => {
            ElMessage.error("删除参数失败");
            console.error("删除参数失败:", err);
          });
      })
      .catch(() => {});
  };
  // èŽ·å–å¯é€‰å‚æ•°åˆ—è¡¨
  const getBaseParamListData = () => {
    getBaseParamList({
      paramName: paramSearchKeyword.value,
      current: paramPage.current,
      size: paramPage.size,
    }).then(res => {
      if (res.code === 200) {
        filteredParamList.value = res.data?.records || [];
        paramPage.total = res.data?.total || 0;
      } else {
        ElMessage.error(res.msg || "查询失败");
      }
    });
  };
  // é€‰æ‹©å‚æ•°
  const handleSelectParam = param => {
    selectedParam.value = param;
  };
  // æäº¤é€‰æ‹©å‚æ•°
  const handleParamSelectSubmit = () => {
    if (!selectedParam.value) {
      ElMessage.warning("请先选择一个参数");
      return;
    }
    if (!props.process || !props.process.id) {
      ElMessage.error("工艺路线项目信息不完整");
      return;
    }
    // åˆ¤æ–­å‚数类型,只有数值类型才传标准值、最大值和最小值
    const isNumericMode = selectedParam.value.valueMode === 1;
    // è°ƒç”¨API新增参数
    addProcessRouteItemParam({
      routeItemId: props.process.id,
      paramId: selectedParam.value.id,
      standardValue: isNumericMode ? selectedParam.value.standardValue || "" : "",
      minValue: isNumericMode ? selectedParam.value.minValue || 0 : null,
      maxValue: isNumericMode ? selectedParam.value.maxValue || 0 : null,
      isRequired: selectedParam.value.isRequired || 0,
      sort: selectedParam.value.sort || 1,
    })
      .then(res => {
        if (res.code === 200) {
          ElMessage.success("添加参数成功");
          selectParamDialogVisible.value = false;
          emit("refresh");
        } else {
          ElMessage.error(res.msg || "添加参数失败");
        }
      })
      .catch(err => {
        ElMessage.error("添加参数失败");
        console.error("添加参数失败:", err);
      });
  };
  // æäº¤ç¼–辑参数
  const handleEditParamSubmit = () => {
    if (!editParamFormRef.value) return;
    editParamFormRef.value.validate(valid => {
      if (valid) {
        // åˆ¤æ–­å‚数类型,只有数值类型才传标准值、最大值和最小值
        const isNumericMode = editParamForm.value.valueMode == 1;
        // è°ƒç”¨API修改参数
        editProcessRouteItemParam({
          id: editParamForm.value.id,
          routeItemId: props.process.id,
          paramId: editParamForm.value.paramId,
          standardValue: isNumericMode
            ? editParamForm.value.standardValue || ""
            : "",
          minValue: isNumericMode ? editParamForm.value.minValue || 0 : null,
          maxValue: isNumericMode ? editParamForm.value.maxValue || 0 : null,
          isRequired: editParamForm.value.isRequired || 0,
        })
          .then(res => {
            if (res.code === 200) {
              ElMessage.success("编辑成功");
              editParamDialogVisible.value = false;
              emit("refresh");
            } else {
              ElMessage.error(res.msg || "编辑失败");
            }
          })
          .catch(err => {
            ElMessage.error("编辑参数失败");
            console.error("编辑参数失败:", err);
          });
      }
    });
  };
  // èŽ·å–å‚æ•°ç±»åž‹æ ‡ç­¾
  const getParamTypeTag = type => {
    const typeMap = {
      1: "primary",
      2: "info",
      3: "warning",
      4: "success",
    };
    return typeMap[type] || "default";
  };
  // èŽ·å–å‚æ•°ç±»åž‹æ–‡æœ¬
  const getParamTypeText = type => {
    const typeMap = {
      1: "数值格式",
      2: "文本格式",
      3: "下拉选项",
      4: "时间格式",
    };
    return typeMap[type] || type;
  };
  watch(
    () => props.modelValue,
    newVal => {
      if (!newVal) {
        // å¼¹çª—关闭时重置数据
        selectParamDialogVisible.value = false;
        editParamDialogVisible.value = false;
        selectedParam.value = null;
        paramSearchKeyword.value = "";
        paramPage.current = 1;
        filteredParamList.value = [];
        editParamForm.value = {
          id: null,
          processId: null,
          paramId: null,
          paramName: "",
          valueMode: "1",
          standardValue: null,
          minValue: null,
          maxValue: null,
          sort: 1,
          isRequired: 0,
          paramType: null,
          paramFormat: "",
          unit: "",
        };
      }
    }
  );
</script>
<style scoped>
  .param-list-container {
    padding: 10px 0;
  }
  .params-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 15px;
    padding-bottom: 10px;
    border-bottom: 1px solid #e4e7ed;
  }
  .params-header span {
    font-size: 16px;
    font-weight: 500;
    color: #303133;
  }
  .params-list {
    max-height: 400px;
    overflow-y: auto;
  }
  .param-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 12px 16px;
    margin-bottom: 8px;
    background-color: #f9f9f9;
    border-radius: 4px;
    transition: all 0.3s ease;
  }
  .param-item:hover {
    background-color: #ecf5ff;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  }
  .param-info {
    display: flex;
    align-items: center;
    gap: 20px;
    flex: 1;
  }
  .param-code {
    font-weight: 500;
    color: #303133;
    min-width: 120px;
  }
  .param-value {
    color: #606266;
    font-size: 14px;
  }
  .param-actions {
    display: flex;
    gap: 10px;
  }
  /* æ»šåŠ¨æ¡æ ·å¼ */
  .params-list::-webkit-scrollbar {
    width: 6px;
  }
  .params-list::-webkit-scrollbar-track {
    background: #f1f1f1;
    border-radius: 3px;
  }
  .params-list::-webkit-scrollbar-thumb {
    background: #c1c1c1;
    border-radius: 3px;
  }
  .params-list::-webkit-scrollbar-thumb:hover {
    background: #a8a8a8;
  }
  /* é€‰æ‹©å‚数对话框样式 */
  .param-select-container {
    display: flex;
    gap: 20px;
  }
  .param-list-area {
    flex: 1;
    min-width: 400px;
  }
  .param-detail-area {
    flex: 1;
    min-width: 300px;
  }
  .area-title {
    font-size: 14px;
    font-weight: 500;
    margin-bottom: 10px;
    color: #303133;
  }
  .search-box {
    display: flex;
    gap: 10px;
    margin-bottom: 10px;
  }
  .param-detail-form {
    background: #f9f9f9;
    padding: 15px;
    border-radius: 4px;
  }
  .detail-text {
    font-weight: 500;
  }
</style>
src/utils/dict.js
@@ -14,7 +14,7 @@
        res.value[dictType] = dicts
      } else {
        getDicts(dictType).then(resp => {
          res.value[dictType] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass }))
          res.value[dictType] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue,id: p.dictCode, elTagType: p.listClass, elTagClass: p.cssClass }))
          useDictStore().setDict(dictType, res.value[dictType])
        })
      }
src/views/basicData/product/ProductSelectDialog.vue
@@ -219,12 +219,19 @@
    }
  }
  // ç›‘听弹窗打开,重置选择
  // ç›‘听弹窗打开,重置选择和搜索条件
  watch(
    () => props.modelValue,
    visible => {
      if (visible) {
        multipleSelection.value = [];
        // é‡ç½®æœç´¢æ¡ä»¶
        query.model = "";
        query.materialCode = "";
        query.productName = "";
        page.pageNum = 1;
        // é‡æ–°åŠ è½½æ•°æ®
        loadData();
      }
    }
  );
src/views/costAccounting/energyCosts/index.vue
@@ -1,204 +1,479 @@
// èƒ½è€—成本核算
<template>
  <div class="app-container">
    <!-- æœç´¢åŒºåŸŸ -->
    <div class="search_form">
      <el-form :model="searchForm"
               :inline="true">
        <el-form-item label="统计维度:">
          <el-radio-group v-model="statisticsType"
                          @change="handleTypeChange">
            <el-radio-button label="day">按日统计</el-radio-button>
            <el-radio-button label="month">按月统计</el-radio-button>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="能耗类型:">
          <el-select v-model="searchForm.energyType"
                     placeholder="全部"
                     clearable
                     style="width: 140px;"
                     @change="handleQuery">
            <el-option label="全部"
                       value="全部" />
            <el-option label="æ°´"
                       value="æ°´" />
            <el-option label="电"
                       value="电" />
            <el-option label="气"
                       value="气" />
          </el-select>
        </el-form-item>
        <el-form-item label="能耗用途:">
          <el-select v-model="searchForm.energyPurpose"
                     placeholder="全部"
                     clearable
                     style="width: 140px;"
                     @change="handleQuery">
            <el-option label="全部"
                       value="全部" />
            <el-option label="生产"
                       value="生产" />
            <el-option label="办公"
                       value="办公" />
          </el-select>
        </el-form-item>
        <el-form-item label="时间范围:">
          <el-date-picker v-if="statisticsType === 'day'"
                          v-model="searchForm.dateRange"
                          type="daterange"
                          range-separator="至"
                          start-placeholder="开始日期"
                          end-placeholder="结束日期"
                          value-format="YYYY-MM-DD"
                          style="width: 240px;"
                          @change="handleQuery" />
          <el-date-picker v-else
                          v-model="searchForm.monthRange"
                          type="monthrange"
                          range-separator="至"
                          start-placeholder="开始月份"
                          end-placeholder="结束月份"
                          value-format="YYYY-MM"
                          style="width: 240px;"
                          @change="handleQuery" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary"
                     @click="handleQuery">查询</el-button>
          <el-button @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>
      <div>
        <el-button type="success"
                   @click="handleExport">导出报表</el-button>
  <div class="energy-cost-page">
    <!-- ç­›é€‰åŒºåŸŸ -->
    <el-card class="filter-card"
             shadow="never">
      <template #header>
        <div class="card-head">
          <div class="card-head-left">
            <el-icon class="card-icon ui-icon">
              <DataLine />
            </el-icon>
            <span class="card-title">查询条件</span>
          </div>
          <div class="card-head-right">
            <el-radio-group v-model="statisticsType"
                            size="small"
                            @change="handleTypeChange">
              <el-radio-button label="day">按日</el-radio-button>
              <el-radio-button label="month">按月</el-radio-button>
            </el-radio-group>
          </div>
        </div>
      </template>
      <div class="filter-layout">
        <el-form :model="searchForm"
                 :inline="true"
                 class="filter-form">
          <el-form-item label="能耗类型">
            <el-select v-model="searchForm.energyType"
                       placeholder="全部"
                       clearable
                       class="w-140"
                       @change="handleQuery">
              <el-option label="全部"
                         value="全部" />
              <el-option label="æ°´"
                         value="æ°´" />
              <el-option label="电"
                         value="电" />
              <el-option label="气"
                         value="气" />
            </el-select>
          </el-form-item>
          <el-form-item label="能耗用途">
            <el-select v-model="searchForm.energyPurpose"
                       placeholder="全部"
                       clearable
                       class="w-140"
                       @change="handleQuery">
              <el-option label="全部"
                         value="全部" />
              <el-option label="生产"
                         value="生产" />
              <el-option label="办公"
                         value="办公" />
            </el-select>
          </el-form-item>
          <el-form-item label="时间范围">
            <el-date-picker v-if="statisticsType === 'day'"
                            v-model="searchForm.dateRange"
                            type="daterange"
                            range-separator="至"
                            start-placeholder="开始日期"
                            end-placeholder="结束日期"
                            value-format="YYYY-MM-DD"
                            class="w-260"
                            @change="handleQuery" />
            <el-date-picker v-else
                            v-model="searchForm.monthRange"
                            type="monthrange"
                            range-separator="至"
                            start-placeholder="开始月份"
                            end-placeholder="结束月份"
                            value-format="YYYY-MM"
                            class="w-260"
                            @change="handleQuery" />
          </el-form-item>
        </el-form>
        <div class="filter-actions">
          <el-button class="lux-btn"
                     type="primary"
                     :loading="tableLoading"
                     @click="handleQuery">刷新</el-button>
          <el-button class="lux-btn"
                     @click="handleReset">重置</el-button>
          <el-button class="lux-btn"
                     type="success"
                     plain
                     @click="handleExport">导出</el-button>
        </div>
      </div>
    </div>
    <!-- ç»Ÿè®¡æ¦‚览卡片 -->
    <div class="statistics-overview">
      <h2 class="section-header">
        <el-icon class="header-icon">
          <DataLine />
        </el-icon>
        èƒ½è€—成本概览
      </h2>
      <el-row :gutter="20">
        <el-col :span="6">
          <div class="overview-card blue-card">
            <div class="overview-icon blue-icon">
              <el-icon>
    </el-card>
    <!-- å›¾è¡¨åŒºåŸŸ -->
    <div class="charts">
      <el-card class="panel-card"
               shadow="never">
        <div class="kpi-strip"
             :class="{ pulse: queryPulse }"
             title="快捷键:Enter åˆ·æ–° / Esc é‡ç½® / Alt+E å¯¼å‡º">
          <button class="kpi-item kpi-total"
                  type="button"
                  :class="{ selected: selectedKpi === 'all' }"
                  @click="handleKpiClick('all')">
            <div class="kpi-left">
              <div class="kpi-label">总能耗成本</div>
              <div class="kpi-value">Â¥{{ formatMoney(animatedOverview.totalCost) }}</div>
              <div class="kpi-meta">
                <span class="kpi-chip"
                      :class="kpiDelta.total.pct >= 0 ? 'up' : 'down'"
                      v-if="kpiDelta.total.valid">{{ kpiDelta.total.pct >= 0 ? '+' : '' }}{{ kpiDelta.total.pct.toFixed(1) }}%</span>
                <svg class="kpi-spark"
                     viewBox="0 0 72 22"
                     aria-hidden="true">
                  <polyline :points="sparklinePoints(kpiSeries.total)"
                            fill="none"
                            stroke="rgba(47, 111, 237, 0.85)"
                            stroke-width="2"
                            stroke-linecap="round"
                            stroke-linejoin="round" />
                </svg>
              </div>
            </div>
            <div class="kpi-icon">
              <el-icon class="ui-icon">
                <Money />
              </el-icon>
            </div>
            <div class="overview-info">
              <div class="overview-label">总能耗成本</div>
              <div class="overview-value">Â¥{{ overview.totalCost }}</div>
            <div class="kpi-actions"
                 @click.stop>
              <button class="kpi-action"
                      type="button"
                      @click="copyKpi('totalCost')">复制</button>
              <button class="kpi-action"
                      type="button"
                      @click="viewKpiDetails('all')">明细</button>
            </div>
          </div>
        </el-col>
        <el-col :span="6">
          <div class="overview-card green-card">
            <div class="overview-icon green-icon">
              <el-icon>
          </button>
          <button class="kpi-item kpi-production"
                  type="button"
                  :class="{ selected: selectedKpi === 'production' }"
                  @click="handleKpiClick('production')">
            <div class="kpi-left">
              <div class="kpi-label">生产能耗成本</div>
              <div class="kpi-value">Â¥{{ formatMoney(animatedOverview.productionCost) }}</div>
              <div class="kpi-meta">
                <span class="kpi-chip"
                      :class="kpiDelta.production.pct >= 0 ? 'up' : 'down'"
                      v-if="kpiDelta.production.valid">{{ kpiDelta.production.pct >= 0 ? '+' : '' }}{{ kpiDelta.production.pct.toFixed(1) }}%</span>
                <svg class="kpi-spark"
                     viewBox="0 0 72 22"
                     aria-hidden="true">
                  <polyline :points="sparklinePoints(kpiSeries.production)"
                            fill="none"
                            stroke="rgba(22, 163, 74, 0.85)"
                            stroke-width="2"
                            stroke-linecap="round"
                            stroke-linejoin="round" />
                </svg>
              </div>
            </div>
            <div class="kpi-icon">
              <el-icon class="ui-icon">
                <DataLine />
              </el-icon>
            </div>
            <div class="overview-info">
              <div class="overview-label">生产能耗成本</div>
              <div class="overview-value">Â¥{{ overview.productionCost }}</div>
            <div class="kpi-actions"
                 @click.stop>
              <button class="kpi-action"
                      type="button"
                      @click="copyKpi('productionCost')">复制</button>
              <button class="kpi-action"
                      type="button"
                      @click="viewKpiDetails('production')">明细</button>
            </div>
          </div>
        </el-col>
        <el-col :span="6">
          <div class="overview-card purple-card">
            <div class="overview-icon purple-icon">
              <el-icon>
          </button>
          <button class="kpi-item kpi-office"
                  type="button"
                  :class="{ selected: selectedKpi === 'office' }"
                  @click="handleKpiClick('office')">
            <div class="kpi-left">
              <div class="kpi-label">办公能耗成本</div>
              <div class="kpi-value">Â¥{{ formatMoney(animatedOverview.officeCost) }}</div>
              <div class="kpi-meta">
                <span class="kpi-chip"
                      :class="kpiDelta.office.pct >= 0 ? 'up' : 'down'"
                      v-if="kpiDelta.office.valid">{{ kpiDelta.office.pct >= 0 ? '+' : '' }}{{ kpiDelta.office.pct.toFixed(1) }}%</span>
                <svg class="kpi-spark"
                     viewBox="0 0 72 22"
                     aria-hidden="true">
                  <polyline :points="sparklinePoints(kpiSeries.office)"
                            fill="none"
                            stroke="rgba(100, 116, 139, 0.90)"
                            stroke-width="2"
                            stroke-linecap="round"
                            stroke-linejoin="round" />
                </svg>
              </div>
            </div>
            <div class="kpi-icon">
              <el-icon class="ui-icon">
                <TrendCharts />
              </el-icon>
            </div>
            <div class="overview-info">
              <div class="overview-label">办公能耗成本</div>
              <div class="overview-value">Â¥{{ overview.officeCost }}</div>
            <div class="kpi-actions"
                 @click.stop>
              <button class="kpi-action"
                      type="button"
                      @click="copyKpi('officeCost')">复制</button>
              <button class="kpi-action"
                      type="button"
                      @click="viewKpiDetails('office')">明细</button>
            </div>
          </div>
        </el-col>
        <el-col :span="6">
          <div class="overview-card gray-card">
            <div class="overview-icon gray-icon">
              <el-icon>
          </button>
          <button class="kpi-item kpi-avg"
                  type="button"
                  @click="handleKpiClick('all')">
            <div class="kpi-left">
              <div class="kpi-label">平均成本</div>
              <div class="kpi-value">Â¥{{ formatMoney(animatedOverview.avgCost) }} <span class="kpi-unit">/{{ statisticsType === 'day' ? '日' : '月' }}</span></div>
              <div class="kpi-meta muted">基于当前筛选与明细统计</div>
            </div>
            <div class="kpi-icon">
              <el-icon class="ui-icon">
                <Histogram />
              </el-icon>
            </div>
            <div class="overview-info">
              <div class="overview-label">平均能耗成本</div>
              <div class="overview-value">Â¥{{ overview.avgCost }} <span class="unit">/{{ statisticsType === 'day' ? '日' : '月' }}</span></div>
            <div class="kpi-actions"
                 @click.stop>
              <button class="kpi-action"
                      type="button"
                      @click="copyKpi('avgCost')">复制</button>
              <button class="kpi-action"
                      type="button"
                      @click="viewKpiDetails('all')">明细</button>
            </div>
          </button>
        </div>
        <div class="panel-head">
          <div class="segmented"
               role="tablist"
               aria-label="分析面板切换"
               :class="{ 'no-active': chartPanel === 'none' }">
            <div class="segmented-indicator"
                 :class="{ hidden: chartPanel === 'none' }"
                 :style="panelIndicatorStyle"></div>
            <button class="segmented-item"
                    type="button"
                    role="tab"
                    :aria-selected="chartPanel === 'core'"
                    :class="{ active: chartPanel === 'core' }"
                    @click="handleChartPanelClick('core')">
              <span class="seg-title">核心分析</span>
              <span class="seg-sub">趋势 / ç±»åž‹å æ¯”</span>
            </button>
            <button class="segmented-item"
                    type="button"
                    role="tab"
                    :aria-selected="chartPanel === 'advanced'"
                    :class="{ active: chartPanel === 'advanced' }"
                    @click="handleChartPanelClick('advanced')">
              <span class="seg-title">高级分析</span>
              <span class="seg-sub">用途占比 / å•价对比</span>
            </button>
          </div>
        </el-col>
      </el-row>
        </div>
        <transition name="lux-collapse">
          <div v-show="chartPanel === 'core'"
               class="panel-body">
            <el-row :gutter="16">
              <el-col :xs="24"
                      :lg="12">
                <el-card class="chart-card"
                         shadow="never">
                  <template #header>
                    <div class="chart-head">
                      <span class="chart-title">能耗成本趋势</span>
                      <div class="chart-tools"
                           @click.stop>
                        <button class="chart-tool"
                                type="button"
                                @click="downloadChart('cost', '能耗成本趋势')">下载</button>
                        <button class="chart-tool"
                                type="button"
                                @click="openBigChart('cost', '能耗成本趋势')">大图</button>
                      </div>
                    </div>
                  </template>
                  <div ref="costChartWrap"
                       class="chart-wrap"
                       v-loading="tableLoading">
                    <div ref="costChart"
                         class="chart-content"
                         v-show="hasTableData"></div>
                    <div class="chart-empty"
                         v-show="!hasTableData">
                      <el-empty description="暂无数据" />
                    </div>
                  </div>
                </el-card>
              </el-col>
              <el-col :xs="24"
                      :lg="12">
                <el-card class="chart-card"
                         shadow="never">
                  <template #header>
                    <div class="chart-head">
                      <span class="chart-title">能耗类型成本占比</span>
                      <div class="chart-tools"
                           @click.stop>
                        <button class="chart-tool"
                                type="button"
                                @click="downloadChart('type', '能耗类型成本占比')">下载</button>
                        <button class="chart-tool"
                                type="button"
                                @click="openBigChart('type', '能耗类型成本占比')">大图</button>
                      </div>
                    </div>
                  </template>
                  <div ref="typeChartWrap"
                       class="chart-wrap"
                       v-loading="tableLoading">
                    <div ref="typeChart"
                         class="chart-content"
                         v-show="hasTableData"></div>
                    <div class="chart-empty"
                         v-show="!hasTableData">
                      <el-empty description="暂无数据" />
                    </div>
                  </div>
                </el-card>
              </el-col>
            </el-row>
          </div>
        </transition>
        <transition name="lux-collapse">
          <div v-show="chartPanel === 'advanced'"
               class="panel-body">
            <el-row :gutter="16"
                    class="charts-row">
              <el-col :xs="24"
                      :lg="12">
                <el-card class="chart-card"
                         shadow="never">
                  <template #header>
                    <div class="chart-head">
                      <span class="chart-title">能耗用途成本占比</span>
                      <div class="chart-tools"
                           @click.stop>
                        <button class="chart-tool"
                                type="button"
                                @click="downloadChart('purpose', '能耗用途成本占比')">下载</button>
                        <button class="chart-tool"
                                type="button"
                                @click="openBigChart('purpose', '能耗用途成本占比')">大图</button>
                      </div>
                    </div>
                  </template>
                  <div ref="purposeChartWrap"
                       class="chart-wrap"
                       v-loading="tableLoading">
                    <div ref="purposeChart"
                         class="chart-content"
                         v-show="hasTableData"></div>
                    <div class="chart-empty"
                         v-show="!hasTableData">
                      <el-empty description="暂无数据" />
                    </div>
                  </div>
                </el-card>
              </el-col>
              <el-col :xs="24"
                      :lg="12">
                <el-card class="chart-card"
                         shadow="never">
                  <template #header>
                    <div class="chart-head">
                      <span class="chart-title">能耗单价对比</span>
                      <div class="chart-tools"
                           @click.stop>
                        <button class="chart-tool"
                                type="button"
                                @click="downloadChart('price', '能耗单价对比')">下载</button>
                        <button class="chart-tool"
                                type="button"
                                @click="openBigChart('price', '能耗单价对比')">大图</button>
                      </div>
                    </div>
                  </template>
                  <div ref="priceChartWrap"
                       class="chart-wrap"
                       v-loading="tableLoading">
                    <div ref="priceChart"
                         class="chart-content"
                         v-show="hasTableData"></div>
                    <div class="chart-empty"
                         v-show="!hasTableData">
                      <el-empty description="暂无数据" />
                    </div>
                  </div>
                </el-card>
              </el-col>
            </el-row>
          </div>
        </transition>
      </el-card>
    </div>
    <!-- å›¾è¡¨åŒºåŸŸ -->
    <div class="charts-container">
      <h2 class="section-header">
        <el-icon class="header-icon">
          <Histogram />
        </el-icon>
        èƒ½è€—成本分析
      </h2>
      <el-row :gutter="20">
        <el-col :span="12">
          <div class="chart-card">
            <div class="chart-title">能耗成本趋势</div>
            <div ref="costChart"
                 class="chart-content"></div>
          </div>
        </el-col>
        <el-col :span="12">
          <div class="chart-card">
            <div class="chart-title">能耗类型成本占比</div>
            <div ref="typeChart"
                 class="chart-content"></div>
          </div>
        </el-col>
      </el-row>
      <el-row :gutter="20"
              style="margin-top: 20px;">
        <el-col :span="12">
          <div class="chart-card">
            <div class="chart-title">能耗用途成本占比</div>
            <div ref="purposeChart"
                 class="chart-content"></div>
          </div>
        </el-col>
        <el-col :span="12">
          <div class="chart-card">
            <div class="chart-title">能耗单价对比</div>
            <div ref="priceChart"
                 class="chart-content"></div>
          </div>
        </el-col>
      </el-row>
    </div>
    <el-dialog v-model="bigChartVisible"
               :title="bigChartTitle"
               width="92%"
               top="6vh"
               class="big-chart-dialog"
               destroy-on-close
               @opened="handleBigChartOpened"
               @closed="handleBigChartClosed">
      <div ref="bigChartEl"
           class="big-chart-canvas"></div>
      <template #footer>
        <div class="big-chart-footer">
          <el-button class="lux-btn"
                     @click="downloadChart(bigChartKey, bigChartTitle)">下载图片</el-button>
          <el-button class="lux-btn"
                     type="primary"
                     @click="bigChartVisible = false">关闭</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- æ•°æ®è¡¨æ ¼ -->
    <div class="table-section">
      <h2 class="section-header">
        <el-icon class="header-icon">
          <List />
        </el-icon>
        è¯¦ç»†æ•°æ®
      </h2>
      <el-table :data="tableData"
    <el-card class="table-card"
             shadow="never">
      <div ref="tableAnchor"></div>
      <template #header>
        <div class="card-head">
          <div class="card-head-left">
            <el-icon class="card-icon ui-icon">
              <List />
            </el-icon>
            <span class="card-title">明细数据</span>
          </div>
          <div class="card-head-right subtle">
            <span>共 {{ page.total }} æ¡</span>
          </div>
        </div>
      </template>
      <el-table :data="displayTableData"
                v-loading="tableLoading"
                border>
                stripe
                :header-cell-style="{ height: '44px' }"
                class="data-table lux-table"
                @sort-change="handleSortChange">
        <template #empty>
          <el-empty description="暂无明细数据" />
        </template>
        <el-table-column type="index"
                         label="序号"
                         width="60"
                         align="center" />
        <el-table-column prop="timePeriod"
                         :label="timeColumnLabel"
                         align="center" />
                         align="center"
                         sortable="custom" />
        <el-table-column prop="energyType"
                         label="能耗类型"
                         width="100"
                         align="center">
                         align="center"
                         :filters="energyTypeFilters"
                         :filter-method="filterEnergyType"
                         filter-placement="bottom-end">
          <template #default="scope">
            <el-tag :type="getEnergyTypeType(scope.row.energyType)">
              {{ scope.row.energyType }}
@@ -208,7 +483,10 @@
        <el-table-column prop="energyPurpose"
                         label="能耗用途"
                         width="100"
                         align="center">
                         align="center"
                         :filters="energyPurposeFilters"
                         :filter-method="filterEnergyPurpose"
                         filter-placement="bottom-end">
          <template #default="scope">
            <el-tag :type="scope.row.energyPurpose === '生产' ? 'primary' : 'info'">
              {{ scope.row.energyPurpose }}
@@ -219,42 +497,44 @@
                         label="用量"
                         align="right">
          <template #default="scope">
            <span class="consumption-value">{{ scope.row.consumption }}</span>
            <span class="consumption-value">{{ formatNumber(scope.row.consumption, 2) }}</span>
            <span class="consumption-unit">{{ scope.row.unit }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="price"
                         label="单价(元)"
                         align="right">
                         align="right"
                         sortable="custom">
          <template #default="scope">
            <span class="price-value">{{ scope.row.price }}</span>
            <span class="price-value">{{ formatNumber(scope.row.price, 2) }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="cost"
                         label="成本(元)"
                         align="right"
                         sortable="custom"
                         fixed="right">
          <template #default="scope">
            <span class="cost-value">Â¥{{ scope.row.cost }}</span>
            <span class="cost-value">Â¥{{ formatNumber(scope.row.cost, 2) }}</span>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <!-- åˆ†é¡µ -->
    <div class="pagination-container">
      <el-pagination v-model:current-page="page.current"
                     v-model:page-size="page.size"
                     :page-sizes="[10, 20, 50, 100]"
                     :total="page.total"
                     layout="total, sizes, prev, pager, next, jumper"
                     @size-change="handleSizeChange"
                     @current-change="handleCurrentChange" />
    </div>
      <div class="pagination-container">
        <el-pagination v-model:current-page="page.current"
                       v-model:page-size="page.size"
                       :page-sizes="[10, 20, 50, 100]"
                       :total="page.total"
                       layout="total, sizes, prev, pager, next, jumper"
                       @size-change="handleSizeChange"
                       @current-change="handleCurrentChange" />
      </div>
    </el-card>
  </div>
</template>
<script setup>
  import { ref, reactive, onMounted, computed, nextTick } from "vue";
  import { ref, reactive, onMounted, onUnmounted, computed, nextTick, watch } from "vue";
  import { ElMessage } from "element-plus";
  import {
    Money,
@@ -262,10 +542,11 @@
    TrendCharts,
    Histogram,
    List,
    ArrowDown,
  } from "@element-plus/icons-vue";
  import * as echarts from "echarts";
  import { energyCostStatistics } from "@/api/costAccounting/energyCosts";
  // import { energyCostStatistics } from "@/api/costAccounting/energyCosts";
  import { energyConsumptionDetailStatistics } from "@/api/energyManagement/energyType";
  // ç»Ÿè®¡ç»´åº¦ï¼šday-按日,month-按月
  const statisticsType = ref("day");
@@ -302,9 +583,279 @@
    avgCost: "0.00",
  });
  const selectedKpi = ref("all"); // all | production | office
  const animatedOverview = reactive({
    totalCost: 0,
    productionCost: 0,
    officeCost: 0,
    avgCost: 0,
  });
  const formatMoney = v => {
    const n = Number.parseFloat(v);
    const value = Number.isFinite(n) ? n : 0;
    return value.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
  };
  const formatNumber = (v, digits = 2) => {
    const n = Number.parseFloat(v);
    if (!Number.isFinite(n)) return "--";
    return n.toLocaleString("zh-CN", { minimumFractionDigits: digits, maximumFractionDigits: digits });
  };
  const animateNumber = (key, toValue, duration = 420) => {
    const from = animatedOverview[key] || 0;
    const to = Number.isFinite(toValue) ? toValue : 0;
    const start = performance.now();
    const easeOut = t => 1 - Math.pow(1 - t, 3);
    const tick = now => {
      const p = Math.min(1, (now - start) / duration);
      animatedOverview[key] = from + (to - from) * easeOut(p);
      if (p < 1) requestAnimationFrame(tick);
    };
    requestAnimationFrame(tick);
  };
  watch(
    () => ({ ...overview }),
    val => {
      animateNumber("totalCost", Number.parseFloat(val.totalCost));
      animateNumber("productionCost", Number.parseFloat(val.productionCost));
      animateNumber("officeCost", Number.parseFloat(val.officeCost));
      animateNumber("avgCost", Number.parseFloat(val.avgCost));
    },
    { deep: true, immediate: true }
  );
  // è¡¨æ ¼æ•°æ®
  const tableData = ref([]);
  const tableLoading = ref(false);
  const hasTableData = computed(() => Array.isArray(tableData.value) && tableData.value.length > 0);
  const queryPulse = ref(false);
  const kpiSeries = computed(() => {
    const rows = Array.isArray(tableData.value) ? tableData.value : [];
    const byTime = new Map();
    for (const r of rows) {
      const t = r?.timePeriod ?? "";
      if (!t) continue;
      if (!byTime.has(t)) byTime.set(t, { total: 0, production: 0, office: 0 });
      const bucket = byTime.get(t);
      const c = Number.parseFloat(r?.cost);
      const cost = Number.isFinite(c) ? c : 0;
      bucket.total += cost;
      if (r?.energyPurpose === "生产") bucket.production += cost;
      if (r?.energyPurpose === "办公") bucket.office += cost;
    }
    const times = Array.from(byTime.keys()).sort((a, b) => String(a).localeCompare(String(b)));
    const total = times.map(t => byTime.get(t).total);
    const production = times.map(t => byTime.get(t).production);
    const office = times.map(t => byTime.get(t).office);
    return { times, total, production, office };
  });
  const kpiDelta = computed(() => {
    const pick = arr => {
      const a = Array.isArray(arr) ? arr : [];
      if (a.length < 2) return { pct: 0, valid: false };
      const prev = a[a.length - 2];
      const cur = a[a.length - 1];
      if (!Number.isFinite(prev) || prev === 0) return { pct: 0, valid: false };
      return { pct: ((cur - prev) / prev) * 100, valid: true };
    };
    return {
      total: pick(kpiSeries.value.total),
      production: pick(kpiSeries.value.production),
      office: pick(kpiSeries.value.office),
    };
  });
  const sparklinePoints = values => {
    const v = (Array.isArray(values) ? values : []).slice(-12);
    if (v.length < 2) return "";
    const min = Math.min(...v);
    const max = Math.max(...v);
    const range = max - min || 1;
    const w = 72;
    const h = 22;
    return v
      .map((n, i) => {
        const x = (i / (v.length - 1)) * w;
        const y = h - ((n - min) / range) * h;
        return `${x.toFixed(2)},${y.toFixed(2)}`;
      })
      .join(" ");
  };
  const handleKpiClick = key => {
    selectedKpi.value = key;
    if (key === "all") searchForm.energyPurpose = "";
    if (key === "production") searchForm.energyPurpose = "生产";
    if (key === "office") searchForm.energyPurpose = "办公";
    page.current = 1;
    handleQuery();
  };
  const viewKpiDetails = key => {
    handleKpiClick(key);
    nextTick(() => {
      const el = tableAnchor.value;
      if (el?.scrollIntoView) el.scrollIntoView({ behavior: "smooth", block: "start" });
    });
  };
  const copyKpi = async field => {
    const map = {
      totalCost: animatedOverview.totalCost,
      productionCost: animatedOverview.productionCost,
      officeCost: animatedOverview.officeCost,
      avgCost: animatedOverview.avgCost,
    };
    const raw = map[field];
    const text = `Â¥${formatMoney(raw)}`;
    try {
      if (navigator?.clipboard?.writeText) {
        await navigator.clipboard.writeText(text);
      } else {
        const input = document.createElement("input");
        input.value = text;
        document.body.appendChild(input);
        input.select();
        document.execCommand("copy");
        document.body.removeChild(input);
      }
      ElMessage.success("已复制到剪贴板");
    } catch (e) {
      console.error(e);
      ElMessage.error("复制失败");
    }
  };
  const getChartByKey = key => {
    if (key === "cost") return costChartInstance;
    if (key === "type") return typeChartInstance;
    if (key === "purpose") return purposeChartInstance;
    if (key === "price") return priceChartInstance;
    return null;
  };
  const ensurePanelForChart = key => {
    if (key === "cost" || key === "type") chartPanel.value = "core";
    if (key === "purpose" || key === "price") chartPanel.value = "advanced";
  };
  const downloadChart = (key, title) => {
    ensurePanelForChart(key);
    nextTick(() => {
      ensureChartsReady(chartPanel.value === "none" ? "core" : chartPanel.value);
      const ins = getChartByKey(key);
      if (!ins) return;
      const url = ins.getDataURL({ pixelRatio: 2, backgroundColor: "#ffffff" });
      const a = document.createElement("a");
      a.href = url;
      const typePart = searchForm.energyType ? `_${searchForm.energyType}` : "";
      const purposePart = searchForm.energyPurpose ? `_${searchForm.energyPurpose}` : "";
      let rangePart = "";
      if (statisticsType.value === "day") {
        if (searchForm.dateRange?.length === 2) rangePart = `_${searchForm.dateRange[0]}~${searchForm.dateRange[1]}`;
      } else {
        if (searchForm.monthRange?.length === 2) rangePart = `_${searchForm.monthRange[0]}~${searchForm.monthRange[1]}`;
      }
      a.download = `${title || "chart"}${typePart}${purposePart}${rangePart}.png`;
      a.click();
    });
  };
  const openBigChart = (key, title) => {
    bigChartKey.value = key;
    bigChartTitle.value = title || "图表";
    bigChartVisible.value = true;
  };
  const handleBigChartOpened = () => {
    nextTick(() => {
      ensurePanelForChart(bigChartKey.value);
      ensureChartsReady(chartPanel.value === "none" ? "core" : chartPanel.value);
      const src = getChartByKey(bigChartKey.value);
      const el = bigChartEl.value;
      if (!src || !el) return;
      try {
        bigChartInstance?.dispose?.();
      } catch (e) {
        // ignore
      }
      bigChartInstance = echarts.init(el);
      const opt = src.getOption();
      bigChartInstance.setOption(opt, true);
      bigChartInstance.resize();
    });
  };
  const handleBigChartClosed = () => {
    try {
      bigChartInstance?.dispose?.();
    } catch (e) {
      // ignore
    }
    bigChartInstance = null;
  };
  const handleBigChartResize = () => {
    try {
      bigChartInstance?.resize?.();
    } catch (e) {
      // ignore
    }
  };
  // è¡¨æ ¼æŽ’序(前端排序:仅影响当前页数据,避免破坏后端分页协议)
  const sortState = reactive({
    prop: "",
    order: "",
  });
  const handleSortChange = ({ prop, order }) => {
    sortState.prop = prop || "";
    sortState.order = order || "";
  };
  const displayTableData = computed(() => {
    const data = Array.isArray(tableData.value) ? [...tableData.value] : [];
    if (!sortState.prop || !sortState.order) return data;
    const prop = sortState.prop;
    const direction = sortState.order === "ascending" ? 1 : -1;
    const numFields = new Set(["price", "cost", "consumption"]);
    return data.sort((a, b) => {
      const av = a?.[prop];
      const bv = b?.[prop];
      if (numFields.has(prop)) {
        const an = Number.parseFloat(av);
        const bn = Number.parseFloat(bv);
        const aNum = Number.isFinite(an) ? an : -Infinity;
        const bNum = Number.isFinite(bn) ? bn : -Infinity;
        return (aNum - bNum) * direction;
      }
      return String(av ?? "").localeCompare(String(bv ?? ""), "zh-Hans-CN") * direction;
    });
  });
  const energyTypeFilters = [
    { text: "æ°´", value: "æ°´" },
    { text: "电", value: "电" },
    { text: "气", value: "气" },
  ];
  const energyPurposeFilters = [
    { text: "生产", value: "生产" },
    { text: "办公", value: "办公" },
  ];
  const filterEnergyType = (value, row) => row.energyType === value;
  const filterEnergyPurpose = (value, row) => row.energyPurpose === value;
  // åˆ†é¡µ
  const page = reactive({
@@ -319,11 +870,78 @@
  const purposeChart = ref(null);
  const priceChart = ref(null);
  const costChartWrap = ref(null);
  const typeChartWrap = ref(null);
  const purposeChartWrap = ref(null);
  const priceChartWrap = ref(null);
  const tableAnchor = ref(null);
  const bigChartVisible = ref(false);
  const bigChartKey = ref("cost");
  const bigChartTitle = ref("");
  const bigChartEl = ref(null);
  let bigChartInstance = null;
  watch(bigChartVisible, v => {
    if (v) window.addEventListener("resize", handleBigChartResize);
    else window.removeEventListener("resize", handleBigChartResize);
  });
  onUnmounted(() => {
    window.removeEventListener("resize", handleBigChartResize);
    try {
      bigChartInstance?.dispose?.();
    } catch (e) {
      // ignore
    }
  });
  // å›¾è¡¨å®žä¾‹
  let costChartInstance = null;
  let typeChartInstance = null;
  let purposeChartInstance = null;
  let priceChartInstance = null;
  // å›¾è¡¨åŒºåˆ‡æ¢ï¼šcore | advanced | none(点击当前选中可收起)
  const chartPanel = ref("core");
  const ensureChartsReady = panel => {
    if (panel === "core") {
      if (costChart.value && !costChartInstance) costChartInstance = echarts.init(costChart.value);
      if (typeChart.value && !typeChartInstance) typeChartInstance = echarts.init(typeChart.value);
      if (costChartInstance) updateCostChart();
      if (typeChartInstance) updateTypeChart();
      return;
    }
    if (panel === "advanced") {
      if (purposeChart.value && !purposeChartInstance) purposeChartInstance = echarts.init(purposeChart.value);
      if (priceChart.value && !priceChartInstance) priceChartInstance = echarts.init(priceChart.value);
      if (purposeChartInstance) updatePurposeChart();
      if (priceChartInstance) updatePriceChart();
    }
  };
  const resizeChartsAfterExpand = () => {
    nextTick(() => {
      ensureChartsReady(chartPanel.value === "none" ? "core" : chartPanel.value);
      handleResize();
      updateCharts();
    });
  };
  const handleChartPanelClick = key => {
    chartPanel.value = chartPanel.value === key ? "none" : key;
  };
  const panelIndicatorStyle = computed(() => {
    const x = chartPanel.value === "advanced" ? "calc(100% + 4px)" : "0";
    return { transform: `translateX(${x})` };
  });
  watch(chartPanel, val => {
    if (val !== "none") resizeChartsAfterExpand();
  });
  // èŽ·å–èƒ½è€—ç±»åž‹æ ‡ç­¾ç±»åž‹
  const getEnergyTypeType = type => {
@@ -338,26 +956,8 @@
  // åˆå§‹åŒ–图表
  const initCharts = () => {
    nextTick(() => {
      // èƒ½è€—成本趋势图
      if (costChart.value) {
        costChartInstance = echarts.init(costChart.value);
        updateCostChart();
      }
      // èƒ½è€—类型成本占比图
      if (typeChart.value) {
        typeChartInstance = echarts.init(typeChart.value);
        updateTypeChart();
      }
      // èƒ½è€—用途成本占比图
      if (purposeChart.value) {
        purposeChartInstance = echarts.init(purposeChart.value);
        updatePurposeChart();
      }
      // èƒ½è€—单价对比图
      if (priceChart.value) {
        priceChartInstance = echarts.init(priceChart.value);
        updatePriceChart();
      }
      // åªåˆå§‹åŒ–可见面板,避免隐藏容器初始化为 0 å°ºå¯¸å¯¼è‡´ç©ºç™½
      ensureChartsReady(chartPanel.value === "none" ? "core" : chartPanel.value);
    });
  };
@@ -368,10 +968,11 @@
      tooltip: {
        trigger: "axis",
        axisPointer: { type: "shadow" },
        backgroundColor: "rgba(255, 255, 255, 0.95)",
        borderColor: "#409EFF",
        backgroundColor: "rgba(255, 255, 255, 0.96)",
        borderColor: "#2f6fed",
        borderWidth: 1,
        textStyle: { color: "#303133" },
        textStyle: { color: "rgba(15, 23, 42, 0.92)" },
        extraCssText: "box-shadow: 0 14px 40px rgba(15,23,42,.14); border-radius: 12px;",
      },
      legend: {
        data: ["生产能耗成本", "办公能耗成本"],
@@ -391,18 +992,18 @@
        data: data.map(item => item.timePeriod),
        axisLabel: {
          rotate: statisticsType.value === "day" ? 45 : 0,
          color: "#606266",
          color: "rgba(15, 23, 42, 0.62)",
        },
        axisLine: { lineStyle: { color: "#ebeef5" } },
        axisLine: { lineStyle: { color: "rgba(15, 23, 42, 0.08)" } },
        splitLine: { show: false },
      },
      yAxis: {
        type: "value",
        name: "成本(元)",
        nameTextStyle: { color: "#606266" },
        axisLabel: { color: "#606266" },
        nameTextStyle: { color: "rgba(15, 23, 42, 0.58)" },
        axisLabel: { color: "rgba(15, 23, 42, 0.58)" },
        axisLine: { show: false },
        splitLine: { lineStyle: { color: "#f0f2f5" } },
        splitLine: { lineStyle: { color: "rgba(15, 23, 42, 0.06)" } },
      },
      series: [
        {
@@ -465,15 +1066,16 @@
      tooltip: {
        trigger: "item",
        formatter: "{a} <br/>{b}: Â¥{c} ({d}%)",
        backgroundColor: "rgba(255, 255, 255, 0.95)",
        borderColor: "#409EFF",
        backgroundColor: "rgba(255, 255, 255, 0.96)",
        borderColor: "#2f6fed",
        borderWidth: 1,
        textStyle: { color: "#303133" },
        textStyle: { color: "rgba(15, 23, 42, 0.92)" },
        extraCssText: "box-shadow: 0 14px 40px rgba(15,23,42,.14); border-radius: 12px;",
      },
      legend: {
        orient: "horizontal",
        bottom: 0,
        textStyle: { color: "#606266" },
        textStyle: { color: "rgba(15, 23, 42, 0.62)" },
      },
      series: [
        {
@@ -510,7 +1112,7 @@
          data: chartData,
        },
      ],
      color: ["#409EFF", "#67C23A", "#E6A23C"],
      color: ["#2f6fed", "#16a34a", "#f59e0b"],
    };
    typeChartInstance.setOption(option);
  };
@@ -538,15 +1140,16 @@
      tooltip: {
        trigger: "item",
        formatter: "{a} <br/>{b}: Â¥{c} ({d}%)",
        backgroundColor: "rgba(255, 255, 255, 0.95)",
        borderColor: "#409EFF",
        backgroundColor: "rgba(255, 255, 255, 0.96)",
        borderColor: "#2f6fed",
        borderWidth: 1,
        textStyle: { color: "#303133" },
        textStyle: { color: "rgba(15, 23, 42, 0.92)" },
        extraCssText: "box-shadow: 0 14px 40px rgba(15,23,42,.14); border-radius: 12px;",
      },
      legend: {
        orient: "horizontal",
        bottom: 0,
        textStyle: { color: "#606266" },
        textStyle: { color: "rgba(15, 23, 42, 0.62)" },
      },
      series: [
        {
@@ -565,15 +1168,15 @@
          label: {
            show: true,
            formatter: "{b}: {d}%",
            color: "#606266",
            color: "rgba(15, 23, 42, 0.62)",
          },
          labelLine: {
            show: true,
            lineStyle: { color: "#dcdfe6" },
            lineStyle: { color: "rgba(15, 23, 42, 0.10)" },
          },
        },
      ],
      color: ["#409EFF", "#67C23A"],
      color: ["#2f6fed", "#16a34a"],
    };
    purposeChartInstance.setOption(option);
  };
@@ -603,16 +1206,17 @@
      tooltip: {
        trigger: "axis",
        axisPointer: { type: "shadow" },
        backgroundColor: "rgba(255, 255, 255, 0.95)",
        borderColor: "#409EFF",
        backgroundColor: "rgba(255, 255, 255, 0.96)",
        borderColor: "#2f6fed",
        borderWidth: 1,
        textStyle: { color: "#303133" },
        textStyle: { color: "rgba(15, 23, 42, 0.92)" },
        extraCssText: "box-shadow: 0 14px 40px rgba(15,23,42,.14); border-radius: 12px;",
      },
      legend: {
        data: ["生产能耗单价", "办公能耗单价"],
        top: 0,
        right: 10,
        textStyle: { color: "#606266" },
        textStyle: { color: "rgba(15, 23, 42, 0.62)" },
      },
      grid: {
        left: "3%",
@@ -624,17 +1228,17 @@
      xAxis: {
        type: "category",
        data: energyTypes,
        axisLabel: { color: "#606266" },
        axisLine: { lineStyle: { color: "#ebeef5" } },
        axisLabel: { color: "rgba(15, 23, 42, 0.62)" },
        axisLine: { lineStyle: { color: "rgba(15, 23, 42, 0.08)" } },
        splitLine: { show: false },
      },
      yAxis: {
        type: "value",
        name: "单价(元)",
        nameTextStyle: { color: "#606266" },
        axisLabel: { color: "#606266" },
        nameTextStyle: { color: "rgba(15, 23, 42, 0.58)" },
        axisLabel: { color: "rgba(15, 23, 42, 0.58)" },
        axisLine: { show: false },
        splitLine: { lineStyle: { color: "#f0f2f5" } },
        splitLine: { lineStyle: { color: "rgba(15, 23, 42, 0.06)" } },
      },
      series: [
        {
@@ -643,8 +1247,8 @@
          data: productionPrices,
          itemStyle: {
            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
              { offset: 0, color: "#409EFF" },
              { offset: 1, color: "#66b1ff" },
              { offset: 0, color: "#2f6fed" },
              { offset: 1, color: "#5b8cff" },
            ]),
            borderRadius: [4, 4, 0, 0],
          },
@@ -655,8 +1259,8 @@
          data: officePrices,
          itemStyle: {
            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
              { offset: 0, color: "#67C23A" },
              { offset: 1, color: "#85ce61" },
              { offset: 0, color: "#16a34a" },
              { offset: 1, color: "rgba(22, 163, 74, 0.65)" },
            ]),
            borderRadius: [4, 4, 0, 0],
          },
@@ -692,6 +1296,10 @@
  // æŸ¥è¯¢
  const handleQuery = () => {
    queryPulse.value = true;
    window.setTimeout(() => {
      queryPulse.value = false;
    }, 520);
    tableLoading.value = true;
    // æž„造请求参数
@@ -699,6 +1307,9 @@
      type: statisticsType.value,
      energyType: searchForm.energyType || undefined,
      energyPurpose: searchForm.energyPurpose || undefined,
      // é¡¹ç›®å†…常用分页参数命名
      pageNum: page.current,
      pageSize: page.size,
    };
    if (statisticsType.value === "day") {
@@ -714,7 +1325,7 @@
    }
    // è°ƒç”¨æŽ¥å£èŽ·å–æ•°æ®
    energyCostStatistics(params)
    energyConsumptionDetailStatistics(params)
      .then(res => {
        if (res.code === 200) {
          tableData.value = res.data.records || [];
@@ -731,12 +1342,22 @@
          ElMessage.error(res.message || "获取数据失败");
          tableData.value = [];
          page.total = 0;
          overview.totalCost = "0.00";
          overview.productionCost = "0.00";
          overview.officeCost = "0.00";
          overview.avgCost = "0.00";
        }
      })
      .catch(err => {
        console.error("获取数据异常:", err);
        // ç”Ÿæˆå‡æ•°æ®
        generateMockData();
        // ã€å‡æ•°æ®ï¼ˆMock)已禁用】接口异常时不再生成随机假数据,避免误用到生产数据链路
        ElMessage.error("获取数据异常");
        tableData.value = [];
        page.total = 0;
        overview.totalCost = "0.00";
        overview.productionCost = "0.00";
        overview.officeCost = "0.00";
        overview.avgCost = "0.00";
      })
      .finally(() => {
        tableLoading.value = false;
@@ -744,6 +1365,8 @@
      });
  };
  // ã€å‡æ•°æ®ï¼ˆMock)已禁用】历史上用于接口异常兜底的随机数据生成逻辑,现已整体注释,避免误用于生产。
  /*
  // ç”Ÿæˆå‡æ•°æ®
  const generateMockData = () => {
    if (statisticsType.value === "day") {
@@ -875,7 +1498,10 @@
    // æ›´æ–°ç»Ÿè®¡æ¦‚览数据
    calculateOverview();
  };
  */
  // ã€å‡æ•°æ®ï¼ˆMock)已禁用】与 generateMockData é…å¥—的前端汇总计算(仅供假数据展示),现已注释
  /*
  // è®¡ç®—统计概览数据
  const calculateOverview = () => {
    let totalCost = 0;
@@ -897,6 +1523,7 @@
    overview.officeCost = officeCost.toFixed(2);
    overview.avgCost = (totalCost / tableData.value.length).toFixed(2);
  };
  */
  // æ›´æ–°æ‰€æœ‰å›¾è¡¨
  const updateCharts = () => {
@@ -941,11 +1568,14 @@
  // åˆ†é¡µå¤§å°å˜åŒ–
  const handleSizeChange = val => {
    page.size = val;
    page.current = 1;
    handleQuery();
  };
  // é¡µç å˜åŒ–
  const handleCurrentChange = val => {
    page.current = val;
    handleQuery();
  };
  // çª—口大小变化时重新渲染图表
@@ -961,175 +1591,1052 @@
    initCharts();
    window.addEventListener("resize", handleResize);
  });
  const handleGlobalHotkeys = e => {
    // é¿å…åœ¨è¾“入框内误触
    const target = e?.target;
    const tag = target?.tagName?.toLowerCase?.();
    const isTyping =
      tag === "input" ||
      tag === "textarea" ||
      target?.isContentEditable ||
      target?.classList?.contains?.("el-input__inner");
    if (isTyping) return;
    // Enter: åˆ·æ–°æŸ¥è¯¢
    if (e.key === "Enter") {
      e.preventDefault();
      handleQuery();
      return;
    }
    // Esc: é‡ç½®
    if (e.key === "Escape") {
      e.preventDefault();
      handleReset();
      return;
    }
    // Alt+E: å¯¼å‡º
    if (e.altKey && (e.key === "e" || e.key === "E")) {
      e.preventDefault();
      handleExport();
    }
  };
  onMounted(() => {
    window.addEventListener("keydown", handleGlobalHotkeys);
  });
  onUnmounted(() => {
    window.removeEventListener("keydown", handleGlobalHotkeys);
  });
</script>
<style scoped lang="scss">
  .app-container {
    padding: 20px;
  .energy-cost-page {
    --lux-bg: #f6f7fb;
    --lux-card: rgba(255, 255, 255, 0.86);
    --lux-card-solid: #ffffff;
    --lux-border: rgba(15, 23, 42, 0.08);
    --lux-text: rgba(15, 23, 42, 0.92);
    --lux-subtle: rgba(15, 23, 42, 0.58);
    --lux-muted: rgba(15, 23, 42, 0.38);
    --lux-primary: #2f6fed;
    --lux-primary-2: #5b8cff;
    --lux-success: #16a34a;
    --lux-warning: #f59e0b;
    --lux-danger: #ef4444;
    --lux-shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
    --lux-shadow-soft: 0 10px 28px rgba(15, 23, 42, 0.06);
    --lux-radius: 14px;
    --lux-radius-sm: 12px;
    padding: 18px 22px 24px;
    background:
      radial-gradient(1200px 420px at 20% 0%, rgba(47, 111, 237, 0.10), transparent 55%),
      radial-gradient(900px 380px at 90% 10%, rgba(22, 163, 74, 0.06), transparent 55%),
      linear-gradient(180deg, var(--lux-bg) 0%, #ffffff 58%);
  }
  .search_form {
  .filter-actions {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: nowrap;
    margin-top: 0;
    padding-top: 0;
    border-top: none;
    justify-content: flex-end;
    flex: 0 0 auto;
    white-space: nowrap;
    align-self: flex-start;
    padding-bottom: 0;
    width: 290px;
  }
  .filter-actions :deep(.el-button) {
    min-width: 78px;
  }
  .filter-actions :deep(.el-button.is-loading) {
    min-width: 90px;
  }
  .filter-layout {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
    padding: 15px;
    background-color: #f5f7fa;
    border-radius: 8px;
    gap: 14px;
  }
  .statistics-overview {
    margin-bottom: 30px;
  .filter-form {
    flex: 1 1 auto;
    min-width: 0;
  }
  .charts-container {
    margin-bottom: 30px;
  .w-260 {
    width: 260px;
    max-width: 100%;
  }
  .table-section {
    margin-bottom: 20px;
  .lux-btn {
    transition: transform 0.18s ease, box-shadow 0.18s ease, filter 0.18s ease;
    &:hover {
      transform: translateY(-1px);
      box-shadow: 0 10px 22px rgba(15, 23, 42, 0.10);
      filter: saturate(1.02);
    }
    &:active {
      transform: translateY(0);
      box-shadow: none;
    }
  }
  .section-header {
  .filter-card {
    margin-bottom: 16px;
    border-radius: var(--lux-radius);
    border-color: var(--lux-border);
    background: var(--lux-card);
    backdrop-filter: blur(10px);
    box-shadow: var(--lux-shadow-soft);
  }
  /* æŸ¥è¯¢åŒºæŽ§ä»¶ç»Ÿä¸€çš®è‚¤ */
  :deep(.filter-card .el-form-item__label) {
    color: rgba(15, 23, 42, 0.70);
    font-weight: 650;
  }
  :deep(.filter-card .el-input__wrapper),
  :deep(.filter-card .el-select__wrapper) {
    border-radius: 12px;
    box-shadow: none;
    border: 1px solid rgba(15, 23, 42, 0.10);
    background: rgba(255, 255, 255, 0.82);
    transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
  }
  :deep(.filter-card .el-input__wrapper:hover),
  :deep(.filter-card .el-select__wrapper:hover) {
    border-color: rgba(47, 111, 237, 0.20);
    transform: translateY(-1px);
  }
  :deep(.filter-card .is-focus .el-input__wrapper),
  :deep(.filter-card .is-focus .el-select__wrapper) {
    border-color: rgba(47, 111, 237, 0.30);
    box-shadow: 0 0 0 3px rgba(47, 111, 237, 0.14);
  }
  :deep(.filter-card .el-range-editor.el-input__wrapper) {
    border-radius: 12px;
  }
  .card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
  }
  .card-head-left {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 200px;
  }
  .card-head-right {
    display: flex;
    align-items: center;
    gap: 10px;
  }
  .card-icon {
    color: var(--lux-primary);
  }
  .card-title {
    font-weight: 760;
    color: var(--lux-text);
  }
  .subtle {
    color: var(--lux-subtle);
    font-size: 12px;
  }
  .filter-form {
    display: flex;
    flex-wrap: nowrap;
    gap: 10px 14px;
    align-items: flex-end;
    min-width: 0;
  }
  .filter-form :deep(.el-form-item) {
    margin-right: 0;
    margin-bottom: 0;
    flex: 0 0 auto;
  }
  .filter-form :deep(.el-form-item__content) {
    min-width: 0;
  }
  .filter-form :deep(.el-form-item:last-child) {
    flex: 1 1 auto;
  }
  .filter-form :deep(.el-form-item:last-child .el-form-item__content) {
    width: 100%;
  }
  .w-140 {
    width: 140px;
  }
  .w-260 {
    width: 260px;
    max-width: 100%;
  }
  @media (max-width: 1280px) {
    .filter-form {
      flex-wrap: wrap;
      align-items: flex-start;
    }
  }
  .section-title {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 10px 0 12px;
    font-size: 14px;
    font-weight: 700;
    color: var(--lux-text);
  }
  .section-icon {
    color: var(--lux-primary);
  }
  .metrics {
    margin-bottom: 10px;
  }
  .ui-icon {
    font-size: 16px;
    transition: transform 0.18s ease, opacity 0.18s ease;
  }
  .card-head-left:hover .ui-icon,
  .section-title:hover .ui-icon {
    transform: translateY(-1px);
  }
  .metric-card {
    border-radius: var(--lux-radius-sm);
    padding: 14px 14px 14px 16px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    border: 1px solid var(--lux-border);
    background: rgba(255, 255, 255, 0.9);
    backdrop-filter: blur(10px);
    min-height: 78px;
    transition: box-shadow 0.20s ease, transform 0.20s ease, border-color 0.20s ease;
    &:hover {
      transform: translateY(-1px);
      box-shadow: var(--lux-shadow);
      border-color: rgba(47, 111, 237, 0.18);
    }
  }
  .metric-left {
    display: flex;
    flex-direction: column;
    gap: 6px;
  }
  .metric-label {
    color: var(--lux-subtle);
    font-size: 12px;
  }
  .metric-value {
    color: var(--lux-text);
    font-size: 20px;
    font-weight: 800;
    letter-spacing: 0.2px;
  }
  .metric-unit {
    font-size: 12px;
    font-weight: 500;
    color: var(--lux-muted);
    margin-left: 4px;
  }
  .metric-right {
    width: 42px;
    height: 42px;
    border-radius: 10px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .metric-icon {
    font-size: 20px;
    color: #fff;
  }
  .metric-total {
    background: linear-gradient(135deg, rgba(47, 111, 237, 0.12), rgba(47, 111, 237, 0.02));
    .metric-right {
      background: linear-gradient(135deg, var(--lux-primary), var(--lux-primary-2));
    }
  }
  .metric-production {
    background: linear-gradient(135deg, rgba(22, 163, 74, 0.12), rgba(22, 163, 74, 0.02));
    .metric-right {
      background: linear-gradient(135deg, var(--lux-success), rgba(22, 163, 74, 0.65));
    }
  }
  .metric-office {
    background: linear-gradient(135deg, rgba(144, 147, 153, 0.14), rgba(144, 147, 153, 0.03));
    .metric-right {
      background: linear-gradient(135deg, #909399, #b1b3b8);
    }
  }
  .metric-avg {
    background: linear-gradient(135deg, rgba(245, 158, 11, 0.12), rgba(245, 158, 11, 0.02));
    .metric-right {
      background: linear-gradient(135deg, var(--lux-warning), rgba(245, 158, 11, 0.62));
    }
  }
  .charts {
    margin-top: 6px;
    margin-bottom: 12px;
  }
  .charts-row {
    margin-top: 16px;
  }
  .kpi-strip {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 12px;
    padding: 4px 4px 10px;
  }
  .kpi-strip.pulse {
    animation: kpiPulse 520ms cubic-bezier(0.16, 1, 0.3, 1);
  }
  @keyframes kpiPulse {
    0% {
      filter: saturate(1.02);
    }
    35% {
      filter: saturate(1.10);
    }
    100% {
      filter: saturate(1.02);
    }
  }
  .kpi-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 12px 12px 12px 14px;
    border-radius: 14px;
    border: 1px solid rgba(15, 23, 42, 0.08);
    background: rgba(255, 255, 255, 0.86);
    transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
    min-height: 68px;
    text-align: left;
    cursor: pointer;
    position: relative;
    overflow: hidden;
    outline: none;
    transform: translateZ(0);
  }
  .kpi-item:hover {
    transform: translateY(-1px);
    box-shadow: 0 16px 40px rgba(15, 23, 42, 0.10);
    border-color: rgba(47, 111, 237, 0.18);
  }
  .kpi-item::before {
    content: "";
    position: absolute;
    inset: 0;
    background:
      radial-gradient(520px 140px at 20% 0%, rgba(255, 255, 255, 0.65), transparent 60%),
      radial-gradient(620px 180px at 90% 40%, rgba(47, 111, 237, 0.10), transparent 55%);
    opacity: 0;
    transform: translateX(-8%) translateY(-2%);
    transition: opacity 0.22s ease, transform 0.42s cubic-bezier(0.16, 1, 0.3, 1);
    pointer-events: none;
  }
  .kpi-item:hover::before {
    opacity: 1;
    transform: translateX(0) translateY(0);
  }
  .kpi-item::after {
    content: "";
    position: absolute;
    inset: -1px;
    border-radius: 15px;
    background: linear-gradient(
      135deg,
      rgba(47, 111, 237, 0.18),
      rgba(255, 255, 255, 0.0),
      rgba(22, 163, 74, 0.14)
    );
    opacity: 0;
    transition: opacity 0.22s ease;
    pointer-events: none;
  }
  .kpi-item:hover::after {
    opacity: 1;
  }
  .kpi-item:active {
    transform: translateY(0);
    box-shadow: 0 10px 22px rgba(15, 23, 42, 0.08);
  }
  .kpi-item:focus-visible {
    box-shadow:
      0 16px 44px rgba(15, 23, 42, 0.10),
      0 0 0 3px rgba(47, 111, 237, 0.18);
    border-color: rgba(47, 111, 237, 0.22);
  }
  .kpi-item.selected {
    border-color: rgba(47, 111, 237, 0.22);
    box-shadow:
      0 16px 44px rgba(15, 23, 42, 0.10),
      inset 0 0 0 1px rgba(47, 111, 237, 0.10);
  }
  .kpi-left {
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
    position: relative;
    z-index: 1;
  }
  .kpi-label {
    font-size: 12px;
    color: var(--lux-subtle);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .kpi-value {
    font-size: 18px;
    font-weight: bold;
    color: #303133;
    margin-bottom: 15px;
    padding-left: 10px;
    border-left: 3px solid #409eff;
    .header-icon {
      margin-right: 8px;
      color: #409eff;
    }
    font-weight: 850;
    letter-spacing: 0.2px;
    color: var(--lux-text);
    line-height: 1.1;
  }
  .overview-card {
  .kpi-meta {
    display: flex;
    align-items: center;
    padding: 20px;
    border-radius: 4px;
    background: #fff;
    border: 1px solid #ebeef5;
    gap: 8px;
    margin-top: 2px;
    min-height: 22px;
  }
    &.blue-card {
      background-color: #ecf5ff;
    }
  .kpi-meta.muted {
    font-size: 11px;
    color: var(--lux-muted);
  }
    &.green-card {
      background-color: #f0f9eb;
    }
  .kpi-chip {
    font-size: 11px;
    font-weight: 700;
    padding: 2px 8px;
    border-radius: 999px;
    border: 1px solid rgba(15, 23, 42, 0.08);
    background: rgba(255, 255, 255, 0.72);
    color: rgba(15, 23, 42, 0.72);
  }
    &.purple-card {
      background-color: #f3f0ff;
    }
  .kpi-chip.up {
    border-color: rgba(22, 163, 74, 0.20);
    color: rgba(22, 163, 74, 0.96);
    background: rgba(22, 163, 74, 0.06);
  }
    &.gray-card {
      background-color: #f5f7fa;
    }
  .kpi-chip.down {
    border-color: rgba(239, 68, 68, 0.20);
    color: rgba(239, 68, 68, 0.96);
    background: rgba(239, 68, 68, 0.06);
  }
    .overview-icon {
      width: 40px;
      height: 40px;
      display: flex;
      align-items: center;
      justify-content: center;
      border-radius: 50%;
      margin-right: 15px;
  .kpi-spark {
    width: 72px;
    height: 22px;
    opacity: 0.9;
    filter: drop-shadow(0 8px 16px rgba(15, 23, 42, 0.10));
  }
      &.blue-icon {
        background-color: #409eff;
        color: #fff;
      }
  .kpi-actions {
    position: absolute;
    top: 10px;
    right: 10px;
    display: flex;
    gap: 6px;
    opacity: 0;
    transform: translateY(-2px);
    pointer-events: none;
    transition: opacity 0.16s ease, transform 0.16s ease;
    z-index: 2;
  }
      &.green-icon {
        background-color: #67c23a;
        color: #fff;
      }
  .kpi-item:hover .kpi-actions {
    opacity: 1;
    transform: translateY(0);
    pointer-events: auto;
  }
      &.purple-icon {
        background-color: #909399;
        color: #fff;
      }
  .kpi-action {
    font-size: 11px;
    font-weight: 650;
    padding: 4px 8px;
    border-radius: 999px;
    border: 1px solid rgba(15, 23, 42, 0.10);
    background: rgba(255, 255, 255, 0.78);
    color: rgba(15, 23, 42, 0.78);
    cursor: pointer;
    transition: background-color 0.16s ease, border-color 0.16s ease, transform 0.16s ease;
  }
      &.gray-icon {
        background-color: #909399;
        color: #fff;
      }
  .kpi-action:hover {
    background: rgba(47, 111, 237, 0.08);
    border-color: rgba(47, 111, 237, 0.22);
    transform: translateY(-1px);
  }
      .el-icon {
        font-size: 20px;
      }
    }
  .kpi-action:active {
    transform: translateY(0);
  }
    .overview-info {
      flex: 1;
  .chart-wrap {
    border-radius: 12px;
    overflow: hidden;
    position: relative;
  }
      .overview-label {
        font-size: 14px;
        color: #606266;
        margin-bottom: 5px;
      }
  .chart-empty {
    height: 240px;
    display: grid;
    place-items: center;
    background: rgba(255, 255, 255, 0.70);
    border-radius: 12px;
    position: absolute;
    inset: 0;
  }
      .overview-value {
        font-size: 20px;
        font-weight: bold;
        color: #303133;
  :deep(.big-chart-dialog .el-dialog) {
    border-radius: 16px;
    overflow: hidden;
  }
        .unit {
          font-size: 12px;
          font-weight: normal;
          color: #909399;
        }
      }
    }
  :deep(.big-chart-dialog .el-dialog__header) {
    padding: 14px 16px;
    background:
      radial-gradient(900px 240px at 10% 0%, rgba(47, 111, 237, 0.10), transparent 55%),
      rgba(255, 255, 255, 0.92);
    border-bottom: 1px solid rgba(15, 23, 42, 0.06);
  }
  :deep(.big-chart-dialog .el-dialog__body) {
    padding: 14px 16px 8px;
    background: rgba(255, 255, 255, 0.92);
  }
  :deep(.big-chart-dialog .el-dialog__footer) {
    padding: 10px 16px 14px;
    background: rgba(255, 255, 255, 0.92);
    border-top: 1px solid rgba(15, 23, 42, 0.06);
  }
  .big-chart-canvas {
    width: 100%;
    height: min(74vh, 760px);
    border-radius: 14px;
    border: 1px solid rgba(15, 23, 42, 0.08);
    background: #ffffff;
  }
  .big-chart-footer {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
  }
  .chart-tools {
    display: flex;
    align-items: center;
    gap: 8px;
    opacity: 0.0;
    transform: translateY(-2px);
    transition: opacity 0.16s ease, transform 0.16s ease;
  }
  .chart-card:hover .chart-tools {
    opacity: 1;
    transform: translateY(0);
  }
  .chart-card:focus-within .chart-tools {
    opacity: 1;
    transform: translateY(0);
  }
  :deep(.chart-wrap .el-loading-mask) {
    border-radius: 12px;
    backdrop-filter: blur(2px);
    background-color: rgba(255, 255, 255, 0.55);
  }
  .chart-tool {
    font-size: 11px;
    font-weight: 650;
    padding: 4px 8px;
    border-radius: 10px;
    border: 1px solid rgba(15, 23, 42, 0.10);
    background: rgba(255, 255, 255, 0.78);
    color: rgba(15, 23, 42, 0.78);
    cursor: pointer;
    transition: background-color 0.16s ease, border-color 0.16s ease, transform 0.16s ease;
  }
  .chart-tool:hover {
    background: rgba(47, 111, 237, 0.08);
    border-color: rgba(47, 111, 237, 0.22);
    transform: translateY(-1px);
  }
  .kpi-unit {
    font-size: 12px;
    font-weight: 600;
    color: var(--lux-muted);
    margin-left: 4px;
  }
  .kpi-icon {
    width: 38px;
    height: 38px;
    border-radius: 12px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    flex: 0 0 auto;
    position: relative;
    z-index: 1;
    transition: transform 0.28s cubic-bezier(0.16, 1, 0.3, 1), filter 0.28s ease;
  }
  .kpi-item:hover .kpi-icon {
    transform: translateY(-1px) rotate(-2deg);
    filter: saturate(1.06);
  }
  .kpi-total {
    background: linear-gradient(135deg, rgba(47, 111, 237, 0.10), rgba(255, 255, 255, 0.86));
  }
  .kpi-total .kpi-icon {
    background: linear-gradient(135deg, var(--lux-primary), var(--lux-primary-2));
  }
  .kpi-production {
    background: linear-gradient(135deg, rgba(22, 163, 74, 0.10), rgba(255, 255, 255, 0.86));
  }
  .kpi-production .kpi-icon {
    background: linear-gradient(135deg, var(--lux-success), rgba(22, 163, 74, 0.65));
  }
  .kpi-office {
    background: linear-gradient(135deg, rgba(100, 116, 139, 0.10), rgba(255, 255, 255, 0.86));
  }
  .kpi-office .kpi-icon {
    background: linear-gradient(135deg, #64748b, #94a3b8);
  }
  .kpi-avg {
    background: linear-gradient(135deg, rgba(245, 158, 11, 0.10), rgba(255, 255, 255, 0.86));
  }
  .kpi-avg .kpi-icon {
    background: linear-gradient(135deg, var(--lux-warning), rgba(245, 158, 11, 0.62));
  }
  .panel-card {
    margin-top: 0;
    border-radius: var(--lux-radius);
    border-color: var(--lux-border);
    background: var(--lux-card);
    backdrop-filter: blur(10px);
    box-shadow: var(--lux-shadow-soft);
    padding-bottom: 8px;
    overflow: hidden;
  }
  .panel-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 8px 4px 6px;
  }
  .segmented {
    position: relative;
    display: grid;
    grid-template-columns: 1fr 1fr;
    padding: 4px;
    border-radius: 14px;
    border: 1px solid rgba(15, 23, 42, 0.08);
    background: rgba(15, 23, 42, 0.03);
    overflow: hidden;
    flex: 1 1 auto;
    min-width: 0;
  }
  .segmented::after {
    content: "";
    position: absolute;
    top: 10px;
    bottom: 10px;
    left: 50%;
    width: 2px;
    border-radius: 999px;
    background: linear-gradient(
      180deg,
      rgba(15, 23, 42, 0.06),
      rgba(15, 23, 42, 0.12),
      rgba(15, 23, 42, 0.06)
    );
    transform: translateX(-0.5px);
    pointer-events: none;
    z-index: 0;
  }
  .segmented.no-active {
    background:
      radial-gradient(900px 220px at 20% 0%, rgba(47, 111, 237, 0.06), transparent 55%),
      rgba(15, 23, 42, 0.03);
    border-color: rgba(15, 23, 42, 0.10);
  }
  .segmented-indicator {
    position: absolute;
    top: 4px;
    left: 4px;
    width: calc(50% - 4px);
    height: calc(100% - 8px);
    border-radius: 13px;
    background: linear-gradient(180deg, rgba(47, 111, 237, 0.10), rgba(255, 255, 255, 0.82));
    border: 1px solid rgba(47, 111, 237, 0.18);
    box-shadow:
      0 14px 30px rgba(15, 23, 42, 0.10),
      0 1px 0 rgba(255, 255, 255, 0.65) inset;
    transition:
      transform 0.36s cubic-bezier(0.16, 1, 0.3, 1),
      opacity 0.20s ease;
    pointer-events: none;
    will-change: transform;
    z-index: 1;
  }
  .segmented-indicator.hidden {
    opacity: 0;
  }
  .segmented-item {
    position: relative;
    z-index: 2;
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: 2px;
    text-align: left;
    padding: 10px 12px;
    border-radius: 12px;
    border: 1px solid transparent;
    background: transparent;
    cursor: pointer;
    transition: transform 0.16s ease, color 0.16s ease, background-color 0.16s ease;
  }
  .segmented-item:hover {
    transform: translateY(-1px);
  }
  .segmented-item:active {
    transform: translateY(0);
  }
  .seg-title {
    font-size: 13px;
    font-weight: 780;
    color: rgba(15, 23, 42, 0.86);
    letter-spacing: 0.2px;
  }
  .seg-sub {
    font-size: 11px;
    color: rgba(15, 23, 42, 0.46);
  }
  .segmented-item.active .seg-title {
    color: var(--lux-text);
  }
  .segmented-item.active .seg-sub {
    color: rgba(15, 23, 42, 0.56);
  }
  .panel-body {
    padding-top: 4px;
  }
  .core-kpi {
    font-size: 12px;
    font-weight: 650;
    color: rgba(15, 23, 42, 0.78);
    padding: 2px 10px;
    border-radius: 999px;
    background: rgba(15, 23, 42, 0.04);
    border: 1px solid rgba(15, 23, 42, 0.06);
  }
  .chart-card {
    background: #fff;
    border-radius: 4px;
    border: 1px solid #ebeef5;
    padding: 20px;
    border-radius: var(--lux-radius);
    border-color: var(--lux-border);
    background: var(--lux-card);
    backdrop-filter: blur(10px);
    box-shadow: var(--lux-shadow-soft);
    transition: box-shadow 0.22s ease, transform 0.22s ease, border-color 0.22s ease;
    .chart-title {
      font-size: 14px;
      font-weight: bold;
      color: #303133;
      margin-bottom: 15px;
      padding-bottom: 10px;
      border-bottom: 1px solid #ebeef5;
    &:hover {
      transform: translateY(-2px);
      box-shadow: var(--lux-shadow);
      border-color: rgba(47, 111, 237, 0.16);
    }
  }
    .chart-content {
      height: 300px;
  .chart-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
  }
  .chart-title {
    font-weight: 700;
    color: var(--lux-text);
  }
  .chart-content {
    height: 240px;
  }
  .table-card {
    border-radius: var(--lux-radius);
    border-color: var(--lux-border);
    background: var(--lux-card);
    backdrop-filter: blur(10px);
    box-shadow: var(--lux-shadow-soft);
    transition: box-shadow 0.22s ease, transform 0.22s ease, border-color 0.22s ease;
    &:hover {
      transform: translateY(-1px);
      box-shadow: var(--lux-shadow);
      border-color: rgba(15, 23, 42, 0.10);
    }
  }
  .data-table {
    width: 100%;
  }
  .consumption-value {
    font-weight: bold;
    color: #409eff;
    color: var(--lux-primary);
  }
  .consumption-unit {
    font-size: 12px;
    color: #909399;
    color: var(--lux-muted);
    margin-left: 2px;
  }
  .price-value {
    font-weight: bold;
    color: #67c23a;
    color: var(--lux-success);
  }
  .cost-value {
    font-weight: bold;
    color: #f56c6c;
    color: var(--lux-danger);
  }
  .pagination-container {
    display: flex;
    justify-content: flex-end;
    padding-top: 12px;
  }
  /* Element Plus æ·±åº¦æ ·å¼ï¼šå¡ç‰‡å¼è¡¨æ ¼è´¨æ„Ÿ */
  :deep(.lux-table) {
    border-radius: 12px;
    overflow: hidden;
    font-variant-numeric: tabular-nums;
  }
  :deep(.lux-table .el-table__inner-wrapper::before) {
    height: 0;
  }
  :deep(.lux-table .el-table__header-wrapper) {
    background:
      linear-gradient(180deg, rgba(15, 23, 42, 0.04) 0%, rgba(15, 23, 42, 0.02) 100%);
  }
  :deep(.lux-table th.el-table__cell) {
    background: transparent;
    color: rgba(15, 23, 42, 0.78);
    font-weight: 700;
    letter-spacing: 0.2px;
    border-bottom: 1px solid rgba(15, 23, 42, 0.08);
  }
  :deep(.lux-table td.el-table__cell) {
    border-bottom: 1px solid rgba(15, 23, 42, 0.06);
  }
  :deep(.lux-table .el-table__row) {
    transition: background-color 0.18s ease;
  }
  :deep(.lux-table .el-table__row:hover > td.el-table__cell) {
    background-color: rgba(47, 111, 237, 0.06) !important;
  }
  :deep(.lux-table .el-table__row:hover) {
    box-shadow: inset 3px 0 0 rgba(47, 111, 237, 0.30);
  }
  :deep(.lux-table .el-table__body tr.el-table__row--striped > td.el-table__cell) {
    background: rgba(15, 23, 42, 0.018);
  }
  :deep(.el-pagination) {
    --el-pagination-button-color: rgba(15, 23, 42, 0.72);
    --el-pagination-button-bg-color: transparent;
    --el-pagination-hover-color: var(--lux-primary);
  }
  :deep(.el-pagination .btn-next),
  :deep(.el-pagination .btn-prev) {
    border-radius: 10px;
    transition: background-color 0.18s ease, transform 0.18s ease;
  }
  :deep(.el-pagination .btn-next:hover),
  :deep(.el-pagination .btn-prev:hover) {
    background-color: rgba(47, 111, 237, 0.06);
    transform: translateY(-1px);
  }
  /* å“åº”式 */
  @media (max-width: 960px) {
    .filter-form {
      flex-direction: column;
      align-items: flex-start;
    }
    .filter-actions {
      justify-content: flex-start;
    }
    .kpi-strip {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
    .panel-head {
      flex-wrap: wrap;
    }
  }
  /* æŠ˜å åŠ¨ç”» */
  .lux-collapse-enter-active,
  .lux-collapse-leave-active {
    transition: max-height 0.22s ease, opacity 0.18s ease;
    overflow: hidden;
  }
  .lux-collapse-enter-from,
  .lux-collapse-leave-to {
    max-height: 0;
    opacity: 0;
  }
  .lux-collapse-enter-to,
  .lux-collapse-leave-from {
    max-height: 600px;
    opacity: 1;
  }
</style>
src/views/productionManagement/processRoute/Edit.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,257 @@
<template>
  <div>
    <el-dialog v-model="isShow"
               title="编辑工艺路线"
               width="400"
               @close="closeModal">
      <el-form label-width="140px"
               :model="formState"
               label-position="top"
               ref="formRef">
        <el-form-item label="产品名称"
                      prop="productModelId"
                      :rules="[
                {
                required: true,
                message: '请选择产品',
                trigger: 'change',
              }
            ]">
          <el-button type="primary"
                     @click="showProductSelectDialog = true">
            {{ formState.productName && formState.productModelName
              ? `${formState.productName} - ${formState.productModelName}`
              : '选择产品' }}
          </el-button>
        </el-form-item>
        <el-form-item label="BOM"
                      prop="bomId"
                      :rules="[
                {
                required: true,
                message: '请选择BOM',
                trigger: 'change',
              }
            ]">
          <el-select v-model="formState.bomId"
                     placeholder="请选择BOM"
                     clearable
                     :disabled="!formState.productModelId || bomOptions.length === 0"
                     style="width: 100%">
            <el-option v-for="item in bomOptions"
                       :key="item.id"
                       :label="item.bomNo || `BOM-${item.id}`"
                       :value="item.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="备注"
                      prop="description">
          <el-input v-model="formState.description"
                    type="textarea" />
        </el-form-item>
      </el-form>
      <!-- äº§å“é€‰æ‹©å¼¹çª— -->
      <ProductSelectDialog v-model="showProductSelectDialog"
                           @confirm="handleProductSelect"
                           single />
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary"
                     @click="handleSubmit">确认</el-button>
          <el-button @click="closeModal">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
  import {
    ref,
    computed,
    getCurrentInstance,
    onMounted,
    nextTick,
    watch,
  } from "vue";
  import { update } from "@/api/productionManagement/processRoute.js";
  import { getByModel } from "@/api/productionManagement/productBom.js";
  import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
  const props = defineProps({
    visible: {
      type: Boolean,
      required: true,
    },
    record: {
      type: Object,
      required: true,
    },
  });
  const emit = defineEmits(["update:visible", "completed"]);
  // å“åº”式数据(替代选项式的 data)
  const formState = ref({
    productId: undefined,
    productModelId: undefined,
    productName: "",
    productModelName: "",
    bomId: undefined,
    description: "",
  });
  const isShow = computed({
    get() {
      return props.visible;
    },
    set(val) {
      emit("update:visible", val);
    },
  });
  const showProductSelectDialog = ref(false);
  const bomOptions = ref([]);
  let { proxy } = getCurrentInstance();
  const closeModal = () => {
    isShow.value = false;
  };
  // è®¾ç½®è¡¨å•数据
  const setFormData = () => {
    if (props.record) {
      formState.value = {
        ...props.record,
        productId: props.record.productId,
        productModelId: props.record.productModelId,
        productName: props.record.productName || "",
        // æ³¨æ„ï¼šrecord中的字段是model,需要映射到productModelName
        productModelName:
          props.record.model || props.record.productModelName || "",
        bomId: props.record.bomId,
        description: props.record.description || "",
      };
      // å¦‚果有产品型号ID,加载BOM列表
      if (props.record.productModelId) {
        loadBomList(props.record.productModelId);
      }
    }
  };
  // åŠ è½½BOM列表
  const loadBomList = async productModelId => {
    if (!productModelId) {
      bomOptions.value = [];
      return;
    }
    try {
      const res = await getByModel(productModelId);
      // å¤„理返回的BOM数据:可能是数组、对象或包含data字段
      let bomList = [];
      if (Array.isArray(res)) {
        bomList = res;
      } else if (res && res.data) {
        bomList = Array.isArray(res.data) ? res.data : [res.data];
      } else if (res && typeof res === "object") {
        bomList = [res];
      }
      bomOptions.value = bomList;
    } catch (error) {
      console.error("加载BOM列表失败:", error);
      bomOptions.value = [];
    }
  };
  // äº§å“é€‰æ‹©å¤„理
  const handleProductSelect = async products => {
    if (products && products.length > 0) {
      const product = products[0];
      // å…ˆæŸ¥è¯¢BOM列表(必选)
      try {
        const res = await getByModel(product.id);
        // å¤„理返回的BOM数据:可能是数组、对象或包含data字段
        let bomList = [];
        if (Array.isArray(res)) {
          bomList = res;
        } else if (res && res.data) {
          bomList = Array.isArray(res.data) ? res.data : [res.data];
        } else if (res && typeof res === "object") {
          bomList = [res];
        }
        if (bomList.length > 0) {
          formState.value.productModelId = product.id;
          formState.value.productName = product.productName;
          formState.value.productModelName = product.model;
          // å¦‚果当前选择的BOM不在新列表中,则重置BOM选择
          const currentBomExists = bomList.some(
            bom => bom.id === formState.value.bomId
          );
          if (!currentBomExists) {
            formState.value.bomId = undefined;
          }
          bomOptions.value = bomList;
          showProductSelectDialog.value = false;
          // è§¦å‘表单验证更新
          proxy.$refs["formRef"]?.validateField("productModelId");
        } else {
          proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
        }
      } catch (error) {
        // å¦‚果接口返回404或其他错误,说明没有BOM
        proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
      }
    }
  };
  const handleSubmit = () => {
    proxy.$refs["formRef"].validate(valid => {
      if (valid) {
        // éªŒè¯æ˜¯å¦é€‰æ‹©äº†äº§å“å’ŒBOM
        if (!formState.value.productModelId) {
          proxy.$modal.msgError("请选择产品");
          return;
        }
        if (!formState.value.bomId) {
          proxy.$modal.msgError("请选择BOM");
          return;
        }
        update(formState.value).then(res => {
          // å…³é—­æ¨¡æ€æ¡†
          isShow.value = false;
          // å‘ŠçŸ¥çˆ¶ç»„件已完成
          emit("completed");
          proxy.$modal.msgSuccess("提交成功");
        });
      }
    });
  };
  defineExpose({
    closeModal,
    handleSubmit,
    isShow,
  });
  // ç›‘听弹窗打开,初始化表单数据
  watch(
    () => props.visible,
    visible => {
      if (visible && props.record) {
        nextTick(() => {
          setFormData();
        });
      }
    },
    { immediate: true }
  );
  onMounted(() => {
    if (props.visible && props.record) {
      setFormData();
    }
  });
</script>
src/views/productionManagement/processRoute/ItemsForm.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,531 @@
<template>
  <div>
    <el-dialog
        v-model="isShow"
        title="工艺路线项目"
        width="800px"
        @close="closeModal"
    >
      <div class="operate-button">
        <el-button
            type="primary"
            @click="isShowProductSelectDialog = true"
            class="mb5"
            style="margin-bottom: 10px;"
        >
          é€‰æ‹©äº§å“
        </el-button>
        <el-switch
            v-model="isTable"
            inline-prompt
            active-text="表格"
            inactive-text="列表"
            @change="handleViewChange"
        />
      </div>
      <el-table
          v-if="isTable"
          ref="multipleTable"
          v-loading="tableLoading"
          border
          :data="routeItems"
          :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
          row-key="id"
          tooltip-effect="dark"
          class="lims-table"
          style="cursor: move;"
      >
        <el-table-column align="center" label="序号" width="60">
          <template #default="scope">
            {{ scope.$index + 1 }}
          </template>
        </el-table-column>
        <el-table-column
            v-for="(item, index) in tableColumn"
            :key="index"
            :label="item.label"
            :width="item.width"
            show-overflow-tooltip
        >
          <template #default="scope" v-if="item.dataType === 'action'">
            <el-button
                v-for="(op, opIndex) in item.operation"
                :key="opIndex"
                :type="op.type"
                :link="op.link"
                size="small"
                @click.stop="op.clickFun(scope.row)"
            >
              {{ op.name }}
            </el-button>
          </template>
          <template #default="scope" v-else>
            <template v-if="item.prop === 'processId'">
              <el-select
                  v-model="scope.row[item.prop]"
                  style="width: 100%;"
                  @mousedown.stop
              >
                <el-option
                    v-for="process in processOptions"
                    :key="process.id"
                    :label="process.name"
                    :value="process.id"
                />
              </el-select>
            </template>
            <template v-else>
              {{ scope.row[item.prop] || '-' }}
            </template>
          </template>
        </el-table-column>
      </el-table>
      <!-- ä½¿ç”¨æ™®é€šdiv替代el-steps -->
      <div
          v-else
          ref="stepsContainer"
          class="mb5 custom-steps"
          style="padding: 10px 0; display: flex; flex-wrap: nowrap; gap: 20px; align-items: flex-start;"
      >
        <div
            v-for="(item, index) in routeItems"
            :key="item.id"
            class="custom-step draggable-step"
            :data-id="item.id"
            style="cursor: move; flex: 0 0 auto; min-width: 220px;"
        >
          <div class="step-content">
            <div class="step-number">{{ index + 1 }}</div>
            <el-card
                :header="item.productName"
                class="step-card"
                style="cursor: move;"
            >
              <div class="step-card-content">
                <p>{{ item.model }}</p>
                <p>{{ item.unit }}</p>
                <el-select
                    v-model="item.processId"
                    style="width: 100%;"
                    @mousedown.stop
                >
                  <el-option
                      v-for="process in processOptions"
                      :key="process.id"
                      :label="process.name"
                      :value="process.id"
                  />
                </el-select>
              </div>
              <template #footer>
                <div class="step-card-footer">
                  <el-button type="danger" link size="small" @click.stop="removeItemByID(item.id)">删除</el-button>
                </div>
              </template>
            </el-card>
          </div>
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="handleSubmit">确认</el-button>
          <el-button @click="closeModal">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <ProductSelectDialog
        v-model="isShowProductSelectDialog"
        @confirm="handelSelectProducts"
    />
  </div>
</template>
<script setup>
import { ref, computed, getCurrentInstance, onMounted, onUnmounted, nextTick } from "vue";
import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
import { findProcessRouteItemList, addOrUpdateProcessRouteItem } from "@/api/productionManagement/processRouteItem.js";
import { processList } from "@/api/productionManagement/productionProcess.js";
import Sortable from 'sortablejs';
const props = defineProps({
  visible: {
    type: Boolean,
    required: true,
    default: false
  },
  record: {
    type: Object,
    required: true,
    default: () => ({})
  }
});
const emit = defineEmits(['update:visible', 'completed']);
const processOptions = ref([]);
const tableLoading = ref(false);
const isShowProductSelectDialog = ref(false);
const routeItems = ref([]);
let tableSortable = null;
let stepsSortable = null;
const multipleTable = ref(null);
const stepsContainer = ref(null);
const isTable = ref(true);
const isShow = computed({
  get() {
    return props.visible;
  },
  set(val) {
    emit('update:visible', val);
  }
});
const tableColumn = ref([
  { label: "产品名称", prop: "productName", width: 180 },
  { label: "规格名称", prop: "model", width: 150 },
  { label: "单位", prop: "unit", width: 80 },
  { label: "工序名称", prop: "processId", width: 180 },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 100,
    operation: [
      {
        name: "删除",
        type: "danger",
        link: true,
        clickFun: (row) => {
          const idx = routeItems.value.findIndex(item => item.id === row.id);
          if (idx > -1) {
            removeItem(idx)
          }
        }
      }
    ]
  }
]);
const removeItem = (index) => {
  routeItems.value.splice(index, 1);
  nextTick(() => initSortable());
};
const removeItemByID = (id) => {
  const idx = routeItems.value.findIndex(item => item.id === id);
  if (idx > -1) {
    routeItems.value.splice(idx, 1);
    nextTick(() => initSortable());
  }
};
const closeModal = () => {
  isShow.value = false;
};
const handelSelectProducts = (products) => {
  destroySortable();
  const newData = products.map(({ id, ...product }) => ({
    ...product,
    productModelId: id,
    routeId: props.record.id,
    id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
    processId: undefined
  }));
  console.log('选择产品前数组:', routeItems.value);
  routeItems.value.push(...newData);
  routeItems.value = [...routeItems.value];
  console.log('选择产品后数组:', routeItems.value);
  // å»¶è¿Ÿåˆå§‹åŒ–,确保DOM完全渲染
  nextTick(() => {
    // å¼ºåˆ¶é‡æ–°æ¸²æŸ“组件
    if (proxy?.$forceUpdate) {
      proxy.$forceUpdate();
    }
    const temp = [...routeItems.value];
    routeItems.value = [];
    nextTick(() => {
      routeItems.value = temp;
      initSortable();
    });
  });
};
const findProcessRouteItems = () => {
  tableLoading.value = true;
  findProcessRouteItemList({ routeId: props.record.id })
      .then(res => {
        tableLoading.value = false;
        routeItems.value = res.data.map(item => ({
          ...item,
          processId: item.processId === 0 ? undefined : item.processId
        }));
        // å»¶è¿Ÿåˆå§‹åŒ–,确保DOM完全渲染
        nextTick(() => {
          setTimeout(() => initSortable(), 100);
        });
      })
      .catch(err => {
        tableLoading.value = false;
        console.error("获取列表失败:", err);
      });
};
const findProcessList = () => {
  processList({})
      .then(res => {
        processOptions.value = res.data;
      })
      .catch(err => {
        console.error("获取工序失败:", err);
      });
};
const { proxy } = getCurrentInstance() || {};
const handleSubmit = () => {
  const hasEmptyProcess = routeItems.value.some(item => !item.processId);
  if (hasEmptyProcess) {
    proxy?.$modal?.msgError("请为所有项目选择工序");
    return;
  }
  addOrUpdateProcessRouteItem({
    routeId: props.record.id,
    processRouteItem: routeItems.value.map(({ id, ...item }) => item)
  })
      .then(res => {
        isShow.value = false;
        emit('completed');
        proxy?.$modal?.msgSuccess("提交成功");
      })
      .catch(err => {
        proxy?.$modal?.msgError(`提交失败:${err.msg || "网络异常"}`);
      });
};
const destroySortable = () => {
  if (tableSortable) {
    tableSortable.destroy();
    tableSortable = null;
  }
  if (stepsSortable) {
    stepsSortable.destroy();
    stepsSortable = null;
  }
};
const initSortable = () => {
  destroySortable();
  if (isTable.value) {
    if (!multipleTable.value) return;
    const tbody = multipleTable.value.$el.querySelector('.el-table__body tbody') ||
        multipleTable.value.$el.querySelector('.el-table__body-wrapper > table > tbody');
    if (!tbody) return;
    tableSortable = new Sortable(tbody, {
      animation: 150,
      ghostClass: 'sortable-ghost',
      handle: '.el-table__row',
      filter: '.el-button, .el-select',
      onEnd: (evt) => {
        if (evt.oldIndex === evt.newIndex || !routeItems.value[evt.oldIndex]) return;
        // ä½¿ç”¨æ•°ç»„ splice æ–¹æ³•重新排序,与表格模式保持一致
        const moveItem = routeItems.value.splice(evt.oldIndex, 1)[0];
        routeItems.value.splice(evt.newIndex, 0, moveItem);
        routeItems.value = [...routeItems.value];
        console.log('排序后数组:', routeItems.value);
      }
    });
  } else {
    if (!stepsContainer.value) return;
    // ä¿®æ”¹ï¼šç›´æŽ¥ä½¿ç”¨stepsContainer.value作为拖拽容器
    const stepsList = stepsContainer.value;
    if (!stepsList) {
      console.warn('未找到步骤条拖拽容器');
      return;
    }
    // ä¿®æ”¹ï¼šç®€åŒ–拖拽配置
    stepsSortable = new Sortable(stepsList, {
      animation: 150,
      ghostClass: 'sortable-ghost',
      draggable: '.draggable-step', // å¯æ‹–拽元素
      handle: '.draggable-step, .step-card', // æ‹–拽手柄
      filter: '.el-button, .el-select, .el-input', // è¿‡æ»¤æŒ‰é’®/选择器
      forceFallback: true,
      fallbackClass: 'sortable-fallback',
      preventOnFilter: true,
      scroll: true,
      scrollSensitivity: 30,
      scrollSpeed: 10,
      bubbleScroll: true,
      onEnd: (evt) => {
        if (evt.oldIndex === evt.newIndex || !routeItems.value[evt.oldIndex]) return;
        // ä½¿ç”¨æ•°ç»„ splice æ–¹æ³•重新排序
        const moveItem = routeItems.value.splice(evt.oldIndex, 1)[0];
        routeItems.value.splice(evt.newIndex, 0, moveItem);
        routeItems.value = [...routeItems.value];
      }
    });
    // è°ƒè¯•:打印容器和实例,确认绑定成功
    console.log('步骤条拖拽容器:', stepsList);
    console.log('Sortable实例:', stepsSortable);
  }
};
const handleViewChange = () => {
  destroySortable();
  // å»¶è¿Ÿåˆå§‹åŒ–,确保视图切换后DOM完全渲染
  nextTick(() => {
    setTimeout(() => initSortable(), 100);
  });
};
onMounted(() => {
  findProcessRouteItems();
  findProcessList();
});
onUnmounted(() => {
  destroySortable();
});
defineExpose({
  closeModal,
  handleSubmit,
  isShow
});
</script>
<style scoped>
:deep(.sortable-ghost) {
  opacity: 0.6;
  background-color: #f5f7fa !important;
}
:deep(.el-table__row) {
  transition: background-color 0.2s;
}
:deep(.el-table__row:hover) {
  background-color: #f9fafc !important;
}
:deep(.el-card__footer){
  padding: 0 !important;
}
.operate-button {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
/* ä¿®æ”¹ï¼šè‡ªå®šä¹‰æ­¥éª¤æ¡å®¹å™¨æ ·å¼ */
.custom-steps {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 20px;
  min-height: 100px;
}
/* ä¿®æ”¹ï¼šè‡ªå®šä¹‰æ­¥éª¤é¡¹æ ·å¼ */
.custom-step {
  cursor: move !important;
  padding: 8px;
  position: relative;
  transition: all 0.2s ease;
  flex: 0 0 auto;
  min-width: 220px;
  touch-action: none;
}
/* æ‹–拽悬浮样式,提示可拖拽 */
.custom-step:hover {
  background-color: rgba(64, 158, 255, 0.05);
  transform: translateY(-2px);
}
.sortable-ghost {
  opacity: 0.4;
  background-color: #f5f7fa !important;
  border: 2px dashed #409eff;
  margin: 10px;
  transform: scale(1.02);
}
.sortable-fallback {
  opacity: 0.9;
  background-color: #f5f7fa;
  border: 1px solid #409eff;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  transform: rotate(2deg);
  margin: 10px;
}
.step-card {
  cursor: move !important;
  transition: box-shadow 0.2s ease;
  user-select: none;
  -webkit-user-select: none;
  pointer-events: auto;
  margin: 10px;
  height: 240px;
}
.step-card:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.step-content {
  width: 220px;
  user-select: none;
}
.step-card-content {
  display: flex;
  flex-direction: column;
  align-items: center;
}
.step-card-footer {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  padding: 10px;
}
/* è‡ªå®šä¹‰åºå·æ ·å¼ä¼˜åŒ– */
.step-number {
  font-weight: bold;
  text-align: center;
  width: 36px;
  height: 36px;
  line-height: 36px;
  margin: 0 auto 10px;
  background: #409eff;
  color: #fff;
  border-radius: 50%;
  font-size: 14px;
}
</style>
src/views/productionManagement/processRoute/New.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,194 @@
<template>
  <div>
    <el-dialog
        v-model="isShow"
        title="新增工艺路线"
        width="400"
        @close="closeModal"
    >
      <el-form label-width="140px" :model="formState" label-position="top" ref="formRef">
        <el-form-item
            label="产品名称"
            prop="productModelId"
            :rules="[
                {
                required: true,
                message: '请选择产品',
                trigger: 'change',
              }
            ]"
        >
          <el-button type="primary" @click="showProductSelectDialog = true">
            {{ formState.productName && formState.productModelName
              ? `${formState.productName} - ${formState.productModelName}`
              : '选择产品' }}
          </el-button>
        </el-form-item>
        <el-form-item
            label="BOM"
            prop="bomId"
            :rules="[
                {
                required: true,
                message: '请选择BOM',
                trigger: 'change',
              }
            ]"
        >
          <el-select
              v-model="formState.bomId"
              placeholder="请选择BOM"
              clearable
              :disabled="!formState.productModelId || bomOptions.length === 0"
              style="width: 100%"
          >
            <el-option
                v-for="item in bomOptions"
                :key="item.id"
                :label="item.bomNo || `BOM-${item.id}`"
                :value="item.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="备注" prop="description">
          <el-input v-model="formState.description" type="textarea" />
        </el-form-item>
      </el-form>
      <!-- äº§å“é€‰æ‹©å¼¹çª— -->
      <ProductSelectDialog
          v-model="showProductSelectDialog"
          @confirm="handleProductSelect"
          single
      />
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="handleSubmit">确认</el-button>
          <el-button @click="closeModal">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import {ref, computed, getCurrentInstance} from "vue";
import {add} from "@/api/productionManagement/processRoute.js";
import {getByModel} from "@/api/productionManagement/productBom.js";
import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
const props = defineProps({
  visible: {
    type: Boolean,
    required: true,
  },
});
const emit = defineEmits(['update:visible', 'completed']);
// å“åº”式数据(替代选项式的 data)
const formState = ref({
  productId: undefined,
  productModelId: undefined,
  productName: "",
  productModelName: "",
  bomId: undefined,
  description: '',
});
const isShow = computed({
  get() {
    return props.visible;
  },
  set(val) {
    emit('update:visible', val);
  },
});
const showProductSelectDialog = ref(false);
const bomOptions = ref([]);
let { proxy } = getCurrentInstance()
const closeModal = () => {
  // é‡ç½®è¡¨å•数据
  formState.value = {
    productId: undefined,
    productModelId: undefined,
    productName: "",
    productModelName: "",
    bomId: undefined,
    description: '',
  };
  bomOptions.value = [];
  isShow.value = false;
};
// äº§å“é€‰æ‹©å¤„理
const handleProductSelect = async (products) => {
  if (products && products.length > 0) {
    const product = products[0];
    // å…ˆæŸ¥è¯¢BOM列表(必选)
    try {
      const res = await getByModel(product.id);
      // å¤„理返回的BOM数据:可能是数组、对象或包含data字段
      let bomList = [];
      if (Array.isArray(res)) {
        bomList = res;
      } else if (res && res.data) {
        bomList = Array.isArray(res.data) ? res.data : [res.data];
      } else if (res && typeof res === 'object') {
        bomList = [res];
      }
      if (bomList.length > 0) {
        formState.value.productModelId = product.id;
        formState.value.productName = product.productName;
        formState.value.productModelName = product.model;
        formState.value.bomId = undefined; // é‡ç½®BOM选择
        bomOptions.value = bomList;
        showProductSelectDialog.value = false;
        // è§¦å‘表单验证更新
        proxy.$refs["formRef"]?.validateField('productModelId');
      } else {
        proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
      }
    } catch (error) {
      // å¦‚果接口返回404或其他错误,说明没有BOM
      proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
    }
  }
};
const handleSubmit = () => {
  proxy.$refs["formRef"].validate(valid => {
    if (valid) {
      // éªŒè¯æ˜¯å¦é€‰æ‹©äº†äº§å“å’ŒBOM
      if (!formState.value.productModelId) {
        proxy.$modal.msgError("请选择产品");
        return;
      }
      if (!formState.value.bomId) {
        proxy.$modal.msgError("请选择BOM");
        return;
      }
      add(formState.value).then(res => {
        // å…³é—­æ¨¡æ€æ¡†
        isShow.value = false;
        // å‘ŠçŸ¥çˆ¶ç»„件已完成
        emit('completed');
        proxy.$modal.msgSuccess("提交成功");
      })
    }
  })
};
defineExpose({
  closeModal,
  handleSubmit,
  isShow,
});
</script>
src/views/productionManagement/processRoute/index.vue
@@ -1,1201 +1,232 @@
<template>
  <div class="app-container">
    <div class="route-header">
      <div class="add-route-btn"
           @click="handleAddRoute">
        <el-icon>
          <Plus />
        </el-icon>
        <span>新增工艺路线</span>
      </div>
    <div class="search_form">
      <el-form :model="searchForm"
               :inline="true">
        <el-form-item label="规格名称:">
          <el-input v-model="searchForm.model"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary"
                     @click="handleQuery">搜索</el-button>
        </el-form-item>
      </el-form>
    </div>
    <div class="route-card-list">
      <div v-for="route in routeList"
           :key="route.id"
           class="route-card">
        <div class="card-header">
          <div class="route-info">
            <span class="route-name"><el-icon style="margin-right: 8px;line-height: 30px;">
                <ScaleToOriginal />
              </el-icon>{{route.routeCode }}<el-tag style="margin-left: 8px"
                      :type="!route.status ? 'warning' : 'success'">{{ !route.status ? '草稿' : '批准' }}</el-tag></span>
            <!-- <span class="route-code">{{ route.routeCode }}</span> -->
          </div>
          <div class="route-actions">
            <el-button v-if="!route.status"
                       link
                       type="success"
                       @click="handleApproveRoute(route)">
              <el-icon>
                <Check />
              </el-icon>
              æ‰¹å‡†
            </el-button>
            <el-button v-if="route.status"
                       link
                       type="warning"
                       @click="handleRevokeApproveRoute(route)">
              <el-icon>
                <Close />
              </el-icon>
              æ’¤é”€æ‰¹å‡†
            </el-button>
            <el-button link
                       type="primary"
                       @click="handleEditRoute(route)">
              <el-icon>
                <Edit />
              </el-icon>
              ç¼–辑
            </el-button>
            <el-button link
                       type="danger"
                       @click="handleDeleteRoute(route)">
              <el-icon>
                <Delete />
              </el-icon>
              åˆ é™¤
            </el-button>
          </div>
        </div>
        <div class="card-body">
          <div class="route-meta">
            <span class="meta-item">
              <el-icon>
                <Box />
              </el-icon>
              <span class="meta-label">产品:</span>
              <span class="meta-value">{{ route.productName }} - {{ route.productModelName }}</span>
            </span>
            <span class="meta-item">
              <el-icon>
                <Document />
              </el-icon>
              <span class="meta-label">BOM:</span>
              <span class="meta-value">{{ route.bomNo || '-' }}</span>
            </span>
            <span class="meta-item">
              <el-icon>
                <Document />
              </el-icon>
              <span class="meta-label">备注:</span>
              <span class="meta-value">{{ route.description || '暂无描述' }}</span>
            </span>
          </div>
          <div class="expand-btn-wrapper">
            <el-button class="expand-btn"
                       :class="{ expanded: route.expanded }"
                       type="primary"
                       text
                       @click="toggleExpand(route)">
              <span class="btn-text">{{ route.expanded ? '收起工序路线' : '展开工序路线' }}</span>
              <el-icon class="expand-icon">
                <component :is="route.expanded ? 'ArrowUp' : 'ArrowDown'" />
              </el-icon>
            </el-button>
          </div>
        </div>
        <div v-if="route.expanded"
             class="process-route">
          <div class="process-flow">
            <div v-for="(process, index) in route.processList"
                 :key="process.id"
                 class="process-flow-item"
                 draggable="true"
                 @dragstart="handleDragStart($event, index, route.id)"
                 @dragover="handleDragOver($event)"
                 @drop="handleDrop($event, index, route.id)"
                 @dragend="handleDragEnd">
              <div class="process-node"
                   :class="{ expanded: process.expanded }">
                <div class="process-node-header">
                  <div class="process-number">{{ index + 1 }}</div>
                  <div class="process-actions">
                    <el-button link
                               type="primary"
                               @click="handleEditProcessSelect(route, index, process)">
                      <el-icon>
                        <Edit />
                      </el-icon>
                    </el-button>
                    <el-button link
                               type="danger"
                               @click="handleDeleteProcess(route.id, process)">
                      <el-icon>
                        <Delete />
                      </el-icon>
                    </el-button>
                  </div>
                </div>
                <div class="process-node-body">
                  <!-- <div class="process-code">{{ process.processId }}</div> -->
                  <div class="process-name">{{ process.processName }}</div>
                  <!-- <div class="process-desc">{{ process.remark || '暂无描述' }}</div> -->
                </div>
                <div class="process-node-footer">
                  <!-- <el-tag size="small"
                          :type="process.status === '1' ? 'success' : 'info'">
                    {{ process.status === '1' ? '启用' : '停用' }}
                  </el-tag> -->
                  <el-button type="primary"
                             link
                             size="small"
                             @click="toggleProcessParams(process)">
                    {{ process.expanded ? '收起参数' : '展开参数' }}
                    ({{ process.paramCount }})
                  </el-button>
                </div>
                <div v-if="process.expanded"
                     class="process-params-section">
                  <div class="params-header">
                    <span>参数列表</span>
                    <el-button type="primary"
                               link
                               size="small"
                               @click="handleAddParam(route.id, process)">
                      <el-icon>
                        <Plus />
                      </el-icon>新增
                    </el-button>
                  </div>
                  <div class="params-list">
                    <div v-for="param in process.paramList"
                         :key="param.id"
                         class="param-item">
                      <div class="param-info">
                        <span class="param-code">{{ param.paramName }}</span>
                        <!-- <span class="param-name">{{ param.paramName }}</span> -->
                        <!-- <el-tag size="small"
                                style="margin-right: 20px;"
                                :type="getParamTypeTag(param.parameterType)">
                          {{ param.parameterType }}
                        </el-tag> -->
                        <span v-if="param.valueMode==1"
                              class="param-value">标准值:{{ param.standardValue || "-" }} {{ param.unit }}</span>
                        <span v-else
                              class="param-value">标准值:{{ param.minValue || "-" }}-{{ param.maxValue || "-" }} {{ param.unit }}</span>
                      </div>
                      <div class="param-actions">
                        <el-button link
                                   type="primary"
                                   size="small"
                                   @click="handleEditParam(route.id, process, param)">
                          ç¼–辑
                        </el-button>
                        <el-button link
                                   type="danger"
                                   size="small"
                                   @click="handleDeleteParam(route.id, process, param)">
                          åˆ é™¤
                        </el-button>
                      </div>
                    </div>
                    <el-empty v-if="!process.paramList || process.paramList.length === 0"
                              description="暂无参数"
                              :image-size="50" />
                  </div>
                </div>
              </div>
              <div v-if="index < route.processList.length - 1"
                   class="flow-arrow">
                <el-icon>
                  <Right />
                </el-icon>
              </div>
            </div>
            <div class="add-process-node"
                 @click="handleSelectProcess(route, index)">
              <el-icon>
                <Plus />
              </el-icon>
              <span>新增工序</span>
            </div>
          </div>
        </div>
    <div class="table_list">
      <div style="text-align: right"
           class="mb10">
        <el-button type="primary"
                   @click="showNewModal">新增工艺路线</el-button>
        <el-button type="danger"
                   @click="handleDelete"
                   :disabled="selectedRows.length === 0"
                   plain>删除工艺路线</el-button>
      </div>
      <PIMTable rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :page="page"
                :isSelection="true"
                @selection-change="handleSelectionChange"
                :tableLoading="tableLoading"
                @pagination="pagination"
                :total="page.total" />
    </div>
    <!-- åˆ†é¡µæŽ§ä»¶ -->
    <div class="pagination-container">
      <el-pagination v-model:current-page="routePage.current"
                     v-model:page-size="routePage.size"
                     :page-sizes="[10, 20, 50, 100]"
                     layout="total, sizes, prev, pager, next, jumper"
                     :total="routePage.total"
                     @size-change="handleRouteSizeChange"
                     @current-change="handleRouteCurrentChange" />
    </div>
    <!-- å·¥è‰ºè·¯çº¿æ–°å¢ž/编辑对话框 -->
    <el-dialog v-model="routeDialogVisible"
               :title="isRouteEdit ? '编辑工艺路线' : '新增工艺路线'"
               width="500px">
      <el-form :model="routeForm"
               :rules="routeRules"
               ref="routeFormRef"
               label-width="120px">
        <el-form-item label="产品名称"
                      prop="productModelId">
          <el-button type="primary"
                     @click="handleProcessProductSelectClick2">
            {{ routeForm.productName && routeForm.productModelName
              ? `${routeForm.productName} - ${routeForm.productModelName}`
              : '选择产品' }}
          </el-button>
        </el-form-item>
        <el-form-item label="BOM"
                      prop="bomId">
          <el-select v-model="routeForm.bomId"
                     placeholder="请选择BOM"
                     clearable
                     :disabled="!routeForm.productModelId || bomOptions.length === 0"
                     style="width: 100%">
            <el-option v-for="item in bomOptions"
                       :key="item.id"
                       :label="item.bomNo || `BOM-${item.id}`"
                       :value="item.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="路线编码"
                      prop="routeCode">
          <el-input v-model="routeForm.routeCode"
                    disabled
                    placeholder="自动生成" />
        </el-form-item>
        <el-form-item label="备注"
                      prop="description">
          <el-input v-model="routeForm.description"
                    type="textarea"
                    :rows="3"
                    placeholder="请输入路线描述" />
        </el-form-item>
        <!-- <el-form-item label="状态"
                      prop="status">
          <el-radio-group v-model="routeForm.status">
            <el-radio label="1">启用</el-radio>
            <el-radio label="0">停用</el-radio>
          </el-radio-group>
        </el-form-item> -->
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="routeDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="handleRouteSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- äº§å“é€‰æ‹©å¼¹çª— -->
    <ProductSelectDialog v-model="showProductSelectDialog"
                         @confirm="handleProductSelect"
                         single />
    <!-- å·¥åºæ–°å¢ž/编辑对话框 -->
    <el-dialog v-model="processDialogVisible"
               :title="isProcessEdit ? '编辑工序' : '新增工序'"
               width="500px">
      <el-form :model="processForm"
               :rules="processRules"
               ref="processFormRef"
               label-width="120px">
        <el-form-item label="工序编码"
                      prop="no">
          <el-input v-model="processForm.no"
                    placeholder="请输入工序编码" />
        </el-form-item>
        <el-form-item label="工序名称"
                      prop="name">
          <el-input v-model="processForm.name"
                    placeholder="请输入工序名称" />
        </el-form-item>
        <el-form-item label="工序描述"
                      prop="remark">
          <el-input v-model="processForm.remark"
                    type="textarea"
                    :rows="3"
                    placeholder="请输入工序描述" />
        </el-form-item>
        <el-form-item label="状态"
                      prop="status">
          <el-radio-group v-model="processForm.status">
            <el-radio :label="true">启用</el-radio>
            <el-radio :label="false">停用</el-radio>
          </el-radio-group>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="processDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="handleProcessSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- é€‰æ‹©å·¥åºå¯¹è¯æ¡† -->
    <el-dialog v-model="selectProcessDialogVisible"
               title="选择工序"
               width="1000px">
      <div class="process-select-container">
        <!-- å·¦ä¾§å·¥åºåˆ—表 -->
        <div class="process-list-area">
          <div class="area-title">可选工序</div>
          <div class="search-box">
            <el-input v-model="processSearchKeyword"
                      placeholder="请输入工序名称搜索"
                      clearable
                      size="small"
                      @input="handleProcessSearch">
              <template #prefix>
                <el-icon>
                  <Search />
                </el-icon>
              </template>
            </el-input>
          </div>
          <el-table :data="filteredProcessList"
                    height="360"
                    border
                    highlight-current-row
                    @current-change="handleProcessSelect">
            <el-table-column prop="no"
                             label="工序编号"
                             width="100" />
            <el-table-column prop="name"
                             label="工序名称" />
            <el-table-column prop="remark"
                             label="工序描述" />
            <el-table-column prop="status"
                             label="状态"
                             width="80">
              <template #default="scope">
                <el-tag size="small"
                        :type="scope.row.status ? 'success' : 'info'">
                  {{ scope.row.status ? '启用' : '停用' }}
                </el-tag>
              </template>
            </el-table-column>
          </el-table>
        </div>
        <!-- å³ä¾§å·¥åºè¯¦æƒ… -->
        <div class="process-detail-area">
          <div class="area-title">工序详情</div>
          <el-form v-if="selectedProcessItem"
                   :model="processForm"
                   label-width="100px"
                   class="process-detail-form">
            <el-form-item label="工序编号">
              <span class="detail-text">{{ selectedProcessItem.no }}</span>
            </el-form-item>
            <el-form-item label="工序名称">
              <span class="detail-text">{{ selectedProcessItem.name }}</span>
            </el-form-item>
            <el-form-item label="工序描述">
              <span class="detail-text">{{ selectedProcessItem.remark || '-' }}</span>
            </el-form-item>
            <el-form-item label="状态">
              <el-tag size="small"
                      :type="selectedProcessItem.status ? 'success' : 'info'">
                {{ selectedProcessItem.status ? '启用' : '停用' }}
              </el-tag>
            </el-form-item>
            <el-form-item label="是否质检">
              <el-tag size="small"
                      :type="selectedProcessItem.isQuality ? 'success' : 'info'">
                {{ selectedProcessItem.isQuality ? '质检' : '非质检' }}
              </el-tag>
            </el-form-item>
            <el-form-item label="产品名称"
                          prop="productModelId">
              <el-button type="primary"
                         @click="handleProcessProductSelectClick">
                {{ processForm.productName && processForm.model
                  ? `${processForm.productName} - ${processForm.model}`
                  : '选择产品' }}
              </el-button>
            </el-form-item>
            <el-form-item label="单位"
                          prop="unit">
              <el-input v-model="processForm.unit"
                        :placeholder="processForm.productModelId ? '根据选择的产品自动带出' : '请先选择产品' "
                        clearable
                        :disabled="true" />
            </el-form-item>
            <el-form-item label="是否质检"
                          prop="isQuality">
              <el-switch v-model="processForm.isQuality"
                         :active-value="true"
                         inactive-value="false" />
            </el-form-item>
          </el-form>
          <el-empty v-else
                    description="请从左侧选择工序" />
        </div>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="selectProcessDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     :disabled="!selectedProcessItem || !processForm.productModelId"
                     @click="handleProcessSelectSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- å‚数新增/编辑对话框 -->
    <el-dialog v-model="paramDialogVisible"
               :title="isParamEdit ? '编辑参数' : '新增参数'"
               width="500px">
      <el-form :model="paramForm"
               :rules="paramRules"
               ref="paramFormRef"
               label-width="120px">
        <el-form-item label="参数编号"
                      prop="parameterCode">
          <el-input v-model="paramForm.parameterCode"
                    placeholder="请输入参数编号" />
        </el-form-item>
        <el-form-item label="参数名称"
                      prop="parameterName">
          <el-input v-model="paramForm.parameterName"
                    placeholder="请输入参数名称" />
        </el-form-item>
        <el-form-item label="参数模式"
                      prop="parameterType2">
          <el-select v-model="paramForm.parameterType2"
                     placeholder="请选择参数模式">
            <el-option label="单值"
                       value="1" />
            <el-option label="区间"
                       value="2" />
          </el-select>
        </el-form-item>
        <el-form-item label="参数类型"
                      prop="parameterType">
          <el-select v-model="paramForm.parameterType"
                     @change="handleParamTypeChange"
                     placeholder="请选择参数类型">
            <el-option label="数值格式"
                       value="数值格式" />
            <el-option label="文本格式"
                       value="文本格式" />
            <el-option label="下拉选项"
                       value="下拉选项" />
            <el-option label="时间格式"
                       value="时间格式" />
          </el-select>
        </el-form-item>
        <el-form-item v-if="paramForm.parameterType === '下拉选项'"
                      label="数据字典"
                      prop="parameterFormat">
          <el-select v-model="paramForm.parameterFormat"
                     placeholder="请选择数据字典">
            <el-option v-for="item in dictTypes"
                       :key="item.dictType"
                       :label="item.dictName"
                       :value="item.dictType" />
          </el-select>
        </el-form-item>
        <el-form-item v-else-if="paramForm.parameterType === '时间格式'"
                      label="时间格式"
                      prop="parameterFormat">
          <el-select v-model="paramForm.parameterFormat"
                     placeholder="请选择时间格式">
            <el-option label="YYYY-MM-DD HH:mm:ss"
                       value="YYYY-MM-DD HH:mm:ss" />
            <el-option label="YYYY-MM-DD"
                       value="YYYY-MM-DD" />
          </el-select>
        </el-form-item>
        <el-form-item v-else
                      label="参数格式"
                      prop="parameterFormat">
          <el-input v-model="paramForm.parameterFormat"
                    placeholder="请输入参数格式" />
        </el-form-item>
        <el-form-item label="标准值"
                      prop="standardValue">
          <el-input v-model="paramForm.standardValue"
                    placeholder="请输入标准值" />
        </el-form-item>
        <el-form-item label="单位"
                      prop="unit">
          <el-input v-model="paramForm.unit"
                    placeholder="请输入单位" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="paramDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="handleParamSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- é€‰æ‹©å‚数对话框 -->
    <el-dialog v-model="selectParamDialogVisible"
               title="选择参数"
               width="1000px">
      <div class="param-select-container">
        <!-- å·¦ä¾§å‚数列表 -->
        <div class="param-list-area">
          <div class="area-title">可选参数</div>
          <div class="search-box">
            <el-input v-model="paramSearchKeyword"
                      placeholder="请输入参数名称搜索"
                      clearable
                      size="small"
                      @input="handleParamSearch">
              <template #prefix>
                <el-icon>
                  <Search />
                </el-icon>
              </template>
            </el-input>
          </div>
          <el-table :data="filteredParamList"
                    height="300"
                    border
                    highlight-current-row
                    @current-change="handleParamSelect">
            <el-table-column prop="paramName"
                             label="参数名称" />
            <el-table-column prop="paramType"
                             label="参数类型">
              <template #default="scope">
                <el-tag size="small"
                        :type="getParamTypeTag(scope.row.paramType)">
                  {{ getParamTypeText(scope.row.paramType) }}
                </el-tag>
              </template>
            </el-table-column>
          </el-table>
          <!-- åˆ†é¡µæŽ§ä»¶ -->
          <div class="pagination-container"
               style="margin-top: 10px;">
            <el-pagination v-model:current-page="paramPage.current"
                           v-model:page-size="paramPage.size"
                           :page-sizes="[10, 20, 50, 100]"
                           layout="total, sizes, prev, pager, next, jumper"
                           :total="paramPage.total"
                           @size-change="handleParamSizeChange"
                           @current-change="handleParamCurrentChange"
                           size="small" />
          </div>
        </div>
        <!-- å³ä¾§å‚数详情 -->
        <div class="param-detail-area">
          <div class="area-title">参数详情</div>
          <el-form v-if="selectedParam"
                   :model="selectedParam"
                   label-width="100px"
                   class="param-detail-form">
            <el-form-item label="参数名称">
              <span class="detail-text">{{ selectedParam.paramName }}</span>
            </el-form-item>
            <el-form-item label="参数模式">
              <el-tag size="small"
                      :type="selectedParam.valueMode == '1' ? 'success' : 'warning'">
                {{ selectedParam.valueMode == '1' ? '单值' : '区间' }}
              </el-tag>
            </el-form-item>
            <el-form-item label="参数类型">
              <el-tag size="small"
                      :type="getParamTypeTag(selectedParam.paramType)">
                {{ getParamTypeText(selectedParam.paramType) }}
              </el-tag>
            </el-form-item>
            <el-form-item label="参数格式">
              <span class="detail-text">{{ selectedParam.paramFormat || '-' }}</span>
            </el-form-item>
            <el-form-item label="单位">
              <span class="detail-text">{{ selectedParam.unit || '-' }}</span>
            </el-form-item>
            <el-form-item label="标准值"
                          v-if="selectedParam.valueMode == '1' && selectedParam.paramType == '1'">
              <el-input v-model="selectedParam.standardValue"
                        type="number"
                        placeholder="请输入默认值" />
            </el-form-item>
            <el-form-item label="最小值"
                          v-if="selectedParam.valueMode == '2' && selectedParam.paramType == '1'">
              <el-input v-model="selectedParam.minValue"
                        type="number"
                        placeholder="请输入最小值" />
            </el-form-item>
            <el-form-item label="最大值"
                          v-if="selectedParam.valueMode == '2' && selectedParam.paramType == '1'">
              <el-input v-model="selectedParam.maxValue"
                        type="number"
                        placeholder="请输入最大值" />
            </el-form-item>
            <el-form-item label="排序">
              <el-input v-model="selectedParam.sort"
                        type="number"
                        placeholder="请输入排序" />
            </el-form-item>
            <el-form-item label="是否必填">
              <el-switch v-model="selectedParam.isRequired"
                         :active-value="1"
                         :inactive-value="0" />
            </el-form-item>
          </el-form>
          <el-empty v-else
                    description="请从左侧选择参数" />
        </div>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="selectParamDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     :disabled="!selectedParam"
                     @click="handleParamSelectSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- ç¼–辑参数对话框 -->
    <el-dialog v-model="editParamDialogVisible"
               title="编辑参数"
               width="600px">
      <el-form :model="editParamForm"
               :rules="editParamRules"
               ref="editParamFormRef"
               label-width="120px">
        <el-form-item label="参数名称">
          <span class="detail-text">{{ editParamForm.paramName }}</span>
        </el-form-item>
        <el-form-item label="参数模式">
          <el-tag size="small"
                  :type="editParamForm.valueMode == '1' ? 'success' : 'warning'">
            {{ editParamForm.valueMode == '1' ? '单值' : '区间' }}
          </el-tag>
        </el-form-item>
        <el-form-item label="参数类型">
          <el-tag size="small"
                  :type="getParamTypeTag(editParamForm.paramType)">
            {{ getParamTypeText(editParamForm.paramType) }}
          </el-tag>
        </el-form-item>
        <el-form-item label="参数格式">
          <span class="detail-text">{{ editParamForm.paramFormat || '-' }}</span>
        </el-form-item>
        <el-form-item label="单位">
          <span class="detail-text">{{ editParamForm.unit || '-' }}</span>
        </el-form-item>
        <el-form-item label="标准值"
                      v-if="editParamForm.valueMode == '1' && editParamForm.paramType == '1'"
                      prop="standardValue">
          <el-input v-model="editParamForm.standardValue"
                    type="number"
                    placeholder="请输入标准值" />
        </el-form-item>
        <el-form-item label="最小值"
                      v-if="editParamForm.valueMode == '2' && editParamForm.paramType == '1'"
                      prop="minValue">
          <el-input v-model="editParamForm.minValue"
                    type="number"
                    placeholder="请输入最小值" />
        </el-form-item>
        <el-form-item label="最大值"
                      v-if="editParamForm.valueMode == '2' && editParamForm.paramType == '1'"
                      prop="maxValue">
          <el-input v-model="editParamForm.maxValue"
                    type="number"
                    placeholder="请输入最大值" />
        </el-form-item>
        <el-form-item label="排序"
                      prop="sort">
          <el-input v-model="editParamForm.sort"
                    type="number"
                    placeholder="请输入排序" />
        </el-form-item>
        <el-form-item label="是否必填"
                      prop="isRequired">
          <el-switch v-model="editParamForm.isRequired"
                     :active-value="1"
                     :inactive-value="0" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="editParamDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="handleEditParamSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <new-process v-if="isShowNewModal"
                 v-model:visible="isShowNewModal"
                 @completed="getList" />
    <edit-process v-if="isShowEditModal"
                  v-model:visible="isShowEditModal"
                  :record="record"
                  @completed="getList" />
    <route-item-form v-if="isShowItemModal"
                     v-model:visible="isShowItemModal"
                     :record="record"
                     @completed="getList" />
  </div>
</template>
<script setup>
  import { ref, reactive, getCurrentInstance, onMounted } from "vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import { onMounted, ref, reactive, toRefs, getCurrentInstance } from "vue";
  import NewProcess from "@/views/productionManagement/processRoute/New.vue";
  import EditProcess from "@/views/productionManagement/processRoute/Edit.vue";
  import RouteItemForm from "@/views/productionManagement/processRoute/ItemsForm.vue";
  import {
    Plus,
    Edit,
    Delete,
    ArrowUp,
    ArrowDown,
    Right,
    Search,
    Check,
    Close,
    Box,
    Document,
  } from "@element-plus/icons-vue";
  import { listType } from "@/api/system/dict/type";
  import { getByModel } from "@/api/productionManagement/productBom.js";
  import { add, update, del } from "@/api/productionManagement/processRoute.js";
  import {
    addOrUpdateProcessRouteItem,
    batchDeleteProcessRouteItem,
    sortProcessRouteItem,
    findProcessRouteItemList,
    getProcessParamList,
    addProcessRouteItemParam,
    editProcessRouteItemParam,
    delProcessRouteItemParam,
  } from "@/api/productionManagement/processRouteItem.js";
  import { list as getProcessListApi } from "@/api/productionManagement/productionProcess.js";
  import { getBaseParamList } from "@/api/basicData/parameterMaintenance.js";
  import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
    listPage,
    del,
    update,
  } from "@/api/productionManagement/processRoute.js";
  import { useRouter } from "vue-router";
  import { ElMessageBox, ElMessage } from "element-plus";
  // å·¥è‰ºè·¯çº¿åˆ—表
  const routeList = ref([]);
  const dictTypes = ref([]);
  // å·¥è‰ºè·¯çº¿åˆ†é¡µ
  const routePage = reactive({
  const router = useRouter();
  const data = reactive({
    searchForm: {
      model: "",
    },
  });
  const { searchForm } = toRefs(data);
  const tableColumn = ref([
    {
      label: "工艺路线编号",
      prop: "processRouteCode",
    },
    {
      label: "产品名称",
      prop: "productName",
    },
    {
      label: "规格名称",
      prop: "model",
    },
    {
      label: "BOM编号",
      prop: "bomNo",
    },
    {
      label: "描述",
      prop: "description",
    },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: "right",
      width: 280,
      operation: [
        {
          name: "编辑",
          type: "text",
          clickFun: row => {
            showEditModal(row);
          },
        },
        {
          name: "路线项目",
          type: "text",
          clickFun: row => {
            showItemModal(row);
          },
        },
        {
          name: "批准",
          type: "primary",
          text: true,
          showHide: row => {
            return !row.status;
          },
          clickFun: row => {
            handleApproveRoute(row);
          },
        },
        {
          name: "取消批准",
          type: "warning",
          text: true,
          showHide: row => {
            return row.status;
          },
          clickFun: row => {
            handleRevokeApproveRoute(row);
          },
        },
      ],
    },
  ]);
  const tableData = ref([]);
  const selectedRows = ref([]);
  const tableLoading = ref(false);
  const isShowNewModal = ref(false);
  const isShowEditModal = ref(false);
  const isShowItemModal = ref(false);
  const record = ref({});
  const page = reactive({
    current: 1,
    size: 10,
    size: 100,
    total: 0,
  });
  // èŽ·å–å…¨å±€å®žä¾‹
  const { proxy } = getCurrentInstance();
  // äº§å“é€‰æ‹©å’ŒBOM相关
  const showProductSelectDialog = ref(false);
  const bomOptions = ref([]);
  // å·¥è‰ºè·¯çº¿å¯¹è¯æ¡†
  const routeDialogVisible = ref(false);
  const isRouteEdit = ref(false);
  const routeFormRef = ref(null);
  const routeForm = reactive({
    id: null,
    productModelId: null,
    productName: "",
    productModelName: "",
    bomId: null,
    routeCode: "",
    description: "",
    status: true,
  });
  const routeRules = {
    productModelId: [
      { required: true, message: "请选择产品", trigger: "change" },
    ],
    bomId: [{ required: true, message: "请选择BOM", trigger: "change" }],
  // æŸ¥è¯¢åˆ—表
  /** æœç´¢æŒ‰é’®æ“ä½œ */
  const handleQuery = () => {
    page.current = 1;
    getList();
  };
  // å·¥åºå¯¹è¯æ¡†
  const processDialogVisible = ref(false);
  const isProcessEdit = ref(false);
  const processFormRef = ref(null);
  const currentRouteId = ref(null);
  const processForm = reactive({
    id: null,
    no: "",
    name: "",
    remark: "",
    status: true,
  });
  const processRules = {
    no: [{ required: true, message: "请输入工序编码", trigger: "blur" }],
    name: [{ required: true, message: "请输入工序名称", trigger: "blur" }],
  const pagination = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
  };
  // é€‰æ‹©å·¥åºå¯¹è¯æ¡†
  const selectProcessDialogVisible = ref(false);
  const availableProcessList = ref([]);
  const filteredProcessList = ref([]);
  const selectedProcessItem = ref(null);
  const processSearchKeyword = ref("");
  const currentRouteIndex = ref(null);
  // å‚数对话框
  const paramDialogVisible = ref(false);
  const isParamEdit = ref(false);
  const paramFormRef = ref(null);
  const currentProcessId = ref(null);
  const paramForm = reactive({
    id: null,
    parameterCode: "",
    parameterName: "",
    parameterType2: "1",
    parameterType: "",
    parameterFormat: "",
    standardValue: "",
    unit: "",
  });
  const paramRules = {
    parameterCode: [
      { required: true, message: "请输入参数编号", trigger: "blur" },
    ],
    parameterName: [
      { required: true, message: "请输入参数名称", trigger: "blur" },
    ],
    parameterType: [
      { required: true, message: "请选择参数类型", trigger: "change" },
    ],
  };
  // é€‰æ‹©å‚数对话框
  const selectParamDialogVisible = ref(false);
  const availableParamList = ref([]);
  const filteredParamList = ref([]);
  const selectedParam = ref(null);
  const paramSearchKeyword = ref("");
  // å¯é€‰å‚数分页
  const paramPage = reactive({
    current: 1,
    size: 10,
    total: 0,
  });
  // ç¼–辑参数对话框
  const editParamDialogVisible = ref(false);
  const editParamFormRef = ref(null);
  const editParamForm = reactive({
    id: null,
    processId: null,
    paramId: null,
    paramName: "",
    valueMode: "1",
    standardValue: null,
    minValue: null,
    maxValue: null,
    sort: 1,
    isRequired: 0,
  });
  const editParamRules = reactive({
    standardValue: [
      {
        required: true,
        message: "请输入标准值",
        trigger: "blur",
        validator: (rule, value, callback) => {
          if (value === null || value === undefined || value === "") {
            callback(new Error("请输入标准值"));
          } else {
            callback();
          }
        },
      },
    ],
    minValue: [
      {
        required: true,
        message: "请输入最小值",
        trigger: "blur",
        validator: (rule, value, callback) => {
          if (value === null || value === undefined || value === "") {
            callback(new Error("请输入最小值"));
          } else {
            callback();
          }
        },
      },
    ],
    maxValue: [
      {
        required: true,
        message: "请输入最大值",
        trigger: "blur",
        validator: (rule, value, callback) => {
          if (value === null || value === undefined || value === "") {
            callback(new Error("请输入最大值"));
          } else {
            callback();
          }
        },
      },
    ],
    sort: [
      {
        required: true,
        message: "请输入排序",
        trigger: "blur",
        validator: (rule, value, callback) => {
          if (value === null || value === undefined || value === "") {
            callback(new Error("请输入排序"));
          } else if (isNaN(value) || value < 1) {
            callback(new Error("排序必须是大于0的整数"));
          } else {
            callback();
          }
        },
      },
    ],
  });
  // æ‹–拽相关
  const draggedItem = ref(null);
  const draggedRouteId = ref(null);
  // èŽ·å–å·¥è‰ºè·¯çº¿åˆ—è¡¨
  const getRouteList = () => {
    // å¯¼å…¥ listPage æ–¹æ³•
    import("@/api/productionManagement/processRoute.js").then(({ listPage }) => {
      listPage({ pageNum: routePage.current, pageSize: routePage.size })
        .then(res => {
          // å¤„理返回的数据,映射到页面需要的格式
          routeList.value = (res.data?.records || []).map(item => ({
            id: item.id,
            productModelId: item.productModelId,
            productName: item.productName,
            productModelName: item.model || item.productModelName,
            bomId: item.bomId,
            bomNo: item.bomNo,
            routeCode: item.processRouteCode || item.routeCode,
            description: item.description || item.description,
            status: item.status,
            expanded: false,
            processList: (item.processList || []).map(process => ({
              ...process,
              processId: process.processId || process.id,
              expanded: false,
            })),
          }));
          // æ›´æ–°åˆ†é¡µæ€»æ•°
          routePage.total = res.data?.total || 0;
        })
        .catch(err => {
          console.error("获取工艺路线列表失败:", err);
          routeList.value = [];
          routePage.total = 0;
        });
    });
  };
  // å±•å¼€/收起工艺路线
  const toggleExpand = route => {
    route.expanded = !route.expanded;
    if (route.expanded) {
      // è°ƒç”¨æŽ¥å£èŽ·å–å·¥åºåˆ—è¡¨
      findProcessRouteItemList({ routeId: route.id })
        .then(res => {
          route.processList = (res.data || []).map(process => ({
            ...process,
            processId: process.processId || process.id,
            expanded: false,
          }));
        })
        .catch(err => {
          console.error("获取工序列表失败:", err);
          route.processList = [];
        });
    }
  };
  // å±•å¼€/收起工序参数
  const toggleProcessParams = process => {
    process.expanded = !process.expanded;
    if (process.expanded && process.id) {
      // è°ƒç”¨æŽ¥å£èŽ·å–å‚æ•°åˆ—è¡¨
      getProcessParamList({
        routeItemId: process.id,
        pageNum: 1,
        pageSize: 1000,
  const getList = () => {
    tableLoading.value = true;
    const params = { ...searchForm.value, ...page };
    params.entryDate = undefined;
    listPage(params)
      .then(res => {
        tableLoading.value = false;
        tableData.value = res.data.records.map(item => ({
          ...item,
        }));
        page.total = res.data.total;
      })
        .then(res => {
          if (res.code === 200) {
            process.paramList = res.data?.records || [];
            process.paramCount = process.paramList.length;
          } else {
            ElMessage.error(res.msg || "获取参数列表失败");
            process.paramList = [];
            process.paramCount = 0;
          }
        })
        .catch(err => {
          console.error("获取参数列表失败:", err);
          ElMessage.error("获取参数列表失败");
          process.paramList = [];
          process.paramCount = 0;
        });
    }
      .catch(err => {
        tableLoading.value = false;
      });
  };
  const toggleProcessParams2 = process => {
    if (process.expanded && process.id) {
      // è°ƒç”¨æŽ¥å£èŽ·å–å‚æ•°åˆ—è¡¨
      getProcessParamList({
        routeItemId: process.id,
        pageNum: 1,
        pageSize: 1000,
  // è¡¨æ ¼é€‰æ‹©æ•°æ®
  const handleSelectionChange = selection => {
    selectedRows.value = selection;
  };
  // æ‰“开新增弹框
  const showNewModal = () => {
    isShowNewModal.value = true;
  };
  const showEditModal = row => {
    isShowEditModal.value = true;
    record.value = row;
  };
  const showItemModal = row => {
    router.push({
      path: "/productionManagement/processRouteItem",
      query: {
        id: row.id,
        processRouteCode: row.processRouteCode || "",
        productName: row.productName || "",
        model: row.model || "",
        bomNo: row.bomNo || "",
        bomId: row.bomId || null,
        description: row.description || "",
        type: "route",
      },
    });
  };
  // åˆ é™¤
  function handleDelete() {
    const ids = selectedRows.value.map(item => item.id);
    proxy.$modal
      .confirm("是否确认删除已勾选的数据项?")
      .then(function () {
        return del(ids);
      })
        .then(res => {
          if (res.code === 200) {
            process.paramList = res.data?.records || [];
            process.paramCount = process.paramList.length;
          } else {
            ElMessage.error(res.msg || "获取参数列表失败");
            process.paramList = [];
            process.paramCount = 0;
          }
        })
        .catch(err => {
          console.error("获取参数列表失败:", err);
          ElMessage.error("获取参数列表失败");
          process.paramList = [];
          process.paramCount = 0;
        });
    }
  };
  // å·¥è‰ºè·¯çº¿æ“ä½œ
  const handleAddRoute = () => {
    isRouteEdit.value = false;
    routeForm.id = null;
    routeForm.productModelId = null;
    routeForm.productName = "";
    routeForm.productModelName = "";
    routeForm.bomId = null;
    routeForm.routeCode = "";
    routeForm.description = "";
    routeForm.status = false;
    bomOptions.value = [];
    routeDialogVisible.value = true;
  };
      .then(() => {
        getList();
        proxy.$modal.msgSuccess("删除成功");
      })
      .catch(() => {});
  }
  const handleEditRoute = route => {
    isRouteEdit.value = true;
    routeForm.id = route.id;
    routeForm.productModelId = route.productModelId;
    routeForm.productName = route.productName;
    routeForm.productModelName = route.productModelName;
    routeForm.bomId = route.bomId;
    routeForm.routeCode = route.routeCode;
    routeForm.description = route.description;
    routeForm.status = route.status;
    routeDialogVisible.value = true;
  };
  const handleDeleteRoute = route => {
    ElMessageBox.confirm("确定要删除该工艺路线吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }).then(() => {
      del(route.id)
        .then(res => {
          ElMessage.success("删除成功");
          getRouteList();
        })
        .catch(err => {
          ElMessage.error("删除失败");
        });
    });
  };
  const handleRouteSubmit = () => {
    routeFormRef.value.validate(valid => {
      if (valid) {
        // æž„建提交数据
        const submitData = {
          ...routeForm,
          // æ³¨æ„ï¼šAPI æœŸæœ›çš„字段名可能与表单字段名不同
          productId: routeForm.productModelId,
          productModelId: routeForm.productModelId,
          description: routeForm.description,
        };
        if (isRouteEdit.value) {
          // ç¼–辑操作
          update(submitData)
            .then(res => {
              ElMessage.success("编辑成功");
              routeDialogVisible.value = false;
              getRouteList();
            })
            .catch(err => {
              ElMessage.error("编辑失败");
            });
        } else {
          // æ–°å¢žæ“ä½œ
          add(submitData)
            .then(res => {
              ElMessage.success("新增成功");
              routeDialogVisible.value = false;
              getRouteList();
            })
            .catch(err => {
              ElMessage.error("新增失败");
            });
        }
      }
    });
  };
  const isform2 = ref(null);
  const handleProcessProductSelectClick = () => {
    isform2.value = true;
    showProductSelectDialog.value = true;
  };
  const handleProcessProductSelectClick2 = () => {
    isform2.value = false;
    showProductSelectDialog.value = true;
  };
  // äº§å“é€‰æ‹©å¤„理
  const handleProductSelect = async products => {
    if (isform2.value) {
      // å¸®æˆ‘写工序中的选择产品的回调,并且把字段加进processForm
      if (products && products.length > 0) {
        const product = products[0];
        console.log("product:", product);
        // æŠŠproduct中的字段添加到processForm中
        // Object.assign(processForm, product);
        processForm.productModelId = product.id;
        processForm.productName = product.productName;
        processForm.model = product.model;
        processForm.unit = product.unit || "";
        console.log("processForm:", processForm);
        // è§¦å‘表单验证更新
        proxy.$refs["processFormRef"]?.validateField("productModelId");
      }
    } else {
      if (products && products.length > 0) {
        const product = products[0];
        // å…ˆæŸ¥è¯¢BOM列表(必选)
        try {
          const res = await getByModel(product.id);
          // å¤„理返回的BOM数据:可能是数组、对象或包含data字段
          let bomList = [];
          if (Array.isArray(res)) {
            bomList = res;
          } else if (res && res.data) {
            bomList = Array.isArray(res.data) ? res.data : [res.data];
          } else if (res && typeof res === "object") {
            bomList = [res];
          }
          console.log("bomList:", bomList);
          if (bomList.length > 0) {
            routeForm.productModelId = product.id;
            routeForm.productName = product.productName;
            routeForm.productModelName = product.model;
            routeForm.bomId = undefined; // é‡ç½®BOM选择
            bomOptions.value = bomList;
            showProductSelectDialog.value = false;
            // è§¦å‘表单验证更新
            proxy.$refs["routeFormRef"]?.validateField("productModelId");
          } else {
            proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
          }
        } catch (error) {
          // å¦‚果接口返回404或其他错误,说明没有BOM
          proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
        }
      }
    }
  };
  // æ‰¹å‡†å·¥è‰ºè·¯çº¿
  const handleApproveRoute = route => {
    ElMessageBox.confirm("确定要批准该工艺路线吗?", "提示", {
      confirmButtonText: "确定",
@@ -1206,7 +237,7 @@
      update({ id: route.id, status: true })
        .then(res => {
          ElMessage.success("批准成功");
          getRouteList();
          getList();
        })
        .catch(err => {
          ElMessage.error("批准失败");
@@ -1214,6 +245,7 @@
    });
  };
  // å–消批准工艺路线
  const handleRevokeApproveRoute = route => {
    ElMessageBox.confirm("确定要撤销批准该工艺路线吗?", "提示", {
      confirmButtonText: "确定",
@@ -1224,1194 +256,17 @@
      update({ id: route.id, status: false })
        .then(res => {
          ElMessage.success("撤销批准成功");
          getRouteList();
          getList();
        })
        .catch(err => {
          ElMessage.error("撤销批准失败");
        });
    });
  };
  // å·¥åºæ“ä½œ
  const handleSelectProcess = (route, index) => {
    console.log("route:", route);
    currentRouteId.value = route.id;
    currentRouteIndex.value = index;
    // é‡ç½®æœç´¢å’Œé€‰æ‹©çŠ¶æ€
    filteredProcessList.value = availableProcessList.value;
    processSearchKeyword.value = "";
    selectedProcessItem.value = null;
    selectProcessDialogVisible.value = true;
  };
  const dragSort = ref(0);
  const currentId = ref(null);
  // ä¿®æ”¹å·¥åº
  const handleEditProcessSelect = (route, index, process) => {
    console.log("route:", route);
    console.log("process:", process);
    currentId.value = process.id;
    currentRouteId.value = route.id;
    currentRouteIndex.value = index;
    // é‡ç½®æœç´¢å’Œé€‰æ‹©çŠ¶æ€
    filteredProcessList.value = availableProcessList.value;
    processSearchKeyword.value = "";
    // è®¾ç½®é€‰ä¸­çš„工序
    filteredProcessList.value.map(item => {
      if (item.id === process.processId) {
        selectedProcessItem.value = item;
      }
    });
    dragSort.value = process.dragSort;
    // selectedProcessItem.value = process;
    // å¡«å……产品选择表单
    processForm.productModelId = process.productModelId;
    processForm.productName = process.productName;
    processForm.model = process.model;
    processForm.processId = process.no;
    // processForm.name = process.name;
    processForm.unit = process.unit || "";
    processForm.isQuality = process.isQuality || false;
    selectProcessDialogVisible.value = true;
  };
  const handleEditProcess = (routeId, process) => {
    currentRouteId.value = routeId;
    isProcessEdit.value = true;
    processForm.id = process.id;
    processForm.no = process.no;
    processForm.name = process.name;
    processForm.remark = process.remark;
    processForm.status = process.status;
    processDialogVisible.value = true;
  };
  const handleDeleteProcess = (routeId, process) => {
    ElMessageBox.confirm("确定要删除该工序吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }).then(() => {
      // è°ƒç”¨API删除工序
      batchDeleteProcessRouteItem([process.id])
        .then(res => {
          ElMessage.success("删除成功");
          // è°ƒç”¨æŽ¥å£æ›´æ–°å·¥åºåˆ—表
          findProcessRouteItemList({ routeId: routeId })
            .then(res => {
              const route = routeList.value.find(r => r.id === routeId);
              if (route) {
                route.processList = (res.data || []).map(process => ({
                  ...process,
                  processId: process.processId || process.id,
                  expanded: false,
                }));
              }
            })
            .catch(err => {
              console.error("获取工序列表失败:", err);
            });
        })
        .catch(err => {
          ElMessage.error("删除失败");
          console.error("删除工序失败:", err);
        });
    });
  };
  const handleProcessSubmit = () => {
    processFormRef.value.validate(valid => {
      if (valid) {
        ElMessage.success(isProcessEdit.value ? "编辑成功" : "新增成功");
        processDialogVisible.value = false;
        // è°ƒç”¨æŽ¥å£æ›´æ–°å·¥åºåˆ—表
        if (currentRouteId.value) {
          findProcessRouteItemList({ routeId: currentRouteId.value })
            .then(res => {
              const route = routeList.value.find(
                r => r.id === currentRouteId.value
              );
              if (route) {
                route.processList = (res.data || []).map(process => ({
                  ...process,
                  processId: process.processId || process.id,
                  expanded: false,
                }));
              }
            })
            .catch(err => {
              console.error("获取工序列表失败:", err);
            });
        }
      }
    });
  };
  // é€‰æ‹©å·¥åºç›¸å…³æ–¹æ³•
  const handleProcessSearch = () => {
    const keyword = processSearchKeyword.value.trim().toLowerCase();
    if (!keyword) {
      filteredProcessList.value = availableProcessList.value;
    } else {
      filteredProcessList.value = availableProcessList.value.filter(
        item =>
          (item.name && item.name.toLowerCase().includes(keyword)) ||
          (item.no && item.no.toLowerCase().includes(keyword))
      );
    }
  };
  const handleProcessSelect = row => {
    selectedProcessItem.value = row;
    // é‡ç½®äº§å“é€‰æ‹©è¡¨å•
    processForm.productModelId = undefined;
    processForm.productName = "";
    processForm.productModelName = "";
    processForm.unit = "";
    processForm.isQuality = row.isQuality || false;
  };
  // å¤„理工序选择时的产品选择
  const handleProcessProductSelect = async products => {
    if (products && products.length > 0) {
      const product = products[0];
      processForm.productModelId = product.id;
      processForm.productName = product.productName;
      processForm.productModelName = product.model;
      processForm.unit = product.unit || "";
      showProductSelectDialog.value = false;
    }
  };
  const handleProcessSelectSubmit = () => {
    if (!selectedProcessItem.value) {
      ElMessage.warning("请先选择一个工序");
      return;
    }
    if (!processForm.productModelId) {
      ElMessage.warning("请选择产品");
      return;
    }
    // æž„建请求参数
    const params = {
      routeId: currentRouteId.value,
      processId: selectedProcessItem.value.id,
      dragSort: routePage.total + 1,
      ...processForm,
    };
    // å¦‚果是修改操作,添加id参数
    if (selectedProcessItem.value.id) {
      params.id = currentId.value;
      params.dragSort = dragSort.value;
    }
    // è°ƒç”¨API添加工序或修改工序
    addOrUpdateProcessRouteItem(params)
      .then(res => {
        ElMessage.success(
          selectedProcessItem.value.id ? "修改工序成功" : "添加工序成功"
        );
        selectProcessDialogVisible.value = false;
        // è°ƒç”¨æŽ¥å£æ›´æ–°å·¥åºåˆ—表
        findProcessRouteItemList({ routeId: currentRouteId.value })
          .then(res => {
            const route = routeList.value.find(
              r => r.id === currentRouteId.value
            );
            if (route) {
              route.processList = (res.data || []).map(process => ({
                ...process,
                processId: process.processId || process.id,
                expanded: false,
              }));
            }
          })
          .catch(err => {
            console.error("获取工序列表失败:", err);
          });
      })
      .catch(err => {
        ElMessage.error(
          selectedProcessItem.value.id ? "修改工序失败" : "添加工序失败"
        );
        console.error(
          selectedProcessItem.value.id ? "修改工序失败:" : "添加工序失败:",
          err
        );
      });
  };
  // å‚数操作
  const handleAddParam = (routeId, process) => {
    currentRouteId.value = routeId;
    currentProcessId.value = process.id;
    selectedParam.value = null;
    paramSearchKeyword.value = "";
    paramPage.current = 1;
    // èŽ·å–å¯é€‰å‚æ•°åˆ—è¡¨
    getBaseParamList({
      paramName: paramSearchKeyword.value,
      current: paramPage.current,
      size: paramPage.size,
    }).then(res => {
      if (res.code === 200) {
        filteredParamList.value = res.data?.records || [];
        paramPage.total = res.data?.total || 0;
      } else {
        ElMessage.error(res.msg || "查询失败");
      }
    });
    selectParamDialogVisible.value = true;
  };
  const handleEditParam = (routeId, process, param) => {
    currentRouteId.value = routeId;
    currentProcessId.value = process.id;
    editParamForm.id = param.id;
    editParamForm.processId = process.id;
    editParamForm.paramId = param.paramId;
    editParamForm.paramName = param.parameterName || param.paramName;
    editParamForm.valueMode = param.parameterType2 || param.valueMode || "1";
    editParamForm.standardValue = param.standardValue;
    editParamForm.minValue = param.minValue;
    editParamForm.maxValue = param.maxValue;
    editParamForm.sort = param.sort || 1;
    editParamForm.isRequired = param.isRequired || 0;
    editParamForm.paramType = param.parameterType || param.paramType;
    editParamForm.paramFormat = param.parameterFormat || param.paramFormat;
    editParamForm.unit = param.unit || param.unit;
    editParamDialogVisible.value = true;
  };
  const handleDeleteParam = (routeId, process, param) => {
    ElMessageBox.confirm("确定要删除该参数吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }).then(() => {
      // è°ƒç”¨API删除参数
      delProcessRouteItemParam(param.id)
        .then(res => {
          ElMessage.success("删除成功");
          // åˆ·æ–°å‚数列表
          toggleProcessParams2(process);
        })
        .catch(err => {
          ElMessage.error("删除参数失败");
          console.error("删除参数失败:", err);
        });
    });
  };
  const handleParamSubmit = () => {
    paramFormRef.value.validate(valid => {
      if (valid) {
        ElMessage.success(isParamEdit.value ? "编辑成功" : "新增成功");
        paramDialogVisible.value = false;
        getRouteList();
      }
    });
  };
  const handleParamTypeChange = () => {
    if (paramForm.parameterType === "数值格式") {
      paramForm.parameterFormat = "#.0000";
    } else if (paramForm.parameterType === "时间格式") {
      paramForm.parameterFormat = "YYYY-MM-DD HH:mm:ss";
    } else {
      paramForm.parameterFormat = "";
    }
  };
  const getParamTypeTag = type => {
    const typeMap = {
      1: "primary",
      2: "info",
      3: "warning",
      4: "success",
    };
    return typeMap[type] || "default";
  };
  const getParamTypeText = type => {
    const typeMap = {
      1: "数值格式",
      2: "文本格式",
      3: "下拉选项",
      4: "时间格式",
    };
    return typeMap[type] || "未知参数类型";
  };
  // é€‰æ‹©å‚数相关方法
  const handleParamSearch = () => {
    // é‡ç½®åˆ†é¡µ
    paramPage.current = 1;
    // é‡æ–°åŠ è½½æ•°æ®
    getBaseParamList({
      paramName: paramSearchKeyword.value,
      current: paramPage.current,
      size: paramPage.size,
    }).then(res => {
      if (res.code === 200) {
        filteredParamList.value = res.data?.records || [];
        paramPage.total = res.data?.total || 0;
      } else {
        ElMessage.error(res.msg || "查询失败");
      }
    });
  };
  const handleParamSelect = row => {
    selectedParam.value = row;
  };
  // å¤„理分页大小变化
  const handleParamSizeChange = size => {
    paramPage.size = size;
    getBaseParamList({
      paramName: paramSearchKeyword.value,
      current: paramPage.current,
      size: paramPage.size,
    }).then(res => {
      if (res.code === 200) {
        filteredParamList.value = res.data?.records || [];
        paramPage.total = res.data?.total || 0;
      } else {
        ElMessage.error(res.msg || "查询失败");
      }
    });
  };
  // å¤„理当前页码变化
  const handleParamCurrentChange = current => {
    paramPage.current = current;
    getBaseParamList({
      paramName: paramSearchKeyword.value,
      current: paramPage.current,
      size: paramPage.size,
    }).then(res => {
      if (res.code === 200) {
        filteredParamList.value = res.data?.records || [];
        paramPage.total = res.data?.total || 0;
      } else {
        ElMessage.error(res.msg || "查询失败");
      }
    });
  };
  // å·¥è‰ºè·¯çº¿åˆ†é¡µå¤„理
  const handleRouteSizeChange = size => {
    routePage.size = size;
    getRouteList();
  };
  const handleRouteCurrentChange = current => {
    routePage.current = current;
    getRouteList();
  };
  const handleParamSelectSubmit = () => {
    if (!selectedParam.value) {
      ElMessage.warning("请先选择一个参数");
      return;
    }
    // æ‰¾åˆ°å¯¹åº”的工艺路线和工序
    const route = routeList.value.find(r => r.id === currentRouteId.value);
    const process = route?.processList.find(p => p.id === currentProcessId.value);
    if (route && process) {
      // æ£€æŸ¥å‚数是否已存在
      // const exists = process.paramList?.some(
      //   p =>
      //     p.paramId === selectedParam.value.id ||
      //     p.parameterCode === selectedParam.value.paramCode
      // );
      // if (exists) {
      //   ElMessage.warning("该参数已存在于工序中");
      //   return;
      // }
      // åˆ¤æ–­å‚数类型,只有数值类型才传标准值、最大值和最小值
      const isNumericMode = selectedParam.value.valueMode === 1;
      // è°ƒç”¨API新增参数
      addProcessRouteItemParam({
        routeItemId: process.id,
        paramId: selectedParam.value.id,
        standardValue: isNumericMode
          ? selectedParam.value.standardValue || ""
          : "",
        minValue: isNumericMode ? selectedParam.value.minValue || 0 : null,
        maxValue: isNumericMode ? selectedParam.value.maxValue || 0 : null,
        isRequired: selectedParam.value.isRequired || 0,
      })
        .then(res => {
          ElMessage.success("添加参数成功");
          selectParamDialogVisible.value = false;
          // åˆ·æ–°å‚数列表
          toggleProcessParams2(process);
        })
        .catch(err => {
          ElMessage.error("添加参数失败");
          console.error("添加参数失败:", err);
        });
    }
  };
  const handleEditParamSubmit = () => {
    editParamFormRef.value.validate(valid => {
      if (valid) {
        // åˆ¤æ–­å‚数类型,只有数值类型才传标准值、最大值和最小值
        const isNumericMode = editParamForm.valueMode == 1;
        // è°ƒç”¨API修改参数
        editProcessRouteItemParam({
          id: editParamForm.id,
          routeItemId: currentProcessId.value,
          paramId: editParamForm.paramId,
          standardValue: isNumericMode ? editParamForm.standardValue || "" : "",
          minValue: isNumericMode ? editParamForm.minValue || 0 : null,
          maxValue: isNumericMode ? editParamForm.maxValue || 0 : null,
          isRequired: editParamForm.isRequired || 0,
        })
          .then(res => {
            ElMessage.success("编辑成功");
            editParamDialogVisible.value = false;
            // æ‰¾åˆ°å¯¹åº”的工艺路线和工序
            const route = routeList.value.find(
              r => r.id === currentRouteId.value
            );
            const process = route?.processList.find(
              p => p.id === currentProcessId.value
            );
            // åˆ·æ–°å‚数列表
            if (process) {
              toggleProcessParams2(process);
            }
          })
          .catch(err => {
            ElMessage.error("编辑参数失败");
            console.error("编辑参数失败:", err);
          });
      }
    });
  };
  // æ‹–拽排序
  const handleDragStart = (event, index, routeId) => {
    draggedItem.value = index;
    draggedRouteId.value = routeId;
    event.dataTransfer.effectAllowed = "move";
  };
  const handleDragOver = event => {
    event.preventDefault();
    event.dataTransfer.dropEffect = "move";
  };
  const handleDrop = (event, dropIndex, routeId) => {
    event.preventDefault();
    if (draggedItem.value === null || draggedItem.value === dropIndex) return;
    const route = routeList.value.find(r => r.id === routeId);
    if (route && route.processList) {
      const draggedProcess = route.processList[draggedItem.value];
      // è®¡ç®—新的排序值
      const newDragSort = dropIndex + 1;
      // è°ƒç”¨API排序工序
      sortProcessRouteItem({
        id: draggedProcess.id,
        dragSort: newDragSort,
      })
        .then(res => {
          // è°ƒç”¨æŽ¥å£èŽ·å–æœ€æ–°çš„å·¥åºåˆ—è¡¨
          findProcessRouteItemList({ routeId: routeId })
            .then(res => {
              if (route) {
                route.processList = (res.data || []).map(process => ({
                  ...process,
                  processId: process.processId || process.id,
                  expanded: false,
                }));
              }
              ElMessage.success("排序成功");
            })
            .catch(err => {
              console.error("获取工序列表失败:", err);
              ElMessage.success("排序成功");
            });
        })
        .catch(err => {
          ElMessage.error("排序失败");
          console.error("排序工序失败:", err);
        });
    }
  };
  const handleDragEnd = () => {
    draggedItem.value = null;
    draggedRouteId.value = null;
  };
  // èŽ·å–æ•°æ®å­—å…¸
  const getDictTypes = () => {
    listType({ pageNum: 1, pageSize: 1000 }).then(res => {
      dictTypes.value = res.rows || [];
    });
  };
  getRouteList();
  getDictTypes();
  // é¡µé¢åŠ è½½æ—¶èŽ·å–å·¥åºåˆ—è¡¨
  onMounted(() => {
    getProcessListApi()
      .then(res => {
        // å¤„理返回的数据,映射到页面需要的格式
        availableProcessList.value = (res.data || []).map(item => ({
          id: item.id,
          no: item.no || item.no,
          name: item.name || item.name,
          remark: item.remark || item.remark,
          status: item.status,
          isQuality: item.isQuality,
        }));
        filteredProcessList.value = availableProcessList.value;
      })
      .catch(() => {
        ElMessage.error("获取工序列表失败");
      });
    getList();
  });
</script>
<style scoped lang="scss">
  .app-container {
    padding: 20px;
    padding-bottom: 80px;
    background-color: #f0f2f5;
    min-height: calc(100vh - 84px);
    overflow: hidden;
  }
  .route-header {
    margin-bottom: 20px;
    .add-route-btn {
      width: 100%;
      display: inline-flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      min-width: 120px;
      height: 100px;
      border: 2px dashed #dcdfe6;
      border-radius: 12px;
      background: #fafafa;
      cursor: pointer;
      transition: all 0.3s ease;
      color: #909399;
      padding: 0 20px;
      .el-icon {
        font-size: 24px;
        margin-bottom: 8px;
      }
      span {
        font-size: 13px;
      }
      &:hover {
        border-color: #409eff;
        background: #ecf5ff;
        color: #409eff;
      }
    }
  }
  .route-card-list {
    display: grid;
    grid-template-columns: repeat(1, 1fr);
    gap: 20px;
    max-height: calc(100vh - 240px);
    overflow-y: auto;
    padding-right: 10px;
  }
  /* è‡ªå®šä¹‰æ»šåŠ¨æ¡æ ·å¼ */
  .route-card-list::-webkit-scrollbar {
    width: 8px;
  }
  .route-card-list::-webkit-scrollbar-track {
    background: #f1f1f1;
    border-radius: 4px;
  }
  .route-card-list::-webkit-scrollbar-thumb {
    background: #c1c1c1;
    border-radius: 4px;
  }
  .route-card-list::-webkit-scrollbar-thumb:hover {
    background: #a8a8a8;
  }
  .route-card {
    background: #fff;
    border-radius: 8px;
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
    overflow: hidden;
    .card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 20px 40px;
      border-bottom: 1px solid #ebeef5;
      background: #f8f9fa;
      .route-info {
        display: flex;
        // flex-direction: column;
        // justify-content: center;
        // items-align: center;
        gap: 4px;
        .route-code {
          font-size: 12px;
          color: #909399;
          font-family: "Courier New", monospace;
          line-height: 30px;
        }
        .route-name {
          font-size: 18px;
          font-weight: 600;
          color: #303133;
          display: flex;
          align-items: center;
        }
      }
      .route-actions {
        display: flex;
        gap: 8px;
        // .el-button {
        //   color: #409eff;
        // }
      }
    }
    .card-body {
      padding: 16px 40px;
      .route-desc {
        font-size: 14px;
        color: #606266;
        margin-bottom: 12px;
      }
      .route-meta {
        display: flex;
        gap: 24px;
        margin-bottom: 12px;
        padding: 10px 14px;
        background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
        border-radius: 8px;
        border-left: 3px solid #409eff;
        .meta-item {
          display: flex;
          align-items: center;
          gap: 6px;
          font-size: 13px;
          margin-right: 40px;
          .el-icon {
            font-size: 14px;
            color: #409eff;
          }
          .meta-label {
            color: #909399;
            font-weight: 500;
          }
          .meta-value {
            color: #303133;
            font-weight: 600;
          }
        }
      }
      .expand-btn-wrapper {
        display: flex;
        justify-content: center;
        margin-top: 8px;
        .expand-btn {
          padding: 8px 20px;
          border-radius: 20px;
          background: linear-gradient(135deg, #ecf5ff 0%, #d9ecff 100%);
          border: 1px solid #b3d8ff;
          transition: all 0.3s ease;
          .btn-text {
            font-size: 13px;
            font-weight: 500;
            color: #409eff;
            margin-right: 6px;
          }
          .expand-icon {
            font-size: 14px;
            color: #409eff;
            transition: transform 0.3s ease;
          }
          &:hover {
            background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
            border-color: #409eff;
            box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
            .btn-text,
            .expand-icon {
              color: #fff;
            }
          }
          &.expanded {
            background: linear-gradient(135deg, #f0f9eb 0%, #e1f3d8 100%);
            border-color: #a5d69a;
            .btn-text,
            .expand-icon {
              color: #67c23a;
            }
            &:hover {
              background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
              border-color: #67c23a;
              box-shadow: 0 4px 12px rgba(103, 194, 58, 0.3);
              .btn-text,
              .expand-icon {
                color: #fff;
              }
            }
          }
        }
      }
    }
    .process-route {
      padding: 0 20px 20px;
      background: #f5f7fa;
      border-top: 1px solid #ebeef5;
      .process-flow {
        display: flex;
        align-items: flex-start;
        gap: 8px;
        padding: 20px 0;
        overflow-x: auto;
        overflow-y: hidden;
        .process-flow-item {
          display: flex;
          align-items: center;
          gap: 8px;
          .process-node {
            background: #fff;
            border-radius: 12px;
            padding: 16px;
            border: 2px solid #ebeef5;
            cursor: move;
            transition: all 0.3s ease;
            // min-width: 180px;
            // max-width: 220px;
            width: 300px;
            &.expanded {
              width: 400px;
            }
            &:hover {
              box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
              transform: translateY(-2px);
              border-color: #409eff;
            }
            &:active {
              cursor: grabbing;
            }
            .process-node-header {
              display: flex;
              justify-content: space-between;
              align-items: center;
              margin-bottom: 12px;
              .process-number {
                width: 28px;
                height: 28px;
                border-radius: 50%;
                background: #409eff;
                color: #ffffff;
                font-size: 12px;
                font-weight: 600;
                display: flex;
                align-items: center;
                justify-content: center;
              }
              .process-actions {
                display: flex;
                gap: 4px;
              }
            }
            .process-node-body {
              text-align: center;
              margin-bottom: 12px;
              .process-code {
                font-size: 11px;
                color: #909399;
                font-family: "Courier New", monospace;
                margin-bottom: 4px;
              }
              .process-name {
                font-size: 15px;
                font-weight: 600;
                color: #303133;
                margin-bottom: 6px;
              }
              .process-desc {
                font-size: 12px;
                color: #606266;
                overflow: hidden;
                text-overflow: ellipsis;
                display: -webkit-box;
                -webkit-line-clamp: 2;
                -webkit-box-orient: vertical;
              }
            }
            .process-node-footer {
              display: flex;
              justify-content: flex-end;
              align-items: center;
              padding-top: 10px;
              border-top: 1px solid #ebeef5;
            }
            .process-params-section {
              margin-top: 12px;
              padding-top: 12px;
              border-top: 1px solid #ebeef5;
              .params-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin-bottom: 8px;
                font-size: 13px;
                font-weight: 600;
                color: #303133;
              }
              .params-list {
                display: flex;
                flex-direction: column;
                gap: 6px;
                max-height: 200px;
                overflow-y: auto;
                .param-item {
                  display: flex;
                  justify-content: space-between;
                  align-items: center;
                  padding: 6px 8px;
                  background: #fafafa;
                  border-radius: 4px;
                  border-left: 2px solid #409eff;
                  font-size: 12px;
                  .param-info {
                    display: flex;
                    flex-direction: row;
                    align-items: center;
                    gap: 6px;
                    flex: 1;
                    min-width: 0;
                    .param-code {
                      font-size: 11px;
                      color: #e6a23c;
                      font-family: "Courier New", monospace;
                      margin-right: 20px;
                    }
                    .param-name {
                      font-size: 12px;
                      color: #303133;
                      font-weight: 500;
                      margin-right: 20px;
                    }
                    .param-value {
                      font-size: 11px;
                      color: #606266;
                    }
                  }
                  .param-actions {
                    display: flex;
                    gap: 4px;
                    flex-shrink: 0;
                  }
                }
              }
            }
          }
          .flow-arrow {
            display: flex;
            align-items: center;
            color: #c0c4cc;
            font-size: 24px;
            padding: 0 4px;
            .el-icon {
              font-size: 20px;
            }
          }
        }
        .add-process-node {
          display: flex;
          flex-direction: column;
          align-items: center;
          justify-content: center;
          min-width: 100px;
          height: 137px;
          border: 2px dashed #dcdfe6;
          border-radius: 12px;
          background: #fafafa;
          cursor: pointer;
          transition: all 0.3s ease;
          color: #909399;
          // margin-left: 10px;
          .el-icon {
            font-size: 24px;
            margin-bottom: 8px;
          }
          span {
            font-size: 13px;
          }
          &:hover {
            border-color: #409eff;
            background: #ecf5ff;
            color: #409eff;
          }
        }
      }
    }
  }
  // æ‹–拽时的样式
  .process-flow-item.dragging {
    opacity: 0.5;
    transform: scale(0.98);
  }
  // é€‰æ‹©å·¥åºå¯¹è¯æ¡†æ ·å¼
  .process-select-container {
    display: flex;
    gap: 20px;
    height: 450px;
    .process-list-area {
      flex: 1;
      display: flex;
      flex-direction: column;
      .area-title {
        font-size: 14px;
        font-weight: 600;
        color: #303133;
        margin-bottom: 12px;
        padding-bottom: 8px;
        border-bottom: 1px solid #ebeef5;
      }
      .search-box {
        margin-bottom: 12px;
        .el-input {
          width: 100%;
        }
      }
    }
    .process-detail-area {
      width: 380px;
      display: flex;
      flex-direction: column;
      background: #f5f7fa;
      border-radius: 8px;
      padding: 16px;
      .area-title {
        font-size: 14px;
        font-weight: 600;
        color: #303133;
        margin-bottom: 16px;
        padding-bottom: 8px;
        border-bottom: 1px solid #ebeef5;
      }
      .process-detail-form {
        .el-form-item {
          margin-bottom: 12px;
          .el-form-item__label {
            color: #606266;
            font-weight: 500;
          }
        }
        .detail-text {
          color: #303133;
          font-weight: 500;
        }
      }
    }
  }
  // é€‰æ‹©å‚数对话框样式
  .param-select-container {
    display: flex;
    gap: 20px;
    height: 450px;
    .param-list-area {
      // flex: 1;
      width: 380px;
      display: flex;
      flex-direction: column;
      .area-title {
        font-size: 14px;
        font-weight: 600;
        color: #303133;
        margin-bottom: 12px;
        padding-bottom: 8px;
        border-bottom: 1px solid #ebeef5;
      }
      .search-box {
        margin-bottom: 12px;
        .el-input {
          width: 100%;
        }
      }
    }
    .param-detail-area {
      // width: 380px;
      flex: 1;
      display: flex;
      flex-direction: column;
      background: #f5f7fa;
      border-radius: 8px;
      padding: 16px;
      .area-title {
        font-size: 14px;
        font-weight: 600;
        color: #303133;
        margin-bottom: 16px;
        padding-bottom: 8px;
        border-bottom: 1px solid #ebeef5;
      }
      .param-detail-form {
        .el-form-item {
          margin-bottom: 12px;
          .el-form-item__label {
            color: #606266;
            font-weight: 500;
          }
        }
        .detail-text {
          color: #303133;
          font-weight: 500;
        }
      }
    }
  }
  // åˆ†é¡µæŽ§ä»¶æ ·å¼
  .pagination-container {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    display: flex;
    justify-content: flex-end;
    padding: 16px 20px;
    background-color: #fff !important;
    border-top: 1px solid #ebeef5;
    box-shadow: 0 -2px 12px 0 rgba(0, 0, 0, 0.1);
    z-index: 100;
    .el-pagination {
      .el-pagination__sizes {
        margin-right: 16px;
      }
      .el-pagination__jump {
        margin-left: 16px;
      }
      .el-pagination__total {
        color: #606266;
        font-size: 14px;
      }
      .el-pagination__button {
        border-radius: 4px;
        transition: all 0.3s ease;
        &:hover:not(:disabled) {
          color: #409eff;
          border-color: #409eff;
        }
      }
      .el-pagination__button--active {
        background-color: #409eff;
        border-color: #409eff;
        color: #fff;
      }
    }
  }
</style>
<style scoped></style>
src/views/productionManagement/processRoute/index2.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2417 @@
<template>
  <div class="app-container">
    <div class="route-header">
      <div class="add-route-btn"
           @click="handleAddRoute">
        <el-icon>
          <Plus />
        </el-icon>
        <span>新增工艺路线</span>
      </div>
    </div>
    <div class="route-card-list">
      <div v-for="route in routeList"
           :key="route.id"
           class="route-card">
        <div class="card-header">
          <div class="route-info">
            <span class="route-name"><el-icon style="margin-right: 8px;line-height: 30px;">
                <ScaleToOriginal />
              </el-icon>{{route.routeCode }}<el-tag style="margin-left: 8px"
                      :type="!route.status ? 'warning' : 'success'">{{ !route.status ? '草稿' : '批准' }}</el-tag></span>
            <!-- <span class="route-code">{{ route.routeCode }}</span> -->
          </div>
          <div class="route-actions">
            <el-button v-if="!route.status"
                       link
                       type="success"
                       @click="handleApproveRoute(route)">
              <el-icon>
                <Check />
              </el-icon>
              æ‰¹å‡†
            </el-button>
            <el-button v-if="route.status"
                       link
                       type="warning"
                       @click="handleRevokeApproveRoute(route)">
              <el-icon>
                <Close />
              </el-icon>
              æ’¤é”€æ‰¹å‡†
            </el-button>
            <el-button link
                       type="primary"
                       @click="handleEditRoute(route)">
              <el-icon>
                <Edit />
              </el-icon>
              ç¼–辑
            </el-button>
            <el-button link
                       type="danger"
                       @click="handleDeleteRoute(route)">
              <el-icon>
                <Delete />
              </el-icon>
              åˆ é™¤
            </el-button>
          </div>
        </div>
        <div class="card-body">
          <div class="route-meta">
            <span class="meta-item">
              <el-icon>
                <Box />
              </el-icon>
              <span class="meta-label">产品:</span>
              <span class="meta-value">{{ route.productName }} - {{ route.productModelName }}</span>
            </span>
            <span class="meta-item">
              <el-icon>
                <Document />
              </el-icon>
              <span class="meta-label">BOM:</span>
              <span class="meta-value">{{ route.bomNo || '-' }}</span>
            </span>
            <span class="meta-item">
              <el-icon>
                <Document />
              </el-icon>
              <span class="meta-label">备注:</span>
              <span class="meta-value">{{ route.description || '暂无描述' }}</span>
            </span>
          </div>
          <div class="expand-btn-wrapper">
            <el-button class="expand-btn"
                       :class="{ expanded: route.expanded }"
                       type="primary"
                       text
                       @click="toggleExpand(route)">
              <span class="btn-text">{{ route.expanded ? '收起工序路线' : '展开工序路线' }}</span>
              <el-icon class="expand-icon">
                <component :is="route.expanded ? 'ArrowUp' : 'ArrowDown'" />
              </el-icon>
            </el-button>
          </div>
        </div>
        <div v-if="route.expanded"
             class="process-route">
          <div class="process-flow">
            <div v-for="(process, index) in route.processList"
                 :key="process.id"
                 class="process-flow-item"
                 draggable="true"
                 @dragstart="handleDragStart($event, index, route.id)"
                 @dragover="handleDragOver($event)"
                 @drop="handleDrop($event, index, route.id)"
                 @dragend="handleDragEnd">
              <div class="process-node"
                   :class="{ expanded: process.expanded }">
                <div class="process-node-header">
                  <div class="process-number">{{ index + 1 }}</div>
                  <div class="process-actions">
                    <el-button link
                               type="primary"
                               @click="handleEditProcessSelect(route, index, process)">
                      <el-icon>
                        <Edit />
                      </el-icon>
                    </el-button>
                    <el-button link
                               type="danger"
                               @click="handleDeleteProcess(route.id, process)">
                      <el-icon>
                        <Delete />
                      </el-icon>
                    </el-button>
                  </div>
                </div>
                <div class="process-node-body">
                  <!-- <div class="process-code">{{ process.processId }}</div> -->
                  <div class="process-name">{{ process.processName }}</div>
                  <!-- <div class="process-desc">{{ process.remark || '暂无描述' }}</div> -->
                </div>
                <div class="process-node-footer">
                  <!-- <el-tag size="small"
                          :type="process.status === '1' ? 'success' : 'info'">
                    {{ process.status === '1' ? '启用' : '停用' }}
                  </el-tag> -->
                  <el-button type="primary"
                             link
                             size="small"
                             @click="toggleProcessParams(process)">
                    {{ process.expanded ? '收起参数' : '展开参数' }}
                    ({{ process.paramCount }})
                  </el-button>
                </div>
                <div v-if="process.expanded"
                     class="process-params-section">
                  <div class="params-header">
                    <span>参数列表</span>
                    <el-button type="primary"
                               link
                               size="small"
                               @click="handleAddParam(route.id, process)">
                      <el-icon>
                        <Plus />
                      </el-icon>新增
                    </el-button>
                  </div>
                  <div class="params-list">
                    <div v-for="param in process.paramList"
                         :key="param.id"
                         class="param-item">
                      <div class="param-info">
                        <span class="param-code">{{ param.paramName }}</span>
                        <!-- <span class="param-name">{{ param.paramName }}</span> -->
                        <!-- <el-tag size="small"
                                style="margin-right: 20px;"
                                :type="getParamTypeTag(param.parameterType)">
                          {{ param.parameterType }}
                        </el-tag> -->
                        <span v-if="param.valueMode==1"
                              class="param-value">标准值:{{ param.standardValue || "-" }} {{ param.unit }}</span>
                        <span v-else
                              class="param-value">标准值:{{ param.minValue || "-" }}-{{ param.maxValue || "-" }} {{ param.unit }}</span>
                      </div>
                      <div class="param-actions">
                        <el-button link
                                   type="primary"
                                   size="small"
                                   @click="handleEditParam(route.id, process, param)">
                          ç¼–辑
                        </el-button>
                        <el-button link
                                   type="danger"
                                   size="small"
                                   @click="handleDeleteParam(route.id, process, param)">
                          åˆ é™¤
                        </el-button>
                      </div>
                    </div>
                    <el-empty v-if="!process.paramList || process.paramList.length === 0"
                              description="暂无参数"
                              :image-size="50" />
                  </div>
                </div>
              </div>
              <div v-if="index < route.processList.length - 1"
                   class="flow-arrow">
                <el-icon>
                  <Right />
                </el-icon>
              </div>
            </div>
            <div class="add-process-node"
                 @click="handleSelectProcess(route, index)">
              <el-icon>
                <Plus />
              </el-icon>
              <span>新增工序</span>
            </div>
          </div>
        </div>
      </div>
    </div>
    <!-- åˆ†é¡µæŽ§ä»¶ -->
    <div class="pagination-container">
      <el-pagination v-model:current-page="routePage.current"
                     v-model:page-size="routePage.size"
                     :page-sizes="[10, 20, 50, 100]"
                     layout="total, sizes, prev, pager, next, jumper"
                     :total="routePage.total"
                     @size-change="handleRouteSizeChange"
                     @current-change="handleRouteCurrentChange" />
    </div>
    <!-- å·¥è‰ºè·¯çº¿æ–°å¢ž/编辑对话框 -->
    <el-dialog v-model="routeDialogVisible"
               :title="isRouteEdit ? '编辑工艺路线' : '新增工艺路线'"
               width="500px">
      <el-form :model="routeForm"
               :rules="routeRules"
               ref="routeFormRef"
               label-width="120px">
        <el-form-item label="产品名称"
                      prop="productModelId">
          <el-button type="primary"
                     @click="handleProcessProductSelectClick2">
            {{ routeForm.productName && routeForm.productModelName
              ? `${routeForm.productName} - ${routeForm.productModelName}`
              : '选择产品' }}
          </el-button>
        </el-form-item>
        <el-form-item label="BOM"
                      prop="bomId">
          <el-select v-model="routeForm.bomId"
                     placeholder="请选择BOM"
                     clearable
                     :disabled="!routeForm.productModelId || bomOptions.length === 0"
                     style="width: 100%">
            <el-option v-for="item in bomOptions"
                       :key="item.id"
                       :label="item.bomNo || `BOM-${item.id}`"
                       :value="item.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="路线编码"
                      prop="routeCode">
          <el-input v-model="routeForm.routeCode"
                    disabled
                    placeholder="自动生成" />
        </el-form-item>
        <el-form-item label="备注"
                      prop="description">
          <el-input v-model="routeForm.description"
                    type="textarea"
                    :rows="3"
                    placeholder="请输入路线描述" />
        </el-form-item>
        <!-- <el-form-item label="状态"
                      prop="status">
          <el-radio-group v-model="routeForm.status">
            <el-radio label="1">启用</el-radio>
            <el-radio label="0">停用</el-radio>
          </el-radio-group>
        </el-form-item> -->
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="routeDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="handleRouteSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- äº§å“é€‰æ‹©å¼¹çª— -->
    <ProductSelectDialog v-model="showProductSelectDialog"
                         @confirm="handleProductSelect"
                         single />
    <!-- å·¥åºæ–°å¢ž/编辑对话框 -->
    <el-dialog v-model="processDialogVisible"
               :title="isProcessEdit ? '编辑工序' : '新增工序'"
               width="500px">
      <el-form :model="processForm"
               :rules="processRules"
               ref="processFormRef"
               label-width="120px">
        <el-form-item label="工序编码"
                      prop="no">
          <el-input v-model="processForm.no"
                    placeholder="请输入工序编码" />
        </el-form-item>
        <el-form-item label="工序名称"
                      prop="name">
          <el-input v-model="processForm.name"
                    placeholder="请输入工序名称" />
        </el-form-item>
        <el-form-item label="工序描述"
                      prop="remark">
          <el-input v-model="processForm.remark"
                    type="textarea"
                    :rows="3"
                    placeholder="请输入工序描述" />
        </el-form-item>
        <el-form-item label="状态"
                      prop="status">
          <el-radio-group v-model="processForm.status">
            <el-radio :label="true">启用</el-radio>
            <el-radio :label="false">停用</el-radio>
          </el-radio-group>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="processDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="handleProcessSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- é€‰æ‹©å·¥åºå¯¹è¯æ¡† -->
    <el-dialog v-model="selectProcessDialogVisible"
               title="选择工序"
               width="1000px">
      <div class="process-select-container">
        <!-- å·¦ä¾§å·¥åºåˆ—表 -->
        <div class="process-list-area">
          <div class="area-title">可选工序</div>
          <div class="search-box">
            <el-input v-model="processSearchKeyword"
                      placeholder="请输入工序名称搜索"
                      clearable
                      size="small"
                      @input="handleProcessSearch">
              <template #prefix>
                <el-icon>
                  <Search />
                </el-icon>
              </template>
            </el-input>
          </div>
          <el-table :data="filteredProcessList"
                    height="360"
                    border
                    highlight-current-row
                    @current-change="handleProcessSelect">
            <el-table-column prop="no"
                             label="工序编号"
                             width="100" />
            <el-table-column prop="name"
                             label="工序名称" />
            <el-table-column prop="remark"
                             label="工序描述" />
            <el-table-column prop="status"
                             label="状态"
                             width="80">
              <template #default="scope">
                <el-tag size="small"
                        :type="scope.row.status ? 'success' : 'info'">
                  {{ scope.row.status ? '启用' : '停用' }}
                </el-tag>
              </template>
            </el-table-column>
          </el-table>
        </div>
        <!-- å³ä¾§å·¥åºè¯¦æƒ… -->
        <div class="process-detail-area">
          <div class="area-title">工序详情</div>
          <el-form v-if="selectedProcessItem"
                   :model="processForm"
                   label-width="100px"
                   class="process-detail-form">
            <el-form-item label="工序编号">
              <span class="detail-text">{{ selectedProcessItem.no }}</span>
            </el-form-item>
            <el-form-item label="工序名称">
              <span class="detail-text">{{ selectedProcessItem.name }}</span>
            </el-form-item>
            <el-form-item label="工序描述">
              <span class="detail-text">{{ selectedProcessItem.remark || '-' }}</span>
            </el-form-item>
            <el-form-item label="状态">
              <el-tag size="small"
                      :type="selectedProcessItem.status ? 'success' : 'info'">
                {{ selectedProcessItem.status ? '启用' : '停用' }}
              </el-tag>
            </el-form-item>
            <el-form-item label="是否质检">
              <el-tag size="small"
                      :type="selectedProcessItem.isQuality ? 'success' : 'info'">
                {{ selectedProcessItem.isQuality ? '质检' : '非质检' }}
              </el-tag>
            </el-form-item>
            <el-form-item label="产品名称"
                          prop="productModelId">
              <el-button type="primary"
                         @click="handleProcessProductSelectClick">
                {{ processForm.productName && processForm.model
                  ? `${processForm.productName} - ${processForm.model}`
                  : '选择产品' }}
              </el-button>
            </el-form-item>
            <el-form-item label="单位"
                          prop="unit">
              <el-input v-model="processForm.unit"
                        :placeholder="processForm.productModelId ? '根据选择的产品自动带出' : '请先选择产品' "
                        clearable
                        :disabled="true" />
            </el-form-item>
            <el-form-item label="是否质检"
                          prop="isQuality">
              <el-switch v-model="processForm.isQuality"
                         :active-value="true"
                         inactive-value="false" />
            </el-form-item>
          </el-form>
          <el-empty v-else
                    description="请从左侧选择工序" />
        </div>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="selectProcessDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     :disabled="!selectedProcessItem || !processForm.productModelId"
                     @click="handleProcessSelectSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- å‚数新增/编辑对话框 -->
    <el-dialog v-model="paramDialogVisible"
               :title="isParamEdit ? '编辑参数' : '新增参数'"
               width="500px">
      <el-form :model="paramForm"
               :rules="paramRules"
               ref="paramFormRef"
               label-width="120px">
        <el-form-item label="参数编号"
                      prop="parameterCode">
          <el-input v-model="paramForm.parameterCode"
                    placeholder="请输入参数编号" />
        </el-form-item>
        <el-form-item label="参数名称"
                      prop="parameterName">
          <el-input v-model="paramForm.parameterName"
                    placeholder="请输入参数名称" />
        </el-form-item>
        <el-form-item label="参数模式"
                      prop="parameterType2">
          <el-select v-model="paramForm.parameterType2"
                     placeholder="请选择参数模式">
            <el-option label="单值"
                       value="1" />
            <el-option label="区间"
                       value="2" />
          </el-select>
        </el-form-item>
        <el-form-item label="参数类型"
                      prop="parameterType">
          <el-select v-model="paramForm.parameterType"
                     @change="handleParamTypeChange"
                     placeholder="请选择参数类型">
            <el-option label="数值格式"
                       value="数值格式" />
            <el-option label="文本格式"
                       value="文本格式" />
            <el-option label="下拉选项"
                       value="下拉选项" />
            <el-option label="时间格式"
                       value="时间格式" />
          </el-select>
        </el-form-item>
        <el-form-item v-if="paramForm.parameterType === '下拉选项'"
                      label="数据字典"
                      prop="parameterFormat">
          <el-select v-model="paramForm.parameterFormat"
                     placeholder="请选择数据字典">
            <el-option v-for="item in dictTypes"
                       :key="item.dictType"
                       :label="item.dictName"
                       :value="item.dictType" />
          </el-select>
        </el-form-item>
        <el-form-item v-else-if="paramForm.parameterType === '时间格式'"
                      label="时间格式"
                      prop="parameterFormat">
          <el-select v-model="paramForm.parameterFormat"
                     placeholder="请选择时间格式">
            <el-option label="YYYY-MM-DD HH:mm:ss"
                       value="YYYY-MM-DD HH:mm:ss" />
            <el-option label="YYYY-MM-DD"
                       value="YYYY-MM-DD" />
          </el-select>
        </el-form-item>
        <el-form-item v-else
                      label="参数格式"
                      prop="parameterFormat">
          <el-input v-model="paramForm.parameterFormat"
                    placeholder="请输入参数格式" />
        </el-form-item>
        <el-form-item label="标准值"
                      prop="standardValue">
          <el-input v-model="paramForm.standardValue"
                    placeholder="请输入标准值" />
        </el-form-item>
        <el-form-item label="单位"
                      prop="unit">
          <el-input v-model="paramForm.unit"
                    placeholder="请输入单位" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="paramDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="handleParamSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- é€‰æ‹©å‚数对话框 -->
    <el-dialog v-model="selectParamDialogVisible"
               title="选择参数"
               width="1000px">
      <div class="param-select-container">
        <!-- å·¦ä¾§å‚数列表 -->
        <div class="param-list-area">
          <div class="area-title">可选参数</div>
          <div class="search-box">
            <el-input v-model="paramSearchKeyword"
                      placeholder="请输入参数名称搜索"
                      clearable
                      size="small"
                      @input="handleParamSearch">
              <template #prefix>
                <el-icon>
                  <Search />
                </el-icon>
              </template>
            </el-input>
          </div>
          <el-table :data="filteredParamList"
                    height="300"
                    border
                    highlight-current-row
                    @current-change="handleParamSelect">
            <el-table-column prop="paramName"
                             label="参数名称" />
            <el-table-column prop="paramType"
                             label="参数类型">
              <template #default="scope">
                <el-tag size="small"
                        :type="getParamTypeTag(scope.row.paramType)">
                  {{ getParamTypeText(scope.row.paramType) }}
                </el-tag>
              </template>
            </el-table-column>
          </el-table>
          <!-- åˆ†é¡µæŽ§ä»¶ -->
          <div class="pagination-container"
               style="margin-top: 10px;">
            <el-pagination v-model:current-page="paramPage.current"
                           v-model:page-size="paramPage.size"
                           :page-sizes="[10, 20, 50, 100]"
                           layout="total, sizes, prev, pager, next, jumper"
                           :total="paramPage.total"
                           @size-change="handleParamSizeChange"
                           @current-change="handleParamCurrentChange"
                           size="small" />
          </div>
        </div>
        <!-- å³ä¾§å‚数详情 -->
        <div class="param-detail-area">
          <div class="area-title">参数详情</div>
          <el-form v-if="selectedParam"
                   :model="selectedParam"
                   label-width="100px"
                   class="param-detail-form">
            <el-form-item label="参数名称">
              <span class="detail-text">{{ selectedParam.paramName }}</span>
            </el-form-item>
            <el-form-item label="参数模式">
              <el-tag size="small"
                      :type="selectedParam.valueMode == '1' ? 'success' : 'warning'">
                {{ selectedParam.valueMode == '1' ? '单值' : '区间' }}
              </el-tag>
            </el-form-item>
            <el-form-item label="参数类型">
              <el-tag size="small"
                      :type="getParamTypeTag(selectedParam.paramType)">
                {{ getParamTypeText(selectedParam.paramType) }}
              </el-tag>
            </el-form-item>
            <el-form-item label="参数格式">
              <span class="detail-text">{{ selectedParam.paramFormat || '-' }}</span>
            </el-form-item>
            <el-form-item label="单位">
              <span class="detail-text">{{ selectedParam.unit || '-' }}</span>
            </el-form-item>
            <el-form-item label="标准值"
                          v-if="selectedParam.valueMode == '1' && selectedParam.paramType == '1'">
              <el-input v-model="selectedParam.standardValue"
                        type="number"
                        placeholder="请输入默认值" />
            </el-form-item>
            <el-form-item label="最小值"
                          v-if="selectedParam.valueMode == '2' && selectedParam.paramType == '1'">
              <el-input v-model="selectedParam.minValue"
                        type="number"
                        placeholder="请输入最小值" />
            </el-form-item>
            <el-form-item label="最大值"
                          v-if="selectedParam.valueMode == '2' && selectedParam.paramType == '1'">
              <el-input v-model="selectedParam.maxValue"
                        type="number"
                        placeholder="请输入最大值" />
            </el-form-item>
            <el-form-item label="排序">
              <el-input v-model="selectedParam.sort"
                        type="number"
                        placeholder="请输入排序" />
            </el-form-item>
            <el-form-item label="是否必填">
              <el-switch v-model="selectedParam.isRequired"
                         :active-value="1"
                         :inactive-value="0" />
            </el-form-item>
          </el-form>
          <el-empty v-else
                    description="请从左侧选择参数" />
        </div>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="selectParamDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     :disabled="!selectedParam"
                     @click="handleParamSelectSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- ç¼–辑参数对话框 -->
    <el-dialog v-model="editParamDialogVisible"
               title="编辑参数"
               width="600px">
      <el-form :model="editParamForm"
               :rules="editParamRules"
               ref="editParamFormRef"
               label-width="120px">
        <el-form-item label="参数名称">
          <span class="detail-text">{{ editParamForm.paramName }}</span>
        </el-form-item>
        <el-form-item label="参数模式">
          <el-tag size="small"
                  :type="editParamForm.valueMode == '1' ? 'success' : 'warning'">
            {{ editParamForm.valueMode == '1' ? '单值' : '区间' }}
          </el-tag>
        </el-form-item>
        <el-form-item label="参数类型">
          <el-tag size="small"
                  :type="getParamTypeTag(editParamForm.paramType)">
            {{ getParamTypeText(editParamForm.paramType) }}
          </el-tag>
        </el-form-item>
        <el-form-item label="参数格式">
          <span class="detail-text">{{ editParamForm.paramFormat || '-' }}</span>
        </el-form-item>
        <el-form-item label="单位">
          <span class="detail-text">{{ editParamForm.unit || '-' }}</span>
        </el-form-item>
        <el-form-item label="标准值"
                      v-if="editParamForm.valueMode == '1' && editParamForm.paramType == '1'"
                      prop="standardValue">
          <el-input v-model="editParamForm.standardValue"
                    type="number"
                    placeholder="请输入标准值" />
        </el-form-item>
        <el-form-item label="最小值"
                      v-if="editParamForm.valueMode == '2' && editParamForm.paramType == '1'"
                      prop="minValue">
          <el-input v-model="editParamForm.minValue"
                    type="number"
                    placeholder="请输入最小值" />
        </el-form-item>
        <el-form-item label="最大值"
                      v-if="editParamForm.valueMode == '2' && editParamForm.paramType == '1'"
                      prop="maxValue">
          <el-input v-model="editParamForm.maxValue"
                    type="number"
                    placeholder="请输入最大值" />
        </el-form-item>
        <el-form-item label="排序"
                      prop="sort">
          <el-input v-model="editParamForm.sort"
                    type="number"
                    placeholder="请输入排序" />
        </el-form-item>
        <el-form-item label="是否必填"
                      prop="isRequired">
          <el-switch v-model="editParamForm.isRequired"
                     :active-value="1"
                     :inactive-value="0" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="editParamDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="handleEditParamSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
  import { ref, reactive, getCurrentInstance, onMounted } from "vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import {
    Plus,
    Edit,
    Delete,
    ArrowUp,
    ArrowDown,
    Right,
    Search,
    Check,
    Close,
    Box,
    Document,
  } from "@element-plus/icons-vue";
  import { listType } from "@/api/system/dict/type";
  import { getByModel } from "@/api/productionManagement/productBom.js";
  import { add, update, del } from "@/api/productionManagement/processRoute.js";
  import {
    addOrUpdateProcessRouteItem,
    batchDeleteProcessRouteItem,
    sortProcessRouteItem,
    findProcessRouteItemList,
    getProcessParamList,
    addProcessRouteItemParam,
    editProcessRouteItemParam,
    delProcessRouteItemParam,
  } from "@/api/productionManagement/processRouteItem.js";
  import { list as getProcessListApi } from "@/api/productionManagement/productionProcess.js";
  import { getBaseParamList } from "@/api/basicData/parameterMaintenance.js";
  import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
  // å·¥è‰ºè·¯çº¿åˆ—表
  const routeList = ref([]);
  const dictTypes = ref([]);
  // å·¥è‰ºè·¯çº¿åˆ†é¡µ
  const routePage = reactive({
    current: 1,
    size: 10,
    total: 0,
  });
  // èŽ·å–å…¨å±€å®žä¾‹
  const { proxy } = getCurrentInstance();
  // äº§å“é€‰æ‹©å’ŒBOM相关
  const showProductSelectDialog = ref(false);
  const bomOptions = ref([]);
  // å·¥è‰ºè·¯çº¿å¯¹è¯æ¡†
  const routeDialogVisible = ref(false);
  const isRouteEdit = ref(false);
  const routeFormRef = ref(null);
  const routeForm = reactive({
    id: null,
    productModelId: null,
    productName: "",
    productModelName: "",
    bomId: null,
    routeCode: "",
    description: "",
    status: true,
  });
  const routeRules = {
    productModelId: [
      { required: true, message: "请选择产品", trigger: "change" },
    ],
    bomId: [{ required: true, message: "请选择BOM", trigger: "change" }],
  };
  // å·¥åºå¯¹è¯æ¡†
  const processDialogVisible = ref(false);
  const isProcessEdit = ref(false);
  const processFormRef = ref(null);
  const currentRouteId = ref(null);
  const processForm = reactive({
    id: null,
    no: "",
    name: "",
    remark: "",
    status: true,
  });
  const processRules = {
    no: [{ required: true, message: "请输入工序编码", trigger: "blur" }],
    name: [{ required: true, message: "请输入工序名称", trigger: "blur" }],
  };
  // é€‰æ‹©å·¥åºå¯¹è¯æ¡†
  const selectProcessDialogVisible = ref(false);
  const availableProcessList = ref([]);
  const filteredProcessList = ref([]);
  const selectedProcessItem = ref(null);
  const processSearchKeyword = ref("");
  const currentRouteIndex = ref(null);
  // å‚数对话框
  const paramDialogVisible = ref(false);
  const isParamEdit = ref(false);
  const paramFormRef = ref(null);
  const currentProcessId = ref(null);
  const paramForm = reactive({
    id: null,
    parameterCode: "",
    parameterName: "",
    parameterType2: "1",
    parameterType: "",
    parameterFormat: "",
    standardValue: "",
    unit: "",
  });
  const paramRules = {
    parameterCode: [
      { required: true, message: "请输入参数编号", trigger: "blur" },
    ],
    parameterName: [
      { required: true, message: "请输入参数名称", trigger: "blur" },
    ],
    parameterType: [
      { required: true, message: "请选择参数类型", trigger: "change" },
    ],
  };
  // é€‰æ‹©å‚数对话框
  const selectParamDialogVisible = ref(false);
  const availableParamList = ref([]);
  const filteredParamList = ref([]);
  const selectedParam = ref(null);
  const paramSearchKeyword = ref("");
  // å¯é€‰å‚数分页
  const paramPage = reactive({
    current: 1,
    size: 10,
    total: 0,
  });
  // ç¼–辑参数对话框
  const editParamDialogVisible = ref(false);
  const editParamFormRef = ref(null);
  const editParamForm = reactive({
    id: null,
    processId: null,
    paramId: null,
    paramName: "",
    valueMode: "1",
    standardValue: null,
    minValue: null,
    maxValue: null,
    sort: 1,
    isRequired: 0,
  });
  const editParamRules = reactive({
    standardValue: [
      {
        required: true,
        message: "请输入标准值",
        trigger: "blur",
        validator: (rule, value, callback) => {
          if (value === null || value === undefined || value === "") {
            callback(new Error("请输入标准值"));
          } else {
            callback();
          }
        },
      },
    ],
    minValue: [
      {
        required: true,
        message: "请输入最小值",
        trigger: "blur",
        validator: (rule, value, callback) => {
          if (value === null || value === undefined || value === "") {
            callback(new Error("请输入最小值"));
          } else {
            callback();
          }
        },
      },
    ],
    maxValue: [
      {
        required: true,
        message: "请输入最大值",
        trigger: "blur",
        validator: (rule, value, callback) => {
          if (value === null || value === undefined || value === "") {
            callback(new Error("请输入最大值"));
          } else {
            callback();
          }
        },
      },
    ],
    sort: [
      {
        required: true,
        message: "请输入排序",
        trigger: "blur",
        validator: (rule, value, callback) => {
          if (value === null || value === undefined || value === "") {
            callback(new Error("请输入排序"));
          } else if (isNaN(value) || value < 1) {
            callback(new Error("排序必须是大于0的整数"));
          } else {
            callback();
          }
        },
      },
    ],
  });
  // æ‹–拽相关
  const draggedItem = ref(null);
  const draggedRouteId = ref(null);
  // èŽ·å–å·¥è‰ºè·¯çº¿åˆ—è¡¨
  const getRouteList = () => {
    // å¯¼å…¥ listPage æ–¹æ³•
    import("@/api/productionManagement/processRoute.js").then(({ listPage }) => {
      listPage({ pageNum: routePage.current, pageSize: routePage.size })
        .then(res => {
          // å¤„理返回的数据,映射到页面需要的格式
          routeList.value = (res.data?.records || []).map(item => ({
            id: item.id,
            productModelId: item.productModelId,
            productName: item.productName,
            productModelName: item.model || item.productModelName,
            bomId: item.bomId,
            bomNo: item.bomNo,
            routeCode: item.processRouteCode || item.routeCode,
            description: item.description || item.description,
            status: item.status,
            expanded: false,
            processList: (item.processList || []).map(process => ({
              ...process,
              processId: process.processId || process.id,
              expanded: false,
            })),
          }));
          // æ›´æ–°åˆ†é¡µæ€»æ•°
          routePage.total = res.data?.total || 0;
        })
        .catch(err => {
          console.error("获取工艺路线列表失败:", err);
          routeList.value = [];
          routePage.total = 0;
        });
    });
  };
  // å±•å¼€/收起工艺路线
  const toggleExpand = route => {
    route.expanded = !route.expanded;
    if (route.expanded) {
      // è°ƒç”¨æŽ¥å£èŽ·å–å·¥åºåˆ—è¡¨
      findProcessRouteItemList({ routeId: route.id })
        .then(res => {
          route.processList = (res.data || []).map(process => ({
            ...process,
            processId: process.processId || process.id,
            expanded: false,
          }));
        })
        .catch(err => {
          console.error("获取工序列表失败:", err);
          route.processList = [];
        });
    }
  };
  // å±•å¼€/收起工序参数
  const toggleProcessParams = process => {
    process.expanded = !process.expanded;
    if (process.expanded && process.id) {
      // è°ƒç”¨æŽ¥å£èŽ·å–å‚æ•°åˆ—è¡¨
      getProcessParamList({
        routeItemId: process.id,
        pageNum: 1,
        pageSize: 1000,
      })
        .then(res => {
          if (res.code === 200) {
            process.paramList = res.data?.records || [];
            process.paramCount = process.paramList.length;
          } else {
            ElMessage.error(res.msg || "获取参数列表失败");
            process.paramList = [];
            process.paramCount = 0;
          }
        })
        .catch(err => {
          console.error("获取参数列表失败:", err);
          ElMessage.error("获取参数列表失败");
          process.paramList = [];
          process.paramCount = 0;
        });
    }
  };
  const toggleProcessParams2 = process => {
    if (process.expanded && process.id) {
      // è°ƒç”¨æŽ¥å£èŽ·å–å‚æ•°åˆ—è¡¨
      getProcessParamList({
        routeItemId: process.id,
        pageNum: 1,
        pageSize: 1000,
      })
        .then(res => {
          if (res.code === 200) {
            process.paramList = res.data?.records || [];
            process.paramCount = process.paramList.length;
          } else {
            ElMessage.error(res.msg || "获取参数列表失败");
            process.paramList = [];
            process.paramCount = 0;
          }
        })
        .catch(err => {
          console.error("获取参数列表失败:", err);
          ElMessage.error("获取参数列表失败");
          process.paramList = [];
          process.paramCount = 0;
        });
    }
  };
  // å·¥è‰ºè·¯çº¿æ“ä½œ
  const handleAddRoute = () => {
    isRouteEdit.value = false;
    routeForm.id = null;
    routeForm.productModelId = null;
    routeForm.productName = "";
    routeForm.productModelName = "";
    routeForm.bomId = null;
    routeForm.routeCode = "";
    routeForm.description = "";
    routeForm.status = false;
    bomOptions.value = [];
    routeDialogVisible.value = true;
  };
  const handleEditRoute = route => {
    isRouteEdit.value = true;
    routeForm.id = route.id;
    routeForm.productModelId = route.productModelId;
    routeForm.productName = route.productName;
    routeForm.productModelName = route.productModelName;
    routeForm.bomId = route.bomId;
    routeForm.routeCode = route.routeCode;
    routeForm.description = route.description;
    routeForm.status = route.status;
    routeDialogVisible.value = true;
  };
  const handleDeleteRoute = route => {
    ElMessageBox.confirm("确定要删除该工艺路线吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }).then(() => {
      del(route.id)
        .then(res => {
          ElMessage.success("删除成功");
          getRouteList();
        })
        .catch(err => {
          ElMessage.error("删除失败");
        });
    });
  };
  const handleRouteSubmit = () => {
    routeFormRef.value.validate(valid => {
      if (valid) {
        // æž„建提交数据
        const submitData = {
          ...routeForm,
          // æ³¨æ„ï¼šAPI æœŸæœ›çš„字段名可能与表单字段名不同
          productId: routeForm.productModelId,
          productModelId: routeForm.productModelId,
          description: routeForm.description,
        };
        if (isRouteEdit.value) {
          // ç¼–辑操作
          update(submitData)
            .then(res => {
              ElMessage.success("编辑成功");
              routeDialogVisible.value = false;
              getRouteList();
            })
            .catch(err => {
              ElMessage.error("编辑失败");
            });
        } else {
          // æ–°å¢žæ“ä½œ
          add(submitData)
            .then(res => {
              ElMessage.success("新增成功");
              routeDialogVisible.value = false;
              getRouteList();
            })
            .catch(err => {
              ElMessage.error("新增失败");
            });
        }
      }
    });
  };
  const isform2 = ref(null);
  const handleProcessProductSelectClick = () => {
    isform2.value = true;
    showProductSelectDialog.value = true;
  };
  const handleProcessProductSelectClick2 = () => {
    isform2.value = false;
    showProductSelectDialog.value = true;
  };
  // äº§å“é€‰æ‹©å¤„理
  const handleProductSelect = async products => {
    if (isform2.value) {
      // å¸®æˆ‘写工序中的选择产品的回调,并且把字段加进processForm
      if (products && products.length > 0) {
        const product = products[0];
        console.log("product:", product);
        // æŠŠproduct中的字段添加到processForm中
        // Object.assign(processForm, product);
        processForm.productModelId = product.id;
        processForm.productName = product.productName;
        processForm.model = product.model;
        processForm.unit = product.unit || "";
        console.log("processForm:", processForm);
        // è§¦å‘表单验证更新
        proxy.$refs["processFormRef"]?.validateField("productModelId");
      }
    } else {
      if (products && products.length > 0) {
        const product = products[0];
        // å…ˆæŸ¥è¯¢BOM列表(必选)
        try {
          const res = await getByModel(product.id);
          // å¤„理返回的BOM数据:可能是数组、对象或包含data字段
          let bomList = [];
          if (Array.isArray(res)) {
            bomList = res;
          } else if (res && res.data) {
            bomList = Array.isArray(res.data) ? res.data : [res.data];
          } else if (res && typeof res === "object") {
            bomList = [res];
          }
          console.log("bomList:", bomList);
          if (bomList.length > 0) {
            routeForm.productModelId = product.id;
            routeForm.productName = product.productName;
            routeForm.productModelName = product.model;
            routeForm.bomId = undefined; // é‡ç½®BOM选择
            bomOptions.value = bomList;
            showProductSelectDialog.value = false;
            // è§¦å‘表单验证更新
            proxy.$refs["routeFormRef"]?.validateField("productModelId");
          } else {
            proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
          }
        } catch (error) {
          // å¦‚果接口返回404或其他错误,说明没有BOM
          proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
        }
      }
    }
  };
  const handleApproveRoute = route => {
    ElMessageBox.confirm("确定要批准该工艺路线吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "info",
    }).then(() => {
      // è°ƒç”¨ä¿®æ”¹æŽ¥å£ï¼Œåªä¿®æ”¹status字段为批准状态
      update({ id: route.id, status: true })
        .then(res => {
          ElMessage.success("批准成功");
          getRouteList();
        })
        .catch(err => {
          ElMessage.error("批准失败");
        });
    });
  };
  const handleRevokeApproveRoute = route => {
    ElMessageBox.confirm("确定要撤销批准该工艺路线吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }).then(() => {
      // è°ƒç”¨ä¿®æ”¹æŽ¥å£ï¼Œåªä¿®æ”¹status字段为草稿状态
      update({ id: route.id, status: false })
        .then(res => {
          ElMessage.success("撤销批准成功");
          getRouteList();
        })
        .catch(err => {
          ElMessage.error("撤销批准失败");
        });
    });
  };
  // å·¥åºæ“ä½œ
  const handleSelectProcess = (route, index) => {
    console.log("route:", route);
    currentRouteId.value = route.id;
    currentRouteIndex.value = index;
    // é‡ç½®æœç´¢å’Œé€‰æ‹©çŠ¶æ€
    filteredProcessList.value = availableProcessList.value;
    processSearchKeyword.value = "";
    selectedProcessItem.value = null;
    selectProcessDialogVisible.value = true;
  };
  const dragSort = ref(0);
  const currentId = ref(null);
  // ä¿®æ”¹å·¥åº
  const handleEditProcessSelect = (route, index, process) => {
    console.log("route:", route);
    console.log("process:", process);
    currentId.value = process.id;
    currentRouteId.value = route.id;
    currentRouteIndex.value = index;
    // é‡ç½®æœç´¢å’Œé€‰æ‹©çŠ¶æ€
    filteredProcessList.value = availableProcessList.value;
    processSearchKeyword.value = "";
    // è®¾ç½®é€‰ä¸­çš„工序
    filteredProcessList.value.map(item => {
      if (item.id === process.processId) {
        selectedProcessItem.value = item;
      }
    });
    dragSort.value = process.dragSort;
    // selectedProcessItem.value = process;
    // å¡«å……产品选择表单
    processForm.productModelId = process.productModelId;
    processForm.productName = process.productName;
    processForm.model = process.model;
    processForm.processId = process.no;
    // processForm.name = process.name;
    processForm.unit = process.unit || "";
    processForm.isQuality = process.isQuality || false;
    selectProcessDialogVisible.value = true;
  };
  const handleEditProcess = (routeId, process) => {
    currentRouteId.value = routeId;
    isProcessEdit.value = true;
    processForm.id = process.id;
    processForm.no = process.no;
    processForm.name = process.name;
    processForm.remark = process.remark;
    processForm.status = process.status;
    processDialogVisible.value = true;
  };
  const handleDeleteProcess = (routeId, process) => {
    ElMessageBox.confirm("确定要删除该工序吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }).then(() => {
      // è°ƒç”¨API删除工序
      batchDeleteProcessRouteItem([process.id])
        .then(res => {
          ElMessage.success("删除成功");
          // è°ƒç”¨æŽ¥å£æ›´æ–°å·¥åºåˆ—表
          findProcessRouteItemList({ routeId: routeId })
            .then(res => {
              const route = routeList.value.find(r => r.id === routeId);
              if (route) {
                route.processList = (res.data || []).map(process => ({
                  ...process,
                  processId: process.processId || process.id,
                  expanded: false,
                }));
              }
            })
            .catch(err => {
              console.error("获取工序列表失败:", err);
            });
        })
        .catch(err => {
          ElMessage.error("删除失败");
          console.error("删除工序失败:", err);
        });
    });
  };
  const handleProcessSubmit = () => {
    processFormRef.value.validate(valid => {
      if (valid) {
        ElMessage.success(isProcessEdit.value ? "编辑成功" : "新增成功");
        processDialogVisible.value = false;
        // è°ƒç”¨æŽ¥å£æ›´æ–°å·¥åºåˆ—表
        if (currentRouteId.value) {
          findProcessRouteItemList({ routeId: currentRouteId.value })
            .then(res => {
              const route = routeList.value.find(
                r => r.id === currentRouteId.value
              );
              if (route) {
                route.processList = (res.data || []).map(process => ({
                  ...process,
                  processId: process.processId || process.id,
                  expanded: false,
                }));
              }
            })
            .catch(err => {
              console.error("获取工序列表失败:", err);
            });
        }
      }
    });
  };
  // é€‰æ‹©å·¥åºç›¸å…³æ–¹æ³•
  const handleProcessSearch = () => {
    const keyword = processSearchKeyword.value.trim().toLowerCase();
    if (!keyword) {
      filteredProcessList.value = availableProcessList.value;
    } else {
      filteredProcessList.value = availableProcessList.value.filter(
        item =>
          (item.name && item.name.toLowerCase().includes(keyword)) ||
          (item.no && item.no.toLowerCase().includes(keyword))
      );
    }
  };
  const handleProcessSelect = row => {
    selectedProcessItem.value = row;
    // é‡ç½®äº§å“é€‰æ‹©è¡¨å•
    processForm.productModelId = undefined;
    processForm.productName = "";
    processForm.productModelName = "";
    processForm.unit = "";
    processForm.isQuality = row.isQuality || false;
  };
  // å¤„理工序选择时的产品选择
  const handleProcessProductSelect = async products => {
    if (products && products.length > 0) {
      const product = products[0];
      processForm.productModelId = product.id;
      processForm.productName = product.productName;
      processForm.productModelName = product.model;
      processForm.unit = product.unit || "";
      showProductSelectDialog.value = false;
    }
  };
  const handleProcessSelectSubmit = () => {
    if (!selectedProcessItem.value) {
      ElMessage.warning("请先选择一个工序");
      return;
    }
    if (!processForm.productModelId) {
      ElMessage.warning("请选择产品");
      return;
    }
    // æž„建请求参数
    const params = {
      routeId: currentRouteId.value,
      processId: selectedProcessItem.value.id,
      dragSort: routePage.total + 1,
      ...processForm,
    };
    // å¦‚果是修改操作,添加id参数
    if (selectedProcessItem.value.id) {
      params.id = currentId.value;
      params.dragSort = dragSort.value;
    }
    // è°ƒç”¨API添加工序或修改工序
    addOrUpdateProcessRouteItem(params)
      .then(res => {
        ElMessage.success(
          selectedProcessItem.value.id ? "修改工序成功" : "添加工序成功"
        );
        selectProcessDialogVisible.value = false;
        // è°ƒç”¨æŽ¥å£æ›´æ–°å·¥åºåˆ—表
        findProcessRouteItemList({ routeId: currentRouteId.value })
          .then(res => {
            const route = routeList.value.find(
              r => r.id === currentRouteId.value
            );
            if (route) {
              route.processList = (res.data || []).map(process => ({
                ...process,
                processId: process.processId || process.id,
                expanded: false,
              }));
            }
          })
          .catch(err => {
            console.error("获取工序列表失败:", err);
          });
      })
      .catch(err => {
        ElMessage.error(
          selectedProcessItem.value.id ? "修改工序失败" : "添加工序失败"
        );
        console.error(
          selectedProcessItem.value.id ? "修改工序失败:" : "添加工序失败:",
          err
        );
      });
  };
  // å‚数操作
  const handleAddParam = (routeId, process) => {
    currentRouteId.value = routeId;
    currentProcessId.value = process.id;
    selectedParam.value = null;
    paramSearchKeyword.value = "";
    paramPage.current = 1;
    // èŽ·å–å¯é€‰å‚æ•°åˆ—è¡¨
    getBaseParamList({
      paramName: paramSearchKeyword.value,
      current: paramPage.current,
      size: paramPage.size,
    }).then(res => {
      if (res.code === 200) {
        filteredParamList.value = res.data?.records || [];
        paramPage.total = res.data?.total || 0;
      } else {
        ElMessage.error(res.msg || "查询失败");
      }
    });
    selectParamDialogVisible.value = true;
  };
  const handleEditParam = (routeId, process, param) => {
    currentRouteId.value = routeId;
    currentProcessId.value = process.id;
    editParamForm.id = param.id;
    editParamForm.processId = process.id;
    editParamForm.paramId = param.paramId;
    editParamForm.paramName = param.parameterName || param.paramName;
    editParamForm.valueMode = param.parameterType2 || param.valueMode || "1";
    editParamForm.standardValue = param.standardValue;
    editParamForm.minValue = param.minValue;
    editParamForm.maxValue = param.maxValue;
    editParamForm.sort = param.sort || 1;
    editParamForm.isRequired = param.isRequired || 0;
    editParamForm.paramType = param.parameterType || param.paramType;
    editParamForm.paramFormat = param.parameterFormat || param.paramFormat;
    editParamForm.unit = param.unit || param.unit;
    editParamDialogVisible.value = true;
  };
  const handleDeleteParam = (routeId, process, param) => {
    ElMessageBox.confirm("确定要删除该参数吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }).then(() => {
      // è°ƒç”¨API删除参数
      delProcessRouteItemParam(param.id)
        .then(res => {
          ElMessage.success("删除成功");
          // åˆ·æ–°å‚数列表
          toggleProcessParams2(process);
        })
        .catch(err => {
          ElMessage.error("删除参数失败");
          console.error("删除参数失败:", err);
        });
    });
  };
  const handleParamSubmit = () => {
    paramFormRef.value.validate(valid => {
      if (valid) {
        ElMessage.success(isParamEdit.value ? "编辑成功" : "新增成功");
        paramDialogVisible.value = false;
        getRouteList();
      }
    });
  };
  const handleParamTypeChange = () => {
    if (paramForm.parameterType === "数值格式") {
      paramForm.parameterFormat = "#.0000";
    } else if (paramForm.parameterType === "时间格式") {
      paramForm.parameterFormat = "YYYY-MM-DD HH:mm:ss";
    } else {
      paramForm.parameterFormat = "";
    }
  };
  const getParamTypeTag = type => {
    const typeMap = {
      1: "primary",
      2: "info",
      3: "warning",
      4: "success",
    };
    return typeMap[type] || "default";
  };
  const getParamTypeText = type => {
    const typeMap = {
      1: "数值格式",
      2: "文本格式",
      3: "下拉选项",
      4: "时间格式",
    };
    return typeMap[type] || "未知参数类型";
  };
  // é€‰æ‹©å‚数相关方法
  const handleParamSearch = () => {
    // é‡ç½®åˆ†é¡µ
    paramPage.current = 1;
    // é‡æ–°åŠ è½½æ•°æ®
    getBaseParamList({
      paramName: paramSearchKeyword.value,
      current: paramPage.current,
      size: paramPage.size,
    }).then(res => {
      if (res.code === 200) {
        filteredParamList.value = res.data?.records || [];
        paramPage.total = res.data?.total || 0;
      } else {
        ElMessage.error(res.msg || "查询失败");
      }
    });
  };
  const handleParamSelect = row => {
    selectedParam.value = row;
  };
  // å¤„理分页大小变化
  const handleParamSizeChange = size => {
    paramPage.size = size;
    getBaseParamList({
      paramName: paramSearchKeyword.value,
      current: paramPage.current,
      size: paramPage.size,
    }).then(res => {
      if (res.code === 200) {
        filteredParamList.value = res.data?.records || [];
        paramPage.total = res.data?.total || 0;
      } else {
        ElMessage.error(res.msg || "查询失败");
      }
    });
  };
  // å¤„理当前页码变化
  const handleParamCurrentChange = current => {
    paramPage.current = current;
    getBaseParamList({
      paramName: paramSearchKeyword.value,
      current: paramPage.current,
      size: paramPage.size,
    }).then(res => {
      if (res.code === 200) {
        filteredParamList.value = res.data?.records || [];
        paramPage.total = res.data?.total || 0;
      } else {
        ElMessage.error(res.msg || "查询失败");
      }
    });
  };
  // å·¥è‰ºè·¯çº¿åˆ†é¡µå¤„理
  const handleRouteSizeChange = size => {
    routePage.size = size;
    getRouteList();
  };
  const handleRouteCurrentChange = current => {
    routePage.current = current;
    getRouteList();
  };
  const handleParamSelectSubmit = () => {
    if (!selectedParam.value) {
      ElMessage.warning("请先选择一个参数");
      return;
    }
    // æ‰¾åˆ°å¯¹åº”的工艺路线和工序
    const route = routeList.value.find(r => r.id === currentRouteId.value);
    const process = route?.processList.find(p => p.id === currentProcessId.value);
    if (route && process) {
      // æ£€æŸ¥å‚数是否已存在
      // const exists = process.paramList?.some(
      //   p =>
      //     p.paramId === selectedParam.value.id ||
      //     p.parameterCode === selectedParam.value.paramCode
      // );
      // if (exists) {
      //   ElMessage.warning("该参数已存在于工序中");
      //   return;
      // }
      // åˆ¤æ–­å‚数类型,只有数值类型才传标准值、最大值和最小值
      const isNumericMode = selectedParam.value.valueMode === 1;
      // è°ƒç”¨API新增参数
      addProcessRouteItemParam({
        routeItemId: process.id,
        paramId: selectedParam.value.id,
        standardValue: isNumericMode
          ? selectedParam.value.standardValue || ""
          : "",
        minValue: isNumericMode ? selectedParam.value.minValue || 0 : null,
        maxValue: isNumericMode ? selectedParam.value.maxValue || 0 : null,
        isRequired: selectedParam.value.isRequired || 0,
      })
        .then(res => {
          ElMessage.success("添加参数成功");
          selectParamDialogVisible.value = false;
          // åˆ·æ–°å‚数列表
          toggleProcessParams2(process);
        })
        .catch(err => {
          ElMessage.error("添加参数失败");
          console.error("添加参数失败:", err);
        });
    }
  };
  const handleEditParamSubmit = () => {
    editParamFormRef.value.validate(valid => {
      if (valid) {
        // åˆ¤æ–­å‚数类型,只有数值类型才传标准值、最大值和最小值
        const isNumericMode = editParamForm.valueMode == 1;
        // è°ƒç”¨API修改参数
        editProcessRouteItemParam({
          id: editParamForm.id,
          routeItemId: currentProcessId.value,
          paramId: editParamForm.paramId,
          standardValue: isNumericMode ? editParamForm.standardValue || "" : "",
          minValue: isNumericMode ? editParamForm.minValue || 0 : null,
          maxValue: isNumericMode ? editParamForm.maxValue || 0 : null,
          isRequired: editParamForm.isRequired || 0,
        })
          .then(res => {
            ElMessage.success("编辑成功");
            editParamDialogVisible.value = false;
            // æ‰¾åˆ°å¯¹åº”的工艺路线和工序
            const route = routeList.value.find(
              r => r.id === currentRouteId.value
            );
            const process = route?.processList.find(
              p => p.id === currentProcessId.value
            );
            // åˆ·æ–°å‚数列表
            if (process) {
              toggleProcessParams2(process);
            }
          })
          .catch(err => {
            ElMessage.error("编辑参数失败");
            console.error("编辑参数失败:", err);
          });
      }
    });
  };
  // æ‹–拽排序
  const handleDragStart = (event, index, routeId) => {
    draggedItem.value = index;
    draggedRouteId.value = routeId;
    event.dataTransfer.effectAllowed = "move";
  };
  const handleDragOver = event => {
    event.preventDefault();
    event.dataTransfer.dropEffect = "move";
  };
  const handleDrop = (event, dropIndex, routeId) => {
    event.preventDefault();
    if (draggedItem.value === null || draggedItem.value === dropIndex) return;
    const route = routeList.value.find(r => r.id === routeId);
    if (route && route.processList) {
      const draggedProcess = route.processList[draggedItem.value];
      // è®¡ç®—新的排序值
      const newDragSort = dropIndex + 1;
      // è°ƒç”¨API排序工序
      sortProcessRouteItem({
        id: draggedProcess.id,
        dragSort: newDragSort,
      })
        .then(res => {
          // è°ƒç”¨æŽ¥å£èŽ·å–æœ€æ–°çš„å·¥åºåˆ—è¡¨
          findProcessRouteItemList({ routeId: routeId })
            .then(res => {
              if (route) {
                route.processList = (res.data || []).map(process => ({
                  ...process,
                  processId: process.processId || process.id,
                  expanded: false,
                }));
              }
              ElMessage.success("排序成功");
            })
            .catch(err => {
              console.error("获取工序列表失败:", err);
              ElMessage.success("排序成功");
            });
        })
        .catch(err => {
          ElMessage.error("排序失败");
          console.error("排序工序失败:", err);
        });
    }
  };
  const handleDragEnd = () => {
    draggedItem.value = null;
    draggedRouteId.value = null;
  };
  // èŽ·å–æ•°æ®å­—å…¸
  const getDictTypes = () => {
    listType({ pageNum: 1, pageSize: 1000 }).then(res => {
      dictTypes.value = res.rows || [];
    });
  };
  getRouteList();
  getDictTypes();
  // é¡µé¢åŠ è½½æ—¶èŽ·å–å·¥åºåˆ—è¡¨
  onMounted(() => {
    getProcessListApi()
      .then(res => {
        // å¤„理返回的数据,映射到页面需要的格式
        availableProcessList.value = (res.data || []).map(item => ({
          id: item.id,
          no: item.no || item.no,
          name: item.name || item.name,
          remark: item.remark || item.remark,
          status: item.status,
          isQuality: item.isQuality,
        }));
        filteredProcessList.value = availableProcessList.value;
      })
      .catch(() => {
        ElMessage.error("获取工序列表失败");
      });
  });
</script>
<style scoped lang="scss">
  .app-container {
    padding: 20px;
    padding-bottom: 80px;
    background-color: #f0f2f5;
    min-height: calc(100vh - 84px);
    overflow: hidden;
  }
  .route-header {
    margin-bottom: 20px;
    .add-route-btn {
      width: 100%;
      display: inline-flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      min-width: 120px;
      height: 100px;
      border: 2px dashed #dcdfe6;
      border-radius: 12px;
      background: #fafafa;
      cursor: pointer;
      transition: all 0.3s ease;
      color: #909399;
      padding: 0 20px;
      .el-icon {
        font-size: 24px;
        margin-bottom: 8px;
      }
      span {
        font-size: 13px;
      }
      &:hover {
        border-color: #409eff;
        background: #ecf5ff;
        color: #409eff;
      }
    }
  }
  .route-card-list {
    display: grid;
    grid-template-columns: repeat(1, 1fr);
    gap: 20px;
    max-height: calc(100vh - 240px);
    overflow-y: auto;
    padding-right: 10px;
  }
  /* è‡ªå®šä¹‰æ»šåŠ¨æ¡æ ·å¼ */
  .route-card-list::-webkit-scrollbar {
    width: 8px;
  }
  .route-card-list::-webkit-scrollbar-track {
    background: #f1f1f1;
    border-radius: 4px;
  }
  .route-card-list::-webkit-scrollbar-thumb {
    background: #c1c1c1;
    border-radius: 4px;
  }
  .route-card-list::-webkit-scrollbar-thumb:hover {
    background: #a8a8a8;
  }
  .route-card {
    background: #fff;
    border-radius: 8px;
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
    overflow: hidden;
    .card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 20px 40px;
      border-bottom: 1px solid #ebeef5;
      background: #f8f9fa;
      .route-info {
        display: flex;
        // flex-direction: column;
        // justify-content: center;
        // items-align: center;
        gap: 4px;
        .route-code {
          font-size: 12px;
          color: #909399;
          font-family: "Courier New", monospace;
          line-height: 30px;
        }
        .route-name {
          font-size: 18px;
          font-weight: 600;
          color: #303133;
          display: flex;
          align-items: center;
        }
      }
      .route-actions {
        display: flex;
        gap: 8px;
        // .el-button {
        //   color: #409eff;
        // }
      }
    }
    .card-body {
      padding: 16px 40px;
      .route-desc {
        font-size: 14px;
        color: #606266;
        margin-bottom: 12px;
      }
      .route-meta {
        display: flex;
        gap: 24px;
        margin-bottom: 12px;
        padding: 10px 14px;
        background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
        border-radius: 8px;
        border-left: 3px solid #409eff;
        .meta-item {
          display: flex;
          align-items: center;
          gap: 6px;
          font-size: 13px;
          margin-right: 40px;
          .el-icon {
            font-size: 14px;
            color: #409eff;
          }
          .meta-label {
            color: #909399;
            font-weight: 500;
          }
          .meta-value {
            color: #303133;
            font-weight: 600;
          }
        }
      }
      .expand-btn-wrapper {
        display: flex;
        justify-content: center;
        margin-top: 8px;
        .expand-btn {
          padding: 8px 20px;
          border-radius: 20px;
          background: linear-gradient(135deg, #ecf5ff 0%, #d9ecff 100%);
          border: 1px solid #b3d8ff;
          transition: all 0.3s ease;
          .btn-text {
            font-size: 13px;
            font-weight: 500;
            color: #409eff;
            margin-right: 6px;
          }
          .expand-icon {
            font-size: 14px;
            color: #409eff;
            transition: transform 0.3s ease;
          }
          &:hover {
            background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
            border-color: #409eff;
            box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
            .btn-text,
            .expand-icon {
              color: #fff;
            }
          }
          &.expanded {
            background: linear-gradient(135deg, #f0f9eb 0%, #e1f3d8 100%);
            border-color: #a5d69a;
            .btn-text,
            .expand-icon {
              color: #67c23a;
            }
            &:hover {
              background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
              border-color: #67c23a;
              box-shadow: 0 4px 12px rgba(103, 194, 58, 0.3);
              .btn-text,
              .expand-icon {
                color: #fff;
              }
            }
          }
        }
      }
    }
    .process-route {
      padding: 0 20px 20px;
      background: #f5f7fa;
      border-top: 1px solid #ebeef5;
      .process-flow {
        display: flex;
        align-items: flex-start;
        gap: 8px;
        padding: 20px 0;
        overflow-x: auto;
        overflow-y: hidden;
        .process-flow-item {
          display: flex;
          align-items: center;
          gap: 8px;
          .process-node {
            background: #fff;
            border-radius: 12px;
            padding: 16px;
            border: 2px solid #ebeef5;
            cursor: move;
            transition: all 0.3s ease;
            // min-width: 180px;
            // max-width: 220px;
            width: 300px;
            &.expanded {
              width: 400px;
            }
            &:hover {
              box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
              transform: translateY(-2px);
              border-color: #409eff;
            }
            &:active {
              cursor: grabbing;
            }
            .process-node-header {
              display: flex;
              justify-content: space-between;
              align-items: center;
              margin-bottom: 12px;
              .process-number {
                width: 28px;
                height: 28px;
                border-radius: 50%;
                background: #409eff;
                color: #ffffff;
                font-size: 12px;
                font-weight: 600;
                display: flex;
                align-items: center;
                justify-content: center;
              }
              .process-actions {
                display: flex;
                gap: 4px;
              }
            }
            .process-node-body {
              text-align: center;
              margin-bottom: 12px;
              .process-code {
                font-size: 11px;
                color: #909399;
                font-family: "Courier New", monospace;
                margin-bottom: 4px;
              }
              .process-name {
                font-size: 15px;
                font-weight: 600;
                color: #303133;
                margin-bottom: 6px;
              }
              .process-desc {
                font-size: 12px;
                color: #606266;
                overflow: hidden;
                text-overflow: ellipsis;
                display: -webkit-box;
                -webkit-line-clamp: 2;
                -webkit-box-orient: vertical;
              }
            }
            .process-node-footer {
              display: flex;
              justify-content: flex-end;
              align-items: center;
              padding-top: 10px;
              border-top: 1px solid #ebeef5;
            }
            .process-params-section {
              margin-top: 12px;
              padding-top: 12px;
              border-top: 1px solid #ebeef5;
              .params-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin-bottom: 8px;
                font-size: 13px;
                font-weight: 600;
                color: #303133;
              }
              .params-list {
                display: flex;
                flex-direction: column;
                gap: 6px;
                max-height: 200px;
                overflow-y: auto;
                .param-item {
                  display: flex;
                  justify-content: space-between;
                  align-items: center;
                  padding: 6px 8px;
                  background: #fafafa;
                  border-radius: 4px;
                  border-left: 2px solid #409eff;
                  font-size: 12px;
                  .param-info {
                    display: flex;
                    flex-direction: row;
                    align-items: center;
                    gap: 6px;
                    flex: 1;
                    min-width: 0;
                    .param-code {
                      font-size: 11px;
                      color: #e6a23c;
                      font-family: "Courier New", monospace;
                      margin-right: 20px;
                    }
                    .param-name {
                      font-size: 12px;
                      color: #303133;
                      font-weight: 500;
                      margin-right: 20px;
                    }
                    .param-value {
                      font-size: 11px;
                      color: #606266;
                    }
                  }
                  .param-actions {
                    display: flex;
                    gap: 4px;
                    flex-shrink: 0;
                  }
                }
              }
            }
          }
          .flow-arrow {
            display: flex;
            align-items: center;
            color: #c0c4cc;
            font-size: 24px;
            padding: 0 4px;
            .el-icon {
              font-size: 20px;
            }
          }
        }
        .add-process-node {
          display: flex;
          flex-direction: column;
          align-items: center;
          justify-content: center;
          min-width: 100px;
          height: 137px;
          border: 2px dashed #dcdfe6;
          border-radius: 12px;
          background: #fafafa;
          cursor: pointer;
          transition: all 0.3s ease;
          color: #909399;
          // margin-left: 10px;
          .el-icon {
            font-size: 24px;
            margin-bottom: 8px;
          }
          span {
            font-size: 13px;
          }
          &:hover {
            border-color: #409eff;
            background: #ecf5ff;
            color: #409eff;
          }
        }
      }
    }
  }
  // æ‹–拽时的样式
  .process-flow-item.dragging {
    opacity: 0.5;
    transform: scale(0.98);
  }
  // é€‰æ‹©å·¥åºå¯¹è¯æ¡†æ ·å¼
  .process-select-container {
    display: flex;
    gap: 20px;
    height: 450px;
    .process-list-area {
      flex: 1;
      display: flex;
      flex-direction: column;
      .area-title {
        font-size: 14px;
        font-weight: 600;
        color: #303133;
        margin-bottom: 12px;
        padding-bottom: 8px;
        border-bottom: 1px solid #ebeef5;
      }
      .search-box {
        margin-bottom: 12px;
        .el-input {
          width: 100%;
        }
      }
    }
    .process-detail-area {
      width: 380px;
      display: flex;
      flex-direction: column;
      background: #f5f7fa;
      border-radius: 8px;
      padding: 16px;
      .area-title {
        font-size: 14px;
        font-weight: 600;
        color: #303133;
        margin-bottom: 16px;
        padding-bottom: 8px;
        border-bottom: 1px solid #ebeef5;
      }
      .process-detail-form {
        .el-form-item {
          margin-bottom: 12px;
          .el-form-item__label {
            color: #606266;
            font-weight: 500;
          }
        }
        .detail-text {
          color: #303133;
          font-weight: 500;
        }
      }
    }
  }
  // é€‰æ‹©å‚数对话框样式
  .param-select-container {
    display: flex;
    gap: 20px;
    height: 450px;
    .param-list-area {
      // flex: 1;
      width: 380px;
      display: flex;
      flex-direction: column;
      .area-title {
        font-size: 14px;
        font-weight: 600;
        color: #303133;
        margin-bottom: 12px;
        padding-bottom: 8px;
        border-bottom: 1px solid #ebeef5;
      }
      .search-box {
        margin-bottom: 12px;
        .el-input {
          width: 100%;
        }
      }
    }
    .param-detail-area {
      // width: 380px;
      flex: 1;
      display: flex;
      flex-direction: column;
      background: #f5f7fa;
      border-radius: 8px;
      padding: 16px;
      .area-title {
        font-size: 14px;
        font-weight: 600;
        color: #303133;
        margin-bottom: 16px;
        padding-bottom: 8px;
        border-bottom: 1px solid #ebeef5;
      }
      .param-detail-form {
        .el-form-item {
          margin-bottom: 12px;
          .el-form-item__label {
            color: #606266;
            font-weight: 500;
          }
        }
        .detail-text {
          color: #303133;
          font-weight: 500;
        }
      }
    }
  }
  // åˆ†é¡µæŽ§ä»¶æ ·å¼
  .pagination-container {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    display: flex;
    justify-content: flex-end;
    padding: 16px 20px;
    background-color: #fff !important;
    border-top: 1px solid #ebeef5;
    box-shadow: 0 -2px 12px 0 rgba(0, 0, 0, 0.1);
    z-index: 100;
    .el-pagination {
      .el-pagination__sizes {
        margin-right: 16px;
      }
      .el-pagination__jump {
        margin-left: 16px;
      }
      .el-pagination__total {
        color: #606266;
        font-size: 14px;
      }
      .el-pagination__button {
        border-radius: 4px;
        transition: all 0.3s ease;
        &:hover:not(:disabled) {
          color: #409eff;
          border-color: #409eff;
        }
      }
      .el-pagination__button--active {
        background-color: #409eff;
        border-color: #409eff;
        color: #fff;
      }
    }
  }
</style>
src/views/productionManagement/processRoute/processRouteItem/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1024 @@
<template>
  <div class="app-container">
    <PageHeader content="工艺路线项目" />
    <!-- å·¥è‰ºè·¯çº¿ä¿¡æ¯å±•示 -->
    <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">
            <span class="info-label">工艺路线编号</span>
          </div>
          <div class="info-value-wrapper">
            <span class="info-value">{{ routeInfo.processRouteCode }}</span>
          </div>
        </div>
        <div class="info-item">
          <div class="info-label-wrapper">
            <span class="info-label">产品名称</span>
          </div>
          <div class="info-value-wrapper">
            <span class="info-value">{{ routeInfo.productName || '-' }}</span>
          </div>
        </div>
        <div class="info-item">
          <div class="info-label-wrapper">
            <span class="info-label">规格名称</span>
          </div>
          <div class="info-value-wrapper">
            <span class="info-value">{{ routeInfo.model || '-' }}</span>
          </div>
        </div>
        <div class="info-item">
          <div class="info-label-wrapper">
            <span class="info-label">BOM编号</span>
          </div>
          <div class="info-value-wrapper">
            <span class="info-value">{{ routeInfo.bomNo || '-' }}</span>
          </div>
        </div>
        <div class="info-item full-width"
             v-if="routeInfo.description">
          <div class="info-label-wrapper">
            <span class="info-label">描述</span>
          </div>
          <div class="info-value-wrapper">
            <span class="info-value">{{ routeInfo.description }}</span>
          </div>
        </div>
      </div>
    </el-card>
    <!-- bom展示 -->
    <!-- è¡¨æ ¼è§†å›¾ -->
    <div v-if="viewMode === 'table'"
         class="section-header">
      <div class="section-title">工艺路线项目列表</div>
      <div class="section-actions">
        <el-button icon="Grid"
                   @click="toggleView"
                   style="margin-right: 10px;">
          å¡ç‰‡è§†å›¾
        </el-button>
        <el-button type="primary"
                   @click="handleAdd">新增</el-button>
      </div>
    </div>
    <el-table v-if="viewMode === 'table'"
              ref="tableRef"
              v-loading="tableLoading"
              border
              :data="tableData"
              :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
              row-key="id"
              tooltip-effect="dark"
              class="lims-table">
      <el-table-column align="center"
                       label="序号"
                       width="60"
                       type="index" />
      <el-table-column label="工序名称"
                       prop="processId"
                       width="200">
        <template #default="scope">
          {{ getProcessName(scope.row.processId) || '-' }}
        </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="参数列表"
                       min-width="160">
        <template #default="scope">
          <el-button type="primary"
                     link
                     size="small"
                     @click="handleViewParams(scope.row)">参数列表</el-button>
        </template>
      </el-table-column>
      <el-table-column label="单位"
                       prop="unit"
                       width="100" />
      <el-table-column label="是否质检"
                       prop="isQuality"
                       width="100">
        <template #default="scope">
          {{scope.row.isQuality ? "是" : "否"}}
        </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)"
                     :disabled="scope.row.isComplete">编辑</el-button>
          <!-- <el-button type="info"
                     link
                     size="small"
                     @click="handleViewParams(scope.row)">参数列表</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>
    <!-- å¡ç‰‡è§†å›¾ -->
    <template v-else>
      <div class="section-header">
        <div class="section-title">工艺路线项目列表</div>
        <div class="section-actions">
          <el-button icon="Menu"
                     @click="toggleView"
                     style="margin-right: 10px;">
            è¡¨æ ¼è§†å›¾
          </el-button>
          <el-button type="primary"
                     @click="handleAdd">新增</el-button>
        </div>
      </div>
      <div v-loading="tableLoading"
           class="card-container">
        <div ref="cardsContainer"
             class="cards-wrapper">
          <div v-for="(item, index) in tableData"
               :key="item.id || index"
               class="process-card"
               :data-index="index">
            <!-- åºå·åœ†åœˆ -->
            <div class="card-header">
              <div class="card-number">{{ index + 1 }}</div>
              <div class="card-process-name">{{ getProcessName(item.processId) || '-' }}</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>
                <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)"
                         :disabled="item.isComplete">编辑</el-button>
              <el-button type="info"
                         link
                         size="small"
                         @click="handleViewParams(item)">参数列表</el-button>
              <el-button type="danger"
                         link
                         size="small"
                         @click="handleDelete(item)"
                         :disabled="item.isComplete">删除</el-button>
            </div>
          </div>
        </div>
      </div>
    </template>
    <!-- æ–°å¢ž/编辑弹窗 -->
    <el-dialog v-model="dialogVisible"
               :title="operationType === 'add' ? '新增工艺路线项目' : '编辑工艺路线项目'"
               width="500px"
               @close="closeDialog">
      <el-form ref="formRef"
               :model="form"
               :rules="rules"
               label-width="120px">
        <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-form-item label="是否质检"
                      prop="isQuality">
          <el-switch v-model="form.isQuality"
                     :active-value="true"
                     inactive-value="false" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="closeDialog">取消</el-button>
        <el-button type="primary"
                   @click="handleSubmit"
                   :loading="submitLoading">确定</el-button>
      </template>
    </el-dialog>
    <!-- äº§å“é€‰æ‹©å¯¹è¯æ¡† -->
    <ProductSelectDialog v-model="showProductSelectDialog"
                         @confirm="handleProductSelect"
                         single />
    <!-- å‚数列表对话框 -->
    <ProcessParamListDialog v-model="showParamListDialog"
                            :title="`${currentProcess ? getProcessName(currentProcess.processId) : ''} - å‚数列表`"
                            :route-id="routeId"
                            :editable="false"
                            :process="currentProcess"
                            :param-list="paramList"
                            @refresh="refreshParamList" />
  </div>
</template>
<script setup>
  import {
    ref,
    computed,
    getCurrentInstance,
    onMounted,
    onUnmounted,
    nextTick,
  } from "vue";
  import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
  import ProcessParamListDialog from "@/components/ProcessParamListDialog.vue";
  import {
    findProcessRouteItemList,
    addOrUpdateProcessRouteItem,
    sortProcessRouteItem,
    batchDeleteProcessRouteItem,
    getProcessParamList,
  } from "@/api/productionManagement/processRouteItem.js";
  import {
    findProductProcessRouteItemList,
    deleteRouteItem,
    addRouteItem,
    addOrUpdateProductProcessRouteItem,
    sortRouteItem,
  } from "@/api/productionManagement/productProcessRoute.js";
  import { processList } from "@/api/productionManagement/productionProcess.js";
  import { useRoute } from "vue-router";
  import { ElMessageBox, ElMessage } from "element-plus";
  import Sortable from "sortablejs";
  const route = useRoute();
  const { proxy } = getCurrentInstance() || {};
  const routeId = computed(() => route.query.id);
  const orderId = computed(() => route.query.orderId);
  const pageType = computed(() => route.query.type);
  const tableLoading = ref(false);
  const tableData = ref([]);
  const dialogVisible = ref(false);
  const operationType = ref("add"); // add | edit
  const formRef = ref(null);
  const submitLoading = ref(false);
  const cardsContainer = ref(null);
  const tableRef = ref(null);
  const viewMode = ref("table"); // table | card
  const routeInfo = ref({
    processRouteCode: "",
    productName: "",
    model: "",
    bomNo: "",
    bomId: null,
    description: "",
  });
  const processOptions = ref([]);
  const showProductSelectDialog = ref(false);
  const showParamListDialog = ref(false);
  const currentProcess = ref(null);
  const paramList = ref([]);
  let tableSortable = null;
  let cardSortable = null;
  // åˆ‡æ¢è§†å›¾
  const toggleView = () => {
    viewMode.value = viewMode.value === "table" ? "card" : "table";
    // åˆ‡æ¢è§†å›¾åŽé‡æ–°åˆå§‹åŒ–拖拽排序
    nextTick(() => {
      initSortable();
    });
  };
  const form = ref({
    id: undefined,
    routeId: routeId.value,
    processId: undefined,
    productModelId: undefined,
    productName: "",
    model: "",
    unit: "",
    isQuality: false,
  });
  const rules = {
    processId: [{ 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 getList = () => {
    tableLoading.value = true;
    const listPromise =
      pageType.value === "order"
        ? findProductProcessRouteItemList({ orderId: orderId.value })
        : findProcessRouteItemList({ routeId: routeId.value });
    listPromise
      .then(res => {
        tableData.value = res.data || [];
        tableLoading.value = false;
        // åˆ—表加载完成后初始化拖拽排序
        nextTick(() => {
          initSortable();
        });
      })
      .catch(err => {
        tableLoading.value = false;
        console.error("获取列表失败:", err);
        proxy?.$modal?.msgError("获取列表失败");
      });
  };
  // èŽ·å–å·¥åºåˆ—è¡¨
  const getProcessList = () => {
    processList({})
      .then(res => {
        processOptions.value = res.data || [];
      })
      .catch(err => {
        console.error("获取工序失败:", err);
      });
  };
  // èŽ·å–å·¥è‰ºè·¯çº¿è¯¦æƒ…ï¼ˆä»Žè·¯ç”±å‚æ•°èŽ·å–ï¼‰
  const getRouteInfo = () => {
    routeInfo.value = {
      processRouteCode: route.query.processRouteCode || "",
      productName: route.query.productName || "",
      model: route.query.model || "",
      bomNo: route.query.bomNo || "",
      bomId: route.query.bomId || null,
      description: route.query.description || "",
    };
  };
  // æ–°å¢ž
  const handleAdd = () => {
    operationType.value = "add";
    resetForm();
    dialogVisible.value = true;
  };
  // ç¼–辑
  const handleEdit = row => {
    operationType.value = "edit";
    form.value = {
      id: row.id,
      routeId: routeId.value,
      processId: row.processId,
      productModelId: row.productModelId,
      productName: row.productName || "",
      model: row.model || "",
      unit: row.unit || "",
      isQuality: row.isQuality,
    };
    dialogVisible.value = true;
  };
  // åˆ é™¤
  const handleDelete = row => {
    ElMessageBox.confirm("确认删除该工艺路线项目?", "提示", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        // ç”Ÿäº§è®¢å•下使用 productProcessRoute çš„删除接口(路由后拼接 id),其它情况使用工艺路线项目批量删除接口
        const deletePromise =
          pageType.value === "order"
            ? deleteRouteItem(row.id)
            : batchDeleteProcessRouteItem([row.id]);
        deletePromise
          .then(() => {
            proxy?.$modal?.msgSuccess("删除成功");
            getList();
          })
          .catch(() => {
            proxy?.$modal?.msgError("删除失败");
          });
      })
      .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 => {
      if (valid) {
        submitLoading.value = true;
        if (operationType.value === "add") {
          // æ–°å¢žï¼šä¼ å•个对象,包含dragSort字段
          // dragSort = å½“前列表长度 + 1,表示新增记录排在最后
          const dragSort = tableData.value.length + 1;
          const isOrderPage = pageType.value === "order";
          const addPromise = isOrderPage
            ? addRouteItem({
                productOrderId: orderId.value,
                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,
              });
          addPromise
            .then(() => {
              proxy?.$modal?.msgSuccess("新增成功");
              closeDialog();
              getList();
            })
            .catch(() => {
              proxy?.$modal?.msgError("新增失败");
            })
            .finally(() => {
              submitLoading.value = false;
            });
        } else {
          // ç¼–辑:生产订单下使用 productProcessRoute/updateRouteItem,其它情况使用工艺路线项目更新接口
          const isOrderPage = pageType.value === "order";
          const updatePromise = isOrderPage
            ? addOrUpdateProductProcessRouteItem({
                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
            .then(() => {
              proxy?.$modal?.msgSuccess("修改成功");
              closeDialog();
              getList();
            })
            .catch(() => {
              proxy?.$modal?.msgError("修改失败");
            })
            .finally(() => {
              submitLoading.value = false;
            });
        }
      }
    });
  };
  // é‡ç½®è¡¨å•
  const resetForm = () => {
    form.value = {
      id: undefined,
      routeId: routeId.value,
      processId: undefined,
      productModelId: undefined,
      productName: "",
      model: "",
      unit: "",
    };
    formRef.value?.resetFields();
  };
  // å…³é—­å¼¹çª—
  const closeDialog = () => {
    dialogVisible.value = false;
    resetForm();
  };
  // åˆå§‹åŒ–拖拽排序
  const initSortable = () => {
    destroySortable();
    if (viewMode.value === "table") {
      // è¡¨æ ¼è§†å›¾çš„æ‹–拽排序
      if (!tableRef.value) return;
      const tbody =
        tableRef.value.$el.querySelector(".el-table__body tbody") ||
        tableRef.value.$el.querySelector(
          ".el-table__body-wrapper > table > tbody"
        );
      if (!tbody) return;
      tableSortable = new Sortable(tbody, {
        animation: 150,
        ghostClass: "sortable-ghost",
        handle: ".el-table__row",
        filter: ".el-button, .el-select",
        onEnd: evt => {
          if (evt.oldIndex === evt.newIndex || !tableData.value[evt.oldIndex])
            return;
          // é‡æ–°æŽ’序数组
          const moveItem = tableData.value.splice(evt.oldIndex, 1)[0];
          tableData.value.splice(evt.newIndex, 0, moveItem);
          // è®¡ç®—新的序号(dragSort从1开始)
          const newIndex = evt.newIndex;
          const dragSort = newIndex + 1;
          // è°ƒç”¨æŽ’序接口
          if (moveItem.id) {
            const isOrderPage = pageType.value === "order";
            const sortPromise = isOrderPage
              ? sortRouteItem({
                  id: moveItem.id,
                  dragSort: dragSort,
                })
              : sortProcessRouteItem({
                  id: moveItem.id,
                  dragSort: dragSort,
                });
            sortPromise
              .then(() => {
                // æ›´æ–°æ‰€æœ‰è¡Œçš„dragSort
                tableData.value.forEach((item, index) => {
                  if (item.id) {
                    item.dragSort = index + 1;
                  }
                });
                proxy?.$modal?.msgSuccess("排序成功");
              })
              .catch(err => {
                // æŽ’序失败,恢复原数组
                tableData.value.splice(newIndex, 1);
                tableData.value.splice(evt.oldIndex, 0, moveItem);
                proxy?.$modal?.msgError("排序失败");
                console.error("排序失败:", err);
              });
          }
        },
      });
    } else {
      // å¡ç‰‡è§†å›¾çš„æ‹–拽排序
      if (!cardsContainer.value) return;
      cardSortable = new Sortable(cardsContainer.value, {
        animation: 150,
        ghostClass: "sortable-ghost",
        handle: ".process-card",
        filter: ".el-button",
        onEnd: evt => {
          if (evt.oldIndex === evt.newIndex || !tableData.value[evt.oldIndex])
            return;
          // é‡æ–°æŽ’序数组
          const moveItem = tableData.value.splice(evt.oldIndex, 1)[0];
          tableData.value.splice(evt.newIndex, 0, moveItem);
          // è®¡ç®—新的序号(dragSort从1开始)
          const newIndex = evt.newIndex;
          const dragSort = newIndex + 1;
          // è°ƒç”¨æŽ’序接口
          if (moveItem.id) {
            const isOrderPage = pageType.value === "order";
            const sortPromise = isOrderPage
              ? sortRouteItem({
                  id: moveItem.id,
                  dragSort: dragSort,
                })
              : sortProcessRouteItem({
                  id: moveItem.id,
                  dragSort: dragSort,
                });
            sortPromise
              .then(() => {
                // æ›´æ–°æ‰€æœ‰è¡Œçš„dragSort
                tableData.value.forEach((item, index) => {
                  if (item.id) {
                    item.dragSort = index + 1;
                  }
                });
                proxy?.$modal?.msgSuccess("排序成功");
              })
              .catch(err => {
                // æŽ’序失败,恢复原数组
                tableData.value.splice(newIndex, 1);
                tableData.value.splice(evt.oldIndex, 0, moveItem);
                proxy?.$modal?.msgError("排序失败");
                console.error("排序失败:", err);
              });
          }
        },
      });
    }
  };
  // é”€æ¯æ‹–拽排序
  const destroySortable = () => {
    if (tableSortable) {
      tableSortable.destroy();
      tableSortable = null;
    }
    if (cardSortable) {
      cardSortable.destroy();
      cardSortable = null;
    }
  };
  onMounted(() => {
    getRouteInfo();
    getList();
    getProcessList();
  });
  // æŸ¥çœ‹å‚数列表
  const handleViewParams = process => {
    currentProcess.value = process;
    // è°ƒç”¨API获取参数列表
    getProcessParamList({
      routeItemId: process.id,
      pageNum: 1,
      pageSize: 1000,
    })
      .then(res => {
        if (res.code === 200) {
          paramList.value = res.data?.records || [];
        } else {
          ElMessage.error(res.msg || "获取参数列表失败");
          paramList.value = [];
        }
        showParamListDialog.value = true;
      })
      .catch(err => {
        console.error("获取参数列表失败:", err);
        ElMessage.error("获取参数列表失败");
        paramList.value = [];
        showParamListDialog.value = true;
      });
  };
  // åˆ·æ–°å‚数列表
  const refreshParamList = () => {
    if (!currentProcess.value) return;
    // é‡æ–°è°ƒç”¨API获取参数列表
    getProcessParamList({
      routeItemId: currentProcess.value.id,
      pageNum: 1,
      pageSize: 1000,
    })
      .then(res => {
        if (res.code === 200) {
          paramList.value = res.data?.records || [];
        } else {
          ElMessage.error(res.msg || "获取参数列表失败");
          paramList.value = [];
        }
      })
      .catch(err => {
        console.error("获取参数列表失败:", err);
        ElMessage.error("获取参数列表失败");
        paramList.value = [];
      });
  };
  onUnmounted(() => {
    destroySortable();
  });
</script>
<style scoped>
  .card-container {
    padding: 20px 0;
  }
  .cards-wrapper {
    display: flex;
    gap: 16px;
    overflow-x: auto;
    padding: 10px 0;
    min-height: 200px;
  }
  .cards-wrapper::-webkit-scrollbar {
    height: 8px;
  }
  .cards-wrapper::-webkit-scrollbar-track {
    background: #f1f1f1;
    border-radius: 4px;
  }
  .cards-wrapper::-webkit-scrollbar-thumb {
    background: #c1c1c1;
    border-radius: 4px;
  }
  .cards-wrapper::-webkit-scrollbar-thumb:hover {
    background: #a8a8a8;
  }
  .process-card {
    flex-shrink: 0;
    width: 220px;
    background: #fff;
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    padding: 16px;
    display: flex;
    flex-direction: column;
    cursor: move;
    transition: all 0.3s;
  }
  .process-card:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    transform: translateY(-2px);
  }
  .card-header {
    text-align: center;
    margin-bottom: 12px;
  }
  .card-number {
    width: 36px;
    height: 36px;
    line-height: 36px;
    border-radius: 50%;
    background: #409eff;
    color: #fff;
    font-weight: bold;
    font-size: 16px;
    margin: 0 auto 8px;
  }
  .card-process-name {
    font-size: 14px;
    color: #333;
    font-weight: 500;
    word-break: break-all;
  }
  .card-content {
    flex: 1;
    margin-bottom: 12px;
    min-height: 60px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .product-info {
    font-size: 13px;
    color: #666;
    text-align: center;
    width: 100%;
  }
  .product-info.empty {
    color: #999;
    text-align: center;
    padding: 20px 0;
  }
  .product-name {
    margin-bottom: 6px;
    word-break: break-all;
    line-height: 1.5;
    text-align: center;
  }
  .product-model {
    color: #909399;
    font-size: 12px;
    word-break: break-all;
    line-height: 1.5;
    text-align: center;
  }
  .product-unit {
    margin-left: 4px;
    color: #409eff;
  }
  .product-tag {
    margin: 10px 0;
  }
  .card-footer {
    display: flex;
    justify-content: space-around;
    padding-top: 12px;
    border-top: 1px solid #f0f0f0;
  }
  .card-footer .el-button {
    padding: 0;
    font-size: 12px;
  }
  :deep(.sortable-ghost) {
    opacity: 0.5;
    background-color: #f5f7fa !important;
  }
  :deep(.sortable-drag) {
    opacity: 0.8;
  }
  /* è¡¨æ ¼è§†å›¾æ ·å¼ */
  :deep(.el-table__row) {
    transition: background-color 0.2s;
    cursor: move;
  }
  :deep(.el-table__row:hover) {
    background-color: #f9fafc !important;
  }
  /* åŒºåŸŸæ ‡é¢˜æ ·å¼ */
  .section-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 12px;
  }
  .section-title {
    font-size: 16px;
    font-weight: 600;
    color: #303133;
    padding-left: 12px;
    position: relative;
    margin-bottom: 0;
  }
  .section-title::before {
    content: "";
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
    width: 3px;
    height: 16px;
    background: #409eff;
    border-radius: 2px;
  }
  .section-actions {
    display: flex;
    align-items: center;
  }
  /* å·¥è‰ºè·¯çº¿ä¿¡æ¯å¡ç‰‡æ ·å¼ */
  .route-info-card {
    margin-bottom: 20px;
    border: 1px solid #e4e7ed;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
    border-radius: 8px;
    overflow: hidden;
  }
  .route-info {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 16px;
    padding: 4px;
  }
  .info-item {
    display: flex;
    flex-direction: column;
    background: #ffffff;
    border-radius: 6px;
    padding: 14px 16px;
    border: 1px solid #f0f2f5;
    transition: all 0.3s ease;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
  }
  .info-item:hover {
    border-color: #409eff;
    box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
    transform: translateY(-1px);
  }
  .info-item.full-width {
    grid-column: 1 / -1;
  }
  .info-label-wrapper {
    margin-bottom: 8px;
  }
  .info-label {
    display: inline-block;
    color: #909399;
    font-size: 12px;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    padding: 2px 0;
    position: relative;
  }
  .info-label::after {
    content: "";
    position: absolute;
    left: 0;
    bottom: 0;
    width: 20px;
    height: 2px;
    background: linear-gradient(90deg, #409eff, transparent);
    border-radius: 1px;
  }
  .info-value-wrapper {
    flex: 1;
  }
  .info-value {
    display: block;
    color: #303133;
    font-size: 15px;
    font-weight: 500;
    line-height: 1.5;
    word-break: break-all;
  }
</style>
src/views/productionPlan/productionPlan/index.vue
@@ -117,6 +117,21 @@
                          value-format="YYYY-MM-DD"
                          style="width: 100%" />
        </el-form-item>
        <el-form-item label="强度"
                      v-if="mergeForm.productName === '砌块'">
          <div v-if="strengthError"
               class="strength-error"
               style="color: red; margin-bottom: 8px;">{{ strengthError }}</div>
          <el-select v-model="mergeForm.strength"
                     placeholder="请选择强度"
                     style="width: 100%"
                     required>
            <el-option v-for="item in block_strength"
                       :key="item.id"
                       :label="item.label"
                       :value="item.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="生产方数">
          <el-input-number v-model="mergeForm.totalAssignedQuantity"
                           :min="0"
@@ -301,14 +316,15 @@
                          placeholder="请选择计划结束日期" />
        </el-form-item>
        <el-form-item label="强度"
                      prop="strength">
                      prop="strength"
                      v-if="form.productName === '砌块'">
          <el-select v-model="form.strength"
                     placeholder="请选择强度"
                     style="width: 100%">
            <el-option label="A3.5"
                       value="A3.5" />
            <el-option label="A5.0"
                       value="A5.0" />
            <el-option v-for="item in block_strength"
                       :key="item.id"
                       :label="item.label"
                       :value="item.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="备注 1"
@@ -334,11 +350,12 @@
</template>
<script setup>
  import { onMounted, ref, reactive, getCurrentInstance } from "vue";
  import { onMounted, ref, reactive, getCurrentInstance, toRefs } from "vue";
  import { ElMessage } from "element-plus";
  import dayjs from "dayjs";
  import ImportDialog from "@/components/Dialog/ImportDialog.vue";
  import { getToken } from "@/utils/auth";
  import { useDict } from "@/utils/dict";
  import {
    productionPlanListPage,
    loadProdData,
@@ -490,6 +507,11 @@
    {
      label: "强度",
      prop: "strength",
      formatData: cell => {
        if (!cell) return "";
        const strengthItem = block_strength.value.find(item => item.id === cell);
        return strengthItem ? strengthItem.label : cell;
      },
    },
    {
@@ -546,6 +568,7 @@
          clickFun: row => {
            // å•独下发操作
            // è®¾ç½®è¡¨å•数据
            strengthError.value = "";
            mergeForm.ids = [row.id];
            mergeForm.materialCode = row.materialCode;
            mergeForm.productName = row.productName || "";
@@ -556,6 +579,7 @@
            mergeForm.totalAssignedQuantity =
              (Number(row.volume) - Number(row.assignedQuantity)).toFixed(4) || 0;
            mergeForm.planCompleteTime = row.planCompleteTime || "";
            mergeForm.strength = row.strength || "";
            sumAssignedQuantity.value = mergeForm.totalAssignedQuantity;
            // æ‰“开弹窗
            isShowNewModal.value = true;
@@ -597,6 +621,7 @@
    height: 0,
    totalAssignedQuantity: 0,
    planCompleteTime: "",
    strength: "",
  });
  // è¿½è¸ªè¿›åº¦å¼¹çª—控制
@@ -625,6 +650,8 @@
  const productOptions = ref([]);
  const specificationOptions = ref([]);
  const formRef = ref(null);
  // èŽ·å–å¼ºåº¦å­—å…¸
  const { block_strength } = useDict("block_strength");
  const form = reactive({
    id: undefined,
    applyNo: "",
@@ -656,6 +683,19 @@
    volume: [{ required: true, message: "请输入方数", trigger: "blur" }],
    productMaterialId: [
      { required: true, message: "请选择产品", trigger: "change" },
    ],
    strength: [
      {
        validator: (rule, value, callback) => {
          if (form.productName === "砌块" && !value) {
            callback(new Error("砌块产品的强度为必填项"));
          } else {
            callback();
          }
        },
        trigger: ["blur", "change"],
        required: false,
      },
    ],
  });
@@ -709,6 +749,26 @@
  const handleProductChange = value => {
    form.productMaterialSkuId = undefined;
    // æŸ¥æ‰¾é€‰ä¸­çš„产品名称
    const findProductName = (options, value) => {
      for (const option of options) {
        if (option.value === value) {
          return option.label;
        }
        if (option.children) {
          const found = findProductName(option.children, value);
          if (found) {
            return found;
          }
        }
      }
      return "";
    };
    form.productName = findProductName(productOptions.value, value);
    // è§¦å‘强度字段验证
    if (formRef.value) {
      formRef.value.validateField("strength");
    }
    fetchSpecificationOptions(value);
  };
@@ -931,6 +991,9 @@
      .catch(() => {});
  };
  const sumAssignedQuantity = ref(0);
  // å“åº”式数据
  const strengthError = ref("");
  // å¤„理合并下发按钮点击
  const handleMerge = () => {
    if (selectedRows.value.length === 0) {
@@ -938,6 +1001,26 @@
      return;
    }
    console.log(selectedRows.value);
    // æ£€æŸ¥å¼ºåº¦ä¸€è‡´æ€§
    const firstRow = selectedRows.value[0];
    const productName = firstRow.productName || "";
    let strengthConsistent = true;
    let firstStrength = firstRow.strength || "";
    strengthError.value = "";
    if (productName === "砌块") {
      for (const row of selectedRows.value) {
        if (row.strength !== firstStrength) {
          strengthConsistent = false;
          break;
        }
      }
      if (!strengthConsistent) {
        strengthError.value = "选择的砌块强度不一致,请重新选择";
      }
    }
    // è®¡ç®—总制造数量
    const totalAssignedQuantity = selectedRows.value.reduce((sum, row) => {
      return (
@@ -950,15 +1033,15 @@
    sumAssignedQuantity.value = totalAssignedQuantity;
    console.log(totalAssignedQuantity);
    // è®¾ç½®è¡¨å•数据
    const firstRow = selectedRows.value[0];
    mergeForm.materialCode = selectedserialNo.value;
    mergeForm.productName = firstRow.productName || "";
    mergeForm.productName = productName;
    mergeForm.model = firstRow.model || "";
    mergeForm.length = firstRow.length || 0;
    mergeForm.width = firstRow.width || 0;
    mergeForm.height = firstRow.height || 0;
    mergeForm.totalAssignedQuantity = totalAssignedQuantity;
    mergeForm.planCompleteTime = firstRow.planCompleteTime || "";
    mergeForm.strength = firstStrength;
    mergeForm.ids = selectedRows.value.map(row => row.id);
    // æ‰“开弹窗
@@ -971,6 +1054,11 @@
      ElMessage.warning("请输入生产方数");
      return;
    }
    // éªŒè¯ç Œå—产品的强度
    if (mergeForm.productName === "砌块" && !mergeForm.strength) {
      ElMessage.error("砌块产品的强度为必填项");
      return;
    }
    console.log(sumAssignedQuantity.value, "sumAssignedQuantity");
    // è®¡ç®—当前选中行的总方数
    const totalVolume = selectedRows.value.reduce((sum, row) => {
src/views/qualityManagement/rawMaterialInspection/components/formDia.vue
@@ -77,6 +77,13 @@
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="数量:"
                          prop="quantity">
              <el-input type="number"
                        v-model="form.quantity" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="单位:"
                          prop="unit">
              <el-input v-model="form.unit"
@@ -249,7 +256,7 @@
        { required: false, message: "请选择指标", trigger: "change" },
      ],
      unit: [{ required: false, message: "请输入", trigger: "blur" }],
      quantity: [{ required: true, message: "请输入", trigger: "blur" }],
      // quantity: [{ required: true, message: "请输入", trigger: "blur" }],
      checkCompany: [{ required: false, message: "请输入", trigger: "blur" }],
      checkResult: [
        { required: false, message: "请选择检测结果", trigger: "change" },
@@ -353,7 +360,7 @@
          modelOptions.value = res || [];
          // åŒæ­¥å›žå¡« model / unit(有些接口返回的 row é‡Œå¯èƒ½æ²¡å¸¦å…¨ï¼‰
          if (form.value.productModelId) {
            handleChangeModel(form.value.productModelId);
            // handleChangeModel(form.value.productModelId);
          }
        } catch (e) {
          console.error("加载规格型号失败", e);
src/views/reportAnalysis/productionStatistics/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1003 @@
<template>
  <div class="dashboard-container">
    <div class="data-dashboard">
      <!-- é¡¶éƒ¨æ ‡é¢˜æ  -->
      <!-- <div class="dashboard-header">
      <div class="factory-name">生产统计看板</div>
    </div> -->
      <!-- ç­›é€‰åŒºåŸŸ -->
      <div class="filter-area">
        <div class="filter-section">
          <span class="filter-label">时间维度:</span>
          <el-radio-group v-model="dateType"
                          @change="handleDateTypeChange"
                          class="radio-group">
            <el-radio-button label="month">月度</el-radio-button>
            <el-radio-button label="year">年度</el-radio-button>
          </el-radio-group>
        </div>
        <div class="filter-section">
          <span class="filter-label">产品类型:</span>
          <el-radio-group v-model="productType"
                          @change="handleProductTypeChange"
                          class="radio-group">
            <el-radio-button label="block">砌块</el-radio-button>
            <el-radio-button label="plate">板材</el-radio-button>
          </el-radio-group>
        </div>
      </div>
      <!-- ä¸»è¦å†…容区域 -->
      <div class="dashboard-content">
        <!-- ç¬¬ä¸€è¡Œ -->
        <div class="row row-1">
          <div class="panel-card card-1">
            <div class="panel-title">产量指标</div>
            <div class="chart-container">
              <div ref="productionChart"
                   style="width: 100%; height: 100%"></div>
            </div>
          </div>
          <div class="panel-card card-2">
            <div class="panel-title">固废处理量</div>
            <div class="chart-container">
              <div ref="solidWasteChart"
                   style="width: 100%; height: 100%"></div>
            </div>
          </div>
          <div class="panel-card card-3">
            <div class="panel-title">综合统计</div>
            <div class="stats-grid">
              <div class="stat-item">
                <div class="stat-label">总产能</div>
                <div class="stat-value production-color">{{ totalProduction }}</div>
                <div class="stat-unit">立方米</div>
              </div>
              <div class="stat-item">
                <div class="stat-label">总固废处理</div>
                <div class="stat-value waste-color">{{ totalSolidWaste }}</div>
                <div class="stat-unit">吨</div>
              </div>
              <div class="stat-item">
                <div class="stat-label">平均单耗</div>
                <div class="stat-value consumption-color">{{ averageUnitConsumption }}</div>
                <div class="stat-unit">吨/立方米</div>
              </div>
              <div class="stat-item">
                <div class="stat-label">总能耗</div>
                <div class="stat-value energy-color">{{ totalEnergy }}</div>
                <div class="stat-unit">kWh</div>
              </div>
            </div>
          </div>
        </div>
        <!-- ç¬¬äºŒè¡Œ -->
        <div class="row row-2">
          <div class="panel-card card-4">
            <div class="panel-title">生产成本单耗</div>
            <div class="chart-container">
              <div ref="costChart"
                   style="width: 100%; height: 100%"></div>
            </div>
          </div>
          <div class="panel-card card-5">
            <div class="panel-title">生产能耗数据</div>
            <div class="chart-container">
              <div ref="energyChart"
                   style="width: 100%; height: 100%"></div>
            </div>
          </div>
        </div>
        <!-- ç¬¬ä¸‰è¡Œ -->
        <div class="row row-3">
          <div class="panel-card card-6">
            <div class="panel-title">单耗数据明细</div>
            <div class="table-container">
              <el-table :data="costTableData"
                        style="width: 100%">
                <el-table-column prop="material"
                                 label="物料类型"
                                 width="120"
                                 align="center">
                  <template #default="scope">
                    <el-tag :type="getMaterialTypeType(scope.row.material)">
                      {{ scope.row.material }}
                    </el-tag>
                  </template>
                </el-table-column>
                <el-table-column prop="unit"
                                 label="单位"
                                 width="100" />
                <el-table-column prop="monthlyConsumption"
                                 label="月度累计用量"
                                 align="right">
                  <template #default="scope">
                    <span class="data-value">{{ scope.row.monthlyConsumption }}</span>
                  </template>
                </el-table-column>
                <el-table-column prop="monthlyProduction"
                                 label="月度累计产量"
                                 align="right">
                  <template #default="scope">
                    <span class="data-value">{{ scope.row.monthlyProduction }}</span>
                  </template>
                </el-table-column>
                <el-table-column prop="monthlyUnitConsumption"
                                 label="月度累计单耗"
                                 align="right">
                  <template #default="scope">
                    <span class="data-value">{{ scope.row.monthlyUnitConsumption }}</span>
                  </template>
                </el-table-column>
                <el-table-column prop="yearlyConsumption"
                                 label="年度累计用量"
                                 align="right">
                  <template #default="scope">
                    <span class="data-value">{{ scope.row.yearlyConsumption }}</span>
                  </template>
                </el-table-column>
                <el-table-column prop="yearlyProduction"
                                 label="年度累计产量"
                                 align="right">
                  <template #default="scope">
                    <span class="data-value">{{ scope.row.yearlyProduction }}</span>
                  </template>
                </el-table-column>
                <el-table-column prop="yearlyUnitConsumption"
                                 label="年度累计单耗"
                                 align="right">
                  <template #default="scope">
                    <span class="data-value">{{ scope.row.yearlyUnitConsumption }}</span>
                  </template>
                </el-table-column>
              </el-table>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
  import {
    ref,
    computed,
    onMounted,
    onBeforeUnmount,
    nextTick,
    watch,
  } from "vue";
  import * as echarts from "echarts";
  // ç­›é€‰æ¡ä»¶
  const dateType = ref("month"); // month æˆ– year
  const productType = ref("block"); // block æˆ– plate
  // å›¾è¡¨å¼•用
  const productionChart = ref(null);
  const solidWasteChart = ref(null);
  const costChart = ref(null);
  const energyChart = ref(null);
  // å›¾è¡¨å®žä¾‹
  let productionChartInstance = null;
  let solidWasteChartInstance = null;
  let costChartInstance = null;
  let energyChartInstance = null;
  // æ¨¡æ‹Ÿæ•°æ®
  const productionData = ref({
    month: [
      { name: "1月", block: 1200, plate: 800 },
      { name: "2月", block: 1300, plate: 850 },
      { name: "3月", block: 1100, plate: 750 },
      { name: "4月", block: 1400, plate: 900 },
      { name: "5月", block: 1500, plate: 950 },
      { name: "6月", block: 1350, plate: 880 },
      { name: "7月", block: 1450, plate: 920 },
      { name: "8月", block: 1600, plate: 1000 },
      { name: "9月", block: 1550, plate: 980 },
      { name: "10月", block: 1700, plate: 1050 },
      { name: "11月", block: 1650, plate: 1020 },
      { name: "12月", block: 1800, plate: 1100 },
    ],
    year: [
      { name: "2023", block: 15000, plate: 9500 },
      { name: "2024", block: 16500, plate: 10200 },
      { name: "2025", block: 18000, plate: 11000 },
    ],
  });
  const solidWasteData = ref({
    month: [
      { name: "1月", ç²‰ç…¤ç°: 200, çŸ³è†: 150, çŸ³ç°: 100 },
      { name: "2月", ç²‰ç…¤ç°: 220, çŸ³è†: 160, çŸ³ç°: 110 },
      { name: "3月", ç²‰ç…¤ç°: 190, çŸ³è†: 140, çŸ³ç°: 95 },
      { name: "4月", ç²‰ç…¤ç°: 230, çŸ³è†: 170, çŸ³ç°: 115 },
      { name: "5月", ç²‰ç…¤ç°: 240, çŸ³è†: 180, çŸ³ç°: 120 },
      { name: "6月", ç²‰ç…¤ç°: 225, çŸ³è†: 165, çŸ³ç°: 112 },
    ],
    year: [
      { name: "2023", ç²‰ç…¤ç°: 2500, çŸ³è†: 1800, çŸ³ç°: 1200 },
      { name: "2024", ç²‰ç…¤ç°: 2700, çŸ³è†: 1950, çŸ³ç°: 1300 },
      { name: "2025", ç²‰ç…¤ç°: 2900, çŸ³è†: 2100, çŸ³ç°: 1400 },
    ],
  });
  const costData = ref({
    materials: ["æ°´æ³¥", "铝粉膏", "脱模剂", "防腐剂", "氯化剂", "冷拔丝"],
    month: {
      consumption: [1200, 50, 80, 30, 40, 60],
      production: [12000, 12000, 12000, 8000, 8000, 8000],
      unitConsumption: [0.1, 0.0042, 0.0067, 0.0038, 0.005, 0.0075],
    },
    year: {
      consumption: [14000, 600, 950, 350, 480, 720],
      production: [140000, 140000, 140000, 95000, 95000, 95000],
      unitConsumption: [0.1, 0.0043, 0.0068, 0.0037, 0.0051, 0.0076],
    },
  });
  const energyData = ref({
    month: [
      { name: "1月", ç”µé‡: 12000, æ°´é‡: 8000, æ°”量: 5000 },
      { name: "2月", ç”µé‡: 13000, æ°´é‡: 8500, æ°”量: 5500 },
      { name: "3月", ç”µé‡: 11000, æ°´é‡: 7500, æ°”量: 4800 },
      { name: "4月", ç”µé‡: 14000, æ°´é‡: 9000, æ°”量: 6000 },
      { name: "5月", ç”µé‡: 15000, æ°´é‡: 9500, æ°”量: 6500 },
      { name: "6月", ç”µé‡: 13500, æ°´é‡: 8800, æ°”量: 5800 },
    ],
    year: [
      { name: "2023", ç”µé‡: 140000, æ°´é‡: 95000, æ°”量: 65000 },
      { name: "2024", ç”µé‡: 150000, æ°´é‡: 100000, æ°”量: 70000 },
      { name: "2025", ç”µé‡: 160000, æ°´é‡: 105000, æ°”量: 75000 },
    ],
  });
  // è®¡ç®—属性
  const productionChartOption = computed(() => {
    const data = productionData.value[dateType.value];
    return {
      tooltip: {
        trigger: "axis",
        axisPointer: {
          type: "shadow",
        },
      },
      legend: {
        data: ["砌块", "板材"],
        textStyle: {
          color: "#333",
        },
      },
      grid: {
        left: "3%",
        right: "4%",
        bottom: "3%",
        containLabel: true,
      },
      xAxis: {
        type: "category",
        data: data.map(item => item.name),
        axisLabel: {
          color: "#333",
        },
      },
      yAxis: {
        type: "value",
        name: "产量 (立方米)",
        axisLabel: {
          color: "#333",
        },
      },
      series: [
        {
          name: "砌块",
          type: "line",
          data: data.map(item => item.block),
          smooth: true,
          lineStyle: {
            width: 3,
          },
          itemStyle: {
            color: "#409EFF",
          },
        },
        {
          name: "板材",
          type: "line",
          data: data.map(item => item.plate),
          smooth: true,
          lineStyle: {
            width: 3,
          },
          itemStyle: {
            color: "#67C23A",
          },
        },
      ],
    };
  });
  const solidWasteChartOption = computed(() => {
    const data = solidWasteData.value[dateType.value];
    return {
      tooltip: {
        trigger: "axis",
        axisPointer: {
          type: "shadow",
        },
      },
      legend: {
        data: ["粉煤灰", "石膏", "石灰"],
        textStyle: {
          color: "#333",
        },
      },
      grid: {
        left: "3%",
        right: "4%",
        bottom: "3%",
        containLabel: true,
      },
      xAxis: {
        type: "category",
        data: data.map(item => item.name),
        axisLabel: {
          color: "#333",
        },
      },
      yAxis: {
        type: "value",
        name: "处理量 (吨)",
        axisLabel: {
          color: "#333",
        },
      },
      series: [
        {
          name: "粉煤灰",
          type: "bar",
          data: data.map(item => item.粉煤灰),
          itemStyle: {
            color: "#909399",
          },
        },
        {
          name: "石膏",
          type: "bar",
          data: data.map(item => item.石膏),
          itemStyle: {
            color: "#E6A23C",
          },
        },
        {
          name: "石灰",
          type: "bar",
          data: data.map(item => item.石灰),
          itemStyle: {
            color: "#F56C6C",
          },
        },
      ],
    };
  });
  const costChartOption = computed(() => {
    const data = costData.value;
    return {
      tooltip: {
        trigger: "axis",
        axisPointer: {
          type: "shadow",
        },
      },
      legend: {
        data: ["月度单耗", "年度单耗"],
        textStyle: {
          color: "#333",
        },
      },
      grid: {
        left: "3%",
        right: "4%",
        bottom: "3%",
        containLabel: true,
      },
      xAxis: {
        type: "category",
        data: data.materials,
        axisLabel: {
          color: "#333",
          rotate: 45,
        },
      },
      yAxis: {
        type: "value",
        name: "单耗 (吨/立方米)",
        axisLabel: {
          color: "#333",
        },
      },
      series: [
        {
          name: "月度单耗",
          type: "bar",
          data: data.month.unitConsumption,
          itemStyle: {
            color: "#409EFF",
          },
        },
        {
          name: "年度单耗",
          type: "bar",
          data: data.year.unitConsumption,
          itemStyle: {
            color: "#67C23A",
          },
        },
      ],
    };
  });
  const energyChartOption = computed(() => {
    const data = energyData.value[dateType.value];
    return {
      tooltip: {
        trigger: "axis",
        axisPointer: {
          type: "shadow",
        },
      },
      legend: {
        data: ["电量", "水量", "气量"],
        textStyle: {
          color: "#333",
        },
      },
      grid: {
        left: "3%",
        right: "4%",
        bottom: "3%",
        containLabel: true,
      },
      xAxis: {
        type: "category",
        data: data.map(item => item.name),
        axisLabel: {
          color: "#333",
        },
      },
      yAxis: {
        type: "value",
        name: "能耗量",
        axisLabel: {
          color: "#333",
        },
      },
      series: [
        {
          name: "电量",
          type: "line",
          data: data.map(item => item.电量),
          smooth: true,
          lineStyle: {
            width: 3,
          },
          itemStyle: {
            color: "#409EFF",
          },
        },
        {
          name: "水量",
          type: "line",
          data: data.map(item => item.水量),
          smooth: true,
          lineStyle: {
            width: 3,
          },
          itemStyle: {
            color: "#67C23A",
          },
        },
        {
          name: "气量",
          type: "line",
          data: data.map(item => item.气量),
          smooth: true,
          lineStyle: {
            width: 3,
          },
          itemStyle: {
            color: "#E6A23C",
          },
        },
      ],
    };
  });
  const costTableData = computed(() => {
    const data = costData.value;
    const materials = data.materials;
    const monthData = data.month;
    const yearData = data.year;
    return materials.map((material, index) => ({
      material,
      unit: "吨/立方米",
      monthlyConsumption: monthData.consumption[index],
      monthlyProduction: monthData.production[index],
      monthlyUnitConsumption: monthData.unitConsumption[index].toFixed(4),
      yearlyConsumption: yearData.consumption[index],
      yearlyProduction: yearData.production[index],
      yearlyUnitConsumption: yearData.unitConsumption[index].toFixed(4),
    }));
  });
  const totalProduction = computed(() => {
    const data = productionData.value[dateType.value];
    if (dateType.value === "month") {
      return data.reduce(
        (sum, item) =>
          sum + item[productType.value === "block" ? "block" : "plate"],
        0
      );
    } else {
      return data[data.length - 1][
        productType.value === "block" ? "block" : "plate"
      ];
    }
  });
  const totalSolidWaste = computed(() => {
    const data = solidWasteData.value[dateType.value];
    if (dateType.value === "month") {
      return data.reduce(
        (sum, item) => sum + item.粉煤灰 + item.石膏 + item.石灰,
        0
      );
    } else {
      const lastItem = data[data.length - 1];
      return lastItem.粉煤灰 + lastItem.石膏 + lastItem.石灰;
    }
  });
  const averageUnitConsumption = computed(() => {
    const data = costData.value;
    const unitConsumption =
      dateType.value === "month"
        ? data.month.unitConsumption
        : data.year.unitConsumption;
    const average =
      unitConsumption.reduce((sum, value) => sum + value, 0) /
      unitConsumption.length;
    return average.toFixed(4);
  });
  const totalEnergy = computed(() => {
    const data = energyData.value[dateType.value];
    if (dateType.value === "month") {
      return data.reduce(
        (sum, item) => sum + item.电量 + item.水量 + item.气量,
        0
      );
    } else {
      const lastItem = data[data.length - 1];
      return lastItem.电量 + lastItem.水量 + lastItem.气量;
    }
  });
  // äº‹ä»¶å¤„理
  const handleDateTypeChange = () => {
    updateCharts();
  };
  const handleProductTypeChange = () => {
    updateCharts();
  };
  // åˆå§‹åŒ–图表
  const initCharts = () => {
    if (productionChart.value) {
      productionChartInstance = echarts.init(productionChart.value);
      productionChartInstance.setOption(productionChartOption.value);
    }
    if (solidWasteChart.value) {
      solidWasteChartInstance = echarts.init(solidWasteChart.value);
      solidWasteChartInstance.setOption(solidWasteChartOption.value);
    }
    if (costChart.value) {
      costChartInstance = echarts.init(costChart.value);
      costChartInstance.setOption(costChartOption.value);
    }
    if (energyChart.value) {
      energyChartInstance = echarts.init(energyChart.value);
      energyChartInstance.setOption(energyChartOption.value);
    }
  };
  // æ›´æ–°å›¾è¡¨
  const updateCharts = () => {
    if (productionChartInstance) {
      productionChartInstance.setOption(productionChartOption.value);
    }
    if (solidWasteChartInstance) {
      solidWasteChartInstance.setOption(solidWasteChartOption.value);
    }
    if (costChartInstance) {
      costChartInstance.setOption(costChartOption.value);
    }
    if (energyChartInstance) {
      energyChartInstance.setOption(energyChartOption.value);
    }
  };
  // è°ƒæ•´å›¾è¡¨å¤§å°
  const resizeCharts = () => {
    productionChartInstance?.resize();
    solidWasteChartInstance?.resize();
    costChartInstance?.resize();
    energyChartInstance?.resize();
  };
  // çª—口大小变化处理
  const handleResize = () => {
    // å»¶è¿Ÿæ‰§è¡Œï¼Œç¡®ä¿DOM更新完成
    setTimeout(() => {
      resizeCharts();
    }, 100);
  };
  // èŽ·å–ç‰©æ–™ç±»åž‹æ ‡ç­¾ç±»åž‹
  const getMaterialTypeType = material => {
    const typeMap = {
      æ°´æ³¥: "primary",
      é“ç²‰è†: "success",
      è„±æ¨¡å‰‚: "warning",
      é˜²è…å‰‚: "danger",
      æ°¯åŒ–剂: "info",
      å†·æ‹”丝: "purple",
    };
    return typeMap[material] || "info";
  };
  // ç”Ÿå‘½å‘¨æœŸé’©å­
  onMounted(() => {
    // ä½¿ç”¨nextTick确保DOM完全渲染后再初始化
    nextTick(() => {
      // åˆå§‹åŒ–图表
      initCharts();
    });
    window.addEventListener("resize", handleResize);
  });
  onBeforeUnmount(() => {
    window.removeEventListener("resize", handleResize);
    // é”€æ¯å›¾è¡¨å®žä¾‹
    productionChartInstance?.dispose();
    solidWasteChartInstance?.dispose();
    costChartInstance?.dispose();
    energyChartInstance?.dispose();
  });
</script>
<style scoped>
  /* å¤–部容器 - å æ®æ•´ä¸ªè§†å£ */
  .dashboard-container {
    position: relative;
    width: 100%;
    /* é¡µé¢åœ¨å¸¸è§„布局下(有顶栏)默认减去 84px,避免内容被裁切 */
    min-height: calc(100vh - 84px);
    background-color: #f5f7fa;
    overflow: hidden;
  }
  /* å†…部内容区域 - è‡ªé€‚应宽度 */
  .data-dashboard {
    position: relative;
    width: 100%;
    min-height: 100%;
    background-color: #ffffff;
    box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
  }
  .dashboard-header {
    position: relative;
    z-index: 1;
    height: 86px;
    background-color: #ffffff;
    border-bottom: 1px solid #e4e7ed;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .factory-name {
    font-weight: 600;
    font-size: 32px;
    color: #303133;
  }
  .filter-area {
    padding: 20px;
    background-color: #ffffff;
    border-bottom: 1px solid #e4e7ed;
    display: flex;
    gap: 40px;
    align-items: center;
    flex-wrap: wrap;
  }
  .filter-section {
    display: flex;
    align-items: center;
    gap: 10px;
  }
  .filter-label {
    font-size: 14px;
    font-weight: 500;
    color: #303133;
    white-space: nowrap;
  }
  .radio-group {
    display: flex;
    align-items: center;
  }
  /* æŒ‰é’®æ ·å¼ */
  :deep(.el-radio-button__inner) {
    border-radius: 4px;
    padding: 8px 20px;
    font-size: 14px;
    transition: all 0.3s ease;
  }
  :deep(.el-radio-button__orig-radio:checked + .el-radio-button__inner) {
    background-color: #409eff;
    border-color: #409eff;
    color: #ffffff;
    box-shadow: 0 2px 4px rgba(64, 158, 255, 0.3);
  }
  :deep(.el-radio-button__inner:hover) {
    color: #409eff;
    border-color: #c6e2ff;
  }
  :deep(.el-radio-button:first-child .el-radio-button__inner) {
    border-radius: 4px 0 0 4px;
  }
  :deep(.el-radio-button:last-child .el-radio-button__inner) {
    border-radius: 0 4px 4px 0;
  }
  .dashboard-content {
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    gap: 20px;
    padding: 20px;
    min-height: 800px;
    overflow: hidden;
  }
  /* è¡Œå¸ƒå±€ */
  .row {
    display: flex;
    gap: 20px;
    align-items: stretch;
  }
  /* ç¬¬ä¸€è¡Œï¼š3个卡片 */
  .row-1 {
    height: 300px;
  }
  /* ç¬¬äºŒè¡Œï¼š2个卡片 */
  .row-2 {
    height: 300px;
  }
  /* ç¬¬ä¸‰è¡Œï¼š1个卡片 */
  .row-3 {
    min-height: 250px;
  }
  /* å¡ç‰‡æ ·å¼ */
  .panel-card {
    background-color: #ffffff;
    border-radius: 8px;
    border: 1px solid #e4e7ed;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
    transition: all 0.3s ease;
  }
  .panel-card:hover {
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
    transform: translateY(-2px);
  }
  /* å¡ç‰‡å¸ƒå±€ */
  .card-1 {
    flex: 1;
  }
  .card-2 {
    flex: 1;
  }
  .card-3 {
    flex: 0.8;
  }
  .card-4 {
    flex: 1.2;
  }
  .card-5 {
    flex: 0.8;
  }
  .card-6 {
    flex: 1;
  }
  .panel-card {
    background-color: #ffffff;
    border-radius: 8px;
    border: 1px solid #e4e7ed;
    overflow: hidden;
    flex: 1;
    display: flex;
    flex-direction: column;
  }
  .panel-title {
    padding: 15px 20px;
    font-size: 16px;
    font-weight: 500;
    color: #303133;
    border-bottom: 1px solid #e4e7ed;
    background-color: #fafafa;
  }
  .card-1 .panel-title {
    border-left: 4px solid #409eff;
  }
  .card-2 .panel-title {
    border-left: 4px solid #f56c6c;
  }
  .card-3 .panel-title {
    border-left: 4px solid #e6a23c;
  }
  .card-4 .panel-title {
    border-left: 4px solid #409eff;
  }
  .card-5 .panel-title {
    border-left: 4px solid #67c23a;
  }
  .card-6 .panel-title {
    border-left: 4px solid #e6a23c;
  }
  .chart-container {
    flex: 1;
    padding: 20px;
  }
  .table-container {
    flex: 1;
    padding: 20px;
    overflow: auto;
  }
  .stats-grid {
    flex: 1;
    padding: 15px;
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    grid-template-rows: repeat(2, 1fr);
    gap: 15px;
  }
  .stat-item {
    background-color: #fafafa;
    border-radius: 8px;
    padding: 15px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    border: 1px solid #e4e7ed;
    min-height: 80px;
  }
  .stat-label {
    font-size: 13px;
    color: #606266;
    margin-bottom: 8px;
  }
  .stat-value {
    font-size: 20px;
    font-weight: 600;
    color: #303133;
    margin-bottom: 3px;
  }
  .production-color {
    color: #409eff;
    text-shadow: 0 2px 4px rgba(64, 158, 255, 0.3);
  }
  .waste-color {
    color: #f56c6c;
    text-shadow: 0 2px 4px rgba(245, 108, 108, 0.3);
  }
  .consumption-color {
    color: #e6a23c;
    text-shadow: 0 2px 4px rgba(230, 162, 60, 0.3);
  }
  .energy-color {
    color: #67c23a;
    text-shadow: 0 2px 4px rgba(103, 194, 58, 0.3);
  }
  .stat-unit {
    font-size: 11px;
    color: #909399;
  }
  /* è¡¨æ ¼æ ·å¼ */
  :deep(.el-table) {
    border-radius: 8px;
    overflow: hidden;
  }
  :deep(.el-table th) {
    background-color: #fafafa;
    font-weight: 500;
  }
  :deep(.el-table tr:hover > td) {
    background-color: #ecf5ff;
  }
  .data-value {
    font-weight: bold;
    color: #409eff;
  }
  /* æŒ‰é’®æ ·å¼ */
  :deep(.el-radio-button__inner) {
    border-radius: 4px;
  }
  :deep(.el-radio-button__orig-radio:checked + .el-radio-button__inner) {
    background-color: #409eff;
    border-color: #409eff;
    color: #ffffff;
  }
</style>
src/views/reportAnalysis/salesStatistics/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1304 @@
<template>
  <div class="sales-statistics-container">
    <div class="data-dashboard">
      <!-- é¡µé¢æ ‡é¢˜ -->
      <!-- <div class="dashboard-header">
        <div class="factory-name">销售统计看板</div>
      </div> -->
      <!-- ç­›é€‰æ¡ä»¶ -->
      <div class="filter-area">
        <div class="filter-section">
          <span class="filter-label">时间范围:</span>
          <el-date-picker v-model="dateRange"
                          type="daterange"
                          range-separator="至"
                          start-placeholder="开始日期"
                          end-placeholder="结束日期"
                          value-format="YYYY-MM-DD"
                          @change="handleDateChange"
                          style="width: 240px;" />
        </div>
        <div class="filter-section">
          <span class="filter-label">产品类型:</span>
          <el-select v-model="productType"
                     placeholder="请选择产品类型"
                     @change="handleFilterChange"
                     style="width: 160px;">
            <el-option label="全部"
                       value="" />
            <el-option label="砌块"
                       value="block" />
            <el-option label="板材"
                       value="board" />
            <el-option label="型材"
                       value="profile" />
          </el-select>
        </div>
        <div class="filter-section">
          <span class="filter-label">销售区域:</span>
          <el-select v-model="salesArea"
                     placeholder="请选择销售区域"
                     @change="handleFilterChange"
                     style="width: 160px;">
            <el-option label="全部"
                       value="" />
            <el-option label="华东"
                       value="east" />
            <el-option label="华北"
                       value="north" />
            <el-option label="华南"
                       value="south" />
            <el-option label="西南"
                       value="southwest" />
            <el-option label="西北"
                       value="northwest" />
          </el-select>
        </div>
        <div class="filter-section">
          <span class="filter-label">统计维度:</span>
          <el-select v-model="statDimension"
                     placeholder="请选择统计维度"
                     @change="handleFilterChange"
                     style="width: 160px;">
            <el-option label="月度"
                       value="month" />
            <el-option label="年度"
                       value="year" />
          </el-select>
        </div>
      </div>
      <div class="dashboard-content">
        <!-- æ ¸å¿ƒæŒ‡æ ‡å¡ç‰‡ -->
        <div class="row row-1">
          <div class="panel-card card-1">
            <div class="panel-title">合计销量</div>
            <div class="stats-grid">
              <div class="stat-item">
                <div class="stat-value sales-volume-color">{{ totalSalesVolume }}</div>
                <div class="stat-unit">立方米</div>
                <div class="stat-change">{{ salesVolumeChange }}%</div>
              </div>
            </div>
          </div>
          <div class="panel-card card-2">
            <div class="panel-title">销售金额</div>
            <div class="stats-grid">
              <div class="stat-item">
                <div class="stat-value sales-amount-color">{{ totalSalesAmount }}</div>
                <div class="stat-unit">万元</div>
                <div class="stat-change">{{ salesAmountChange }}%</div>
              </div>
            </div>
          </div>
          <div class="panel-card card-3">
            <div class="panel-title">新增客户</div>
            <div class="stats-grid">
              <div class="stat-item">
                <div class="stat-value new-customer-color">{{ newCustomerCount }}</div>
                <div class="stat-unit">个</div>
                <div class="stat-change">{{ customerCountChange }}%</div>
              </div>
            </div>
          </div>
          <div class="panel-card card-4">
            <div class="panel-title">合计客户</div>
            <div class="stats-grid">
              <div class="stat-item">
                <div class="stat-value total-customer-color">{{ totalCustomerCount }}</div>
                <div class="stat-unit">个</div>
                <div class="stat-change">{{ totalCustomerChange }}%</div>
              </div>
            </div>
          </div>
        </div>
        <!-- é”€é‡å’Œé”€å”®é‡‘额趋势 -->
        <div class="row row-2">
          <div class="panel-card card-5">
            <div class="panel-title">销量趋势</div>
            <div class="chart-container">
              <div ref="salesVolumeChart"
                   style="width: 100%; height: 100%;"></div>
            </div>
          </div>
          <div class="panel-card card-6">
            <div class="panel-title">销售金额趋势</div>
            <div class="chart-container">
              <div ref="salesAmountChart"
                   style="width: 100%; height: 100%;"></div>
            </div>
          </div>
        </div>
        <!-- ç´¯è®¡æ•°æ®è¶‹åŠ¿ -->
        <!-- <div class="row row-3">
          <div class="panel-card card-10">
            <div class="panel-title">累计销量趋势</div>
            <div class="chart-container">
              <div ref="cumulativeSalesVolumeChart"
                   style="width: 100%; height: 100%;"></div>
            </div>
          </div>
          <div class="panel-card card-11">
            <div class="panel-title">累计销售金额趋势</div>
            <div class="chart-container">
              <div ref="cumulativeSalesAmountChart"
                   style="width: 100%; height: 100%;"></div>
            </div>
          </div>
        </div> -->
        <!-- å›¾è¡¨åŒºåŸŸå’Œè¡¨æ ¼ -->
        <div class="row row-4">
          <!-- å·¦è¾¹ï¼šè¯¦ç»†æ•°æ®è¡¨æ ¼ -->
          <div class="panel-card card-9"
               style="flex: 2;">
            <div class="panel-title">销售统计详细数据</div>
            <div class="table-container">
              <el-table :data="tableData"
                        style="width: 100%">
                <el-table-column prop="productType"
                                 label="产品类型"
                                 width="120"
                                 align="center">
                  <template #default="scope">
                    <el-tag :type="getProductTypeType(scope.row.productType)">
                      {{ scope.row.productType }}
                    </el-tag>
                  </template>
                </el-table-column>
                <el-table-column prop="salesArea"
                                 label="销售区域"
                                 width="120"
                                 align="center">
                  <template #default="scope">
                    <el-tag :type="getSalesAreaType(scope.row.salesArea)">
                      {{ scope.row.salesArea }}
                    </el-tag>
                  </template>
                </el-table-column>
                <el-table-column prop="period"
                                 label="统计周期"
                                 width="120" />
                <el-table-column prop="salesVolume"
                                 label="销量(立方米)"
                                 align="right">
                  <template #default="scope">
                    <span class="data-value">{{ scope.row.salesVolume }}</span>
                  </template>
                </el-table-column>
                <el-table-column prop="salesAmount"
                                 label="销售金额(万元)"
                                 align="right">
                  <template #default="scope">
                    <span class="data-value">{{ scope.row.salesAmount }}</span>
                  </template>
                </el-table-column>
                <el-table-column prop="newCustomers"
                                 label="新增客户(个)"
                                 width="150"
                                 align="right">
                  <template #default="scope">
                    <span class="data-value">{{ scope.row.newCustomers }}</span>
                  </template>
                </el-table-column>
                <el-table-column prop="totalCustomers"
                                 label="合计客户(个)"
                                 width="150"
                                 align="right">
                  <template #default="scope">
                    <span class="data-value">{{ scope.row.totalCustomers }}</span>
                  </template>
                </el-table-column>
              </el-table>
            </div>
          </div>
          <!-- å³è¾¹ï¼šäº§å“ç±»åž‹åˆ†å¸ƒå’Œé”€å”®åŒºåŸŸåˆ†å¸ƒ -->
          <div class="chart-column"
               style="flex: 1; display: flex; flex-direction: column; gap: 20px;">
            <div class="panel-card card-7"
                 style="flex: 1;">
              <div class="panel-title">产品类型分布</div>
              <div class="chart-container">
                <div ref="productTypeChart"
                     style="width: 100%; height: 100%;"></div>
              </div>
            </div>
            <div class="panel-card card-8"
                 style="flex: 1;">
              <div class="panel-title">销售区域分布</div>
              <div class="chart-container">
                <div ref="salesAreaChart"
                     style="width: 100%; height: 100%;"></div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
  import {
    ref,
    computed,
    onMounted,
    onBeforeUnmount,
    watch,
    nextTick,
  } from "vue";
  import { useRouter } from "vue-router";
  import * as echarts from "echarts";
  import dayjs from "dayjs";
  const router = useRouter();
  // ç­›é€‰æ¡ä»¶
  const dateRange = ref([]);
  const productType = ref("");
  const salesArea = ref("");
  const statDimension = ref("month");
  // å›¾è¡¨å¼•用
  const salesVolumeChart = ref(null);
  const salesAmountChart = ref(null);
  const productTypeChart = ref(null);
  const salesAreaChart = ref(null);
  const cumulativeSalesVolumeChart = ref(null);
  const cumulativeSalesAmountChart = ref(null);
  // å›¾è¡¨å®žä¾‹
  let salesVolumeChartInstance = null;
  let salesAmountChartInstance = null;
  let productTypeChartInstance = null;
  let salesAreaChartInstance = null;
  let cumulativeSalesVolumeChartInstance = null;
  let cumulativeSalesAmountChartInstance = null;
  // æ¨¡æ‹Ÿæ•°æ®
  const mockData = [
    // 2026å¹´1月数据
    {
      productType: "砌块",
      salesArea: "华东",
      period: "2026-01",
      salesVolume: 1200,
      salesAmount: 180,
      newCustomers: 5,
      totalCustomers: 120,
    },
    {
      productType: "砌块",
      salesArea: "华北",
      period: "2026-01",
      salesVolume: 800,
      salesAmount: 120,
      newCustomers: 3,
      totalCustomers: 80,
    },
    {
      productType: "砌块",
      salesArea: "华南",
      period: "2026-01",
      salesVolume: 600,
      salesAmount: 90,
      newCustomers: 2,
      totalCustomers: 60,
    },
    {
      productType: "板材",
      salesArea: "华东",
      period: "2026-01",
      salesVolume: 900,
      salesAmount: 270,
      newCustomers: 4,
      totalCustomers: 100,
    },
    {
      productType: "板材",
      salesArea: "华北",
      period: "2026-01",
      salesVolume: 500,
      salesAmount: 150,
      newCustomers: 2,
      totalCustomers: 70,
    },
    {
      productType: "型材",
      salesArea: "华东",
      period: "2026-01",
      salesVolume: 400,
      salesAmount: 200,
      newCustomers: 3,
      totalCustomers: 50,
    },
    // 2026å¹´2月数据
    {
      productType: "砌块",
      salesArea: "华东",
      period: "2026-02",
      salesVolume: 1300,
      salesAmount: 195,
      newCustomers: 4,
      totalCustomers: 124,
    },
    {
      productType: "砌块",
      salesArea: "华北",
      period: "2026-02",
      salesVolume: 850,
      salesAmount: 127.5,
      newCustomers: 2,
      totalCustomers: 82,
    },
    {
      productType: "砌块",
      salesArea: "华南",
      period: "2026-02",
      salesVolume: 650,
      salesAmount: 97.5,
      newCustomers: 1,
      totalCustomers: 61,
    },
    {
      productType: "板材",
      salesArea: "华东",
      period: "2026-02",
      salesVolume: 950,
      salesAmount: 285,
      newCustomers: 3,
      totalCustomers: 103,
    },
    {
      productType: "板材",
      salesArea: "华北",
      period: "2026-02",
      salesVolume: 550,
      salesAmount: 165,
      newCustomers: 1,
      totalCustomers: 71,
    },
    {
      productType: "型材",
      salesArea: "华东",
      period: "2026-02",
      salesVolume: 450,
      salesAmount: 225,
      newCustomers: 2,
      totalCustomers: 52,
    },
    // 2026å¹´3月数据
    {
      productType: "砌块",
      salesArea: "华东",
      period: "2026-03",
      salesVolume: 1400,
      salesAmount: 210,
      newCustomers: 6,
      totalCustomers: 130,
    },
    {
      productType: "砌块",
      salesArea: "华北",
      period: "2026-03",
      salesVolume: 900,
      salesAmount: 135,
      newCustomers: 3,
      totalCustomers: 85,
    },
    {
      productType: "砌块",
      salesArea: "华南",
      period: "2026-03",
      salesVolume: 700,
      salesAmount: 105,
      newCustomers: 2,
      totalCustomers: 63,
    },
    {
      productType: "板材",
      salesArea: "华东",
      period: "2026-03",
      salesVolume: 1000,
      salesAmount: 300,
      newCustomers: 5,
      totalCustomers: 108,
    },
    {
      productType: "板材",
      salesArea: "华北",
      period: "2026-03",
      salesVolume: 600,
      salesAmount: 180,
      newCustomers: 2,
      totalCustomers: 73,
    },
    {
      productType: "型材",
      salesArea: "华东",
      period: "2026-03",
      salesVolume: 500,
      salesAmount: 250,
      newCustomers: 3,
      totalCustomers: 55,
    },
    // è¥¿å—和西北地区数据
    {
      productType: "砌块",
      salesArea: "西南",
      period: "2026-03",
      salesVolume: 500,
      salesAmount: 75,
      newCustomers: 2,
      totalCustomers: 40,
    },
    {
      productType: "板材",
      salesArea: "西南",
      period: "2026-03",
      salesVolume: 300,
      salesAmount: 90,
      newCustomers: 1,
      totalCustomers: 30,
    },
    {
      productType: "砌块",
      salesArea: "西北",
      period: "2026-03",
      salesVolume: 400,
      salesAmount: 60,
      newCustomers: 1,
      totalCustomers: 35,
    },
    {
      productType: "板材",
      salesArea: "西北",
      period: "2026-03",
      salesVolume: 200,
      salesAmount: 60,
      newCustomers: 1,
      totalCustomers: 25,
    },
  ];
  // è®¡ç®—属性
  const filteredData = computed(() => {
    let result = [...mockData];
    // æŒ‰äº§å“ç±»åž‹ç­›é€‰
    if (productType.value) {
      result = result.filter(item => {
        const typeMap = { block: "砌块", board: "板材", profile: "型材" };
        return item.productType === typeMap[productType.value];
      });
    }
    // æŒ‰é”€å”®åŒºåŸŸç­›é€‰
    if (salesArea.value) {
      result = result.filter(item => {
        const areaMap = {
          east: "华东",
          north: "华北",
          south: "华南",
          southwest: "西南",
          northwest: "西北",
        };
        return item.salesArea === areaMap[salesArea.value];
      });
    }
    // æŒ‰æ—¶é—´èŒƒå›´ç­›é€‰
    if (dateRange.value && dateRange.value.length === 2) {
      const startDate = dayjs(dateRange.value[0]);
      const endDate = dayjs(dateRange.value[1]);
      result = result.filter(item => {
        const itemDate = dayjs(item.period);
        return (
          itemDate.isAfter(startDate.subtract(1, "day")) &&
          itemDate.isBefore(endDate.add(1, "day"))
        );
      });
    }
    return result;
  });
  // æ ¸å¿ƒæŒ‡æ ‡è®¡ç®—
  const totalSalesVolume = computed(() => {
    return filteredData.value.reduce((sum, item) => sum + item.salesVolume, 0);
  });
  const totalSalesAmount = computed(() => {
    return filteredData.value
      .reduce((sum, item) => sum + item.salesAmount, 0)
      .toFixed(2);
  });
  const newCustomerCount = computed(() => {
    return filteredData.value.reduce((sum, item) => sum + item.newCustomers, 0);
  });
  const totalCustomerCount = computed(() => {
    // è®¡ç®—每个区域和产品类型的最大客户数
    const customerMap = {};
    filteredData.value.forEach(item => {
      const key = `${item.productType}-${item.salesArea}`;
      if (!customerMap[key] || item.totalCustomers > customerMap[key]) {
        customerMap[key] = item.totalCustomers;
      }
    });
    return Object.values(customerMap).reduce((sum, count) => sum + count, 0);
  });
  // å˜åŒ–率计算(模拟)
  const salesVolumeChange = ref("+5.2");
  const salesAmountChange = ref("+7.8");
  const customerCountChange = ref("+3.5");
  const totalCustomerChange = ref("+2.1");
  // è¡¨æ ¼æ•°æ®
  const tableData = computed(() => {
    return filteredData.value.map(item => {
      // è®¡ç®—累计值(模拟)
      const cumulativeSalesVolume = item.salesVolume * 1.5;
      const cumulativeSalesAmount = item.salesAmount * 1.5;
      const cumulativeNewCustomers = item.newCustomers * 2;
      return {
        ...item,
        cumulativeSalesVolume,
        cumulativeSalesAmount,
        cumulativeNewCustomers,
      };
    });
  });
  // é”€é‡è¶‹åŠ¿å›¾è¡¨é…ç½®
  const salesVolumeChartOption = computed(() => {
    // æŒ‰å‘¨æœŸåˆ†ç»„
    const periodMap = {};
    filteredData.value.forEach(item => {
      if (!periodMap[item.period]) {
        periodMap[item.period] = 0;
      }
      periodMap[item.period] += item.salesVolume;
    });
    const periods = Object.keys(periodMap).sort();
    const values = periods.map(period => periodMap[period]);
    return {
      tooltip: {
        trigger: "axis",
        formatter: "{b}: {c} ç«‹æ–¹ç±³",
      },
      xAxis: {
        type: "category",
        data: periods,
      },
      yAxis: {
        type: "value",
        name: "销量(立方米)",
      },
      series: [
        {
          data: values,
          type: "line",
          smooth: true,
          lineStyle: {
            width: 3,
          },
          itemStyle: {
            color: "#409EFF",
          },
        },
      ],
    };
  });
  // é”€å”®é‡‘额趋势图表配置
  const salesAmountChartOption = computed(() => {
    // æŒ‰å‘¨æœŸåˆ†ç»„
    const periodMap = {};
    filteredData.value.forEach(item => {
      if (!periodMap[item.period]) {
        periodMap[item.period] = 0;
      }
      periodMap[item.period] += item.salesAmount;
    });
    const periods = Object.keys(periodMap).sort();
    const values = periods.map(period => periodMap[period]);
    return {
      tooltip: {
        trigger: "axis",
        formatter: "{b}: {c} ä¸‡å…ƒ",
      },
      xAxis: {
        type: "category",
        data: periods,
      },
      yAxis: {
        type: "value",
        name: "销售金额(万元)",
      },
      series: [
        {
          data: values,
          type: "bar",
          itemStyle: {
            color: "#67C23A",
          },
        },
      ],
    };
  });
  // äº§å“ç±»åž‹åˆ†å¸ƒå›¾è¡¨é…ç½®
  const productTypeChartOption = computed(() => {
    // æŒ‰äº§å“ç±»åž‹åˆ†ç»„
    const typeMap = {};
    filteredData.value.forEach(item => {
      if (!typeMap[item.productType]) {
        typeMap[item.productType] = 0;
      }
      typeMap[item.productType] += item.salesVolume;
    });
    const types = Object.keys(typeMap);
    const values = types.map(type => typeMap[type]);
    return {
      tooltip: {
        trigger: "item",
        formatter: "{b}: {c} ç«‹æ–¹ç±³ ({d}%)",
      },
      series: [
        {
          type: "pie",
          radius: "60%",
          data: types.map((type, index) => ({
            name: type,
            value: values[index],
          })),
          emphasis: {
            itemStyle: {
              shadowBlur: 10,
              shadowOffsetX: 0,
              shadowColor: "rgba(0, 0, 0, 0.5)",
            },
          },
        },
      ],
    };
  });
  // é”€å”®åŒºåŸŸåˆ†å¸ƒå›¾è¡¨é…ç½®
  const salesAreaChartOption = computed(() => {
    // æŒ‰é”€å”®åŒºåŸŸåˆ†ç»„
    const areaMap = {};
    filteredData.value.forEach(item => {
      if (!areaMap[item.salesArea]) {
        areaMap[item.salesArea] = 0;
      }
      areaMap[item.salesArea] += item.salesVolume;
    });
    const areas = Object.keys(areaMap);
    const values = areas.map(area => areaMap[area]);
    return {
      tooltip: {
        trigger: "item",
        formatter: "{b}: {c} ç«‹æ–¹ç±³ ({d}%)",
      },
      series: [
        {
          type: "pie",
          radius: "60%",
          data: areas.map((area, index) => ({
            name: area,
            value: values[index],
          })),
          emphasis: {
            itemStyle: {
              shadowBlur: 10,
              shadowOffsetX: 0,
              shadowColor: "rgba(0, 0, 0, 0.5)",
            },
          },
        },
      ],
    };
  });
  // ç´¯è®¡é”€é‡è¶‹åŠ¿å›¾è¡¨é…ç½®
  const cumulativeSalesVolumeChartOption = computed(() => {
    // æŒ‰å‘¨æœŸåˆ†ç»„
    const periodMap = {};
    let cumulativeValue = 0;
    // æŒ‰å‘¨æœŸæŽ’序
    const sortedData = [...filteredData.value].sort((a, b) =>
      a.period.localeCompare(b.period)
    );
    sortedData.forEach(item => {
      cumulativeValue += item.salesVolume;
      periodMap[item.period] = cumulativeValue;
    });
    const periods = Object.keys(periodMap).sort();
    const values = periods.map(period => periodMap[period]);
    return {
      tooltip: {
        trigger: "axis",
        formatter: "{b}: {c} ç«‹æ–¹ç±³",
      },
      xAxis: {
        type: "category",
        data: periods,
      },
      yAxis: {
        type: "value",
        name: "累计销量(立方米)",
      },
      series: [
        {
          data: values,
          type: "line",
          smooth: true,
          areaStyle: {
            opacity: 0.3,
          },
          itemStyle: {
            color: "#E6A23C",
          },
          lineStyle: {
            width: 3,
          },
        },
      ],
    };
  });
  // ç´¯è®¡é”€å”®é‡‘额趋势图表配置
  const cumulativeSalesAmountChartOption = computed(() => {
    // æŒ‰å‘¨æœŸåˆ†ç»„
    const periodMap = {};
    let cumulativeValue = 0;
    // æŒ‰å‘¨æœŸæŽ’序
    const sortedData = [...filteredData.value].sort((a, b) =>
      a.period.localeCompare(b.period)
    );
    sortedData.forEach(item => {
      cumulativeValue += item.salesAmount;
      periodMap[item.period] = cumulativeValue;
    });
    const periods = Object.keys(periodMap).sort();
    const values = periods.map(period => periodMap[period]);
    return {
      tooltip: {
        trigger: "axis",
        formatter: "{b}: {c} ä¸‡å…ƒ",
      },
      xAxis: {
        type: "category",
        data: periods,
      },
      yAxis: {
        type: "value",
        name: "累计销售金额(万元)",
      },
      series: [
        {
          data: values,
          type: "bar",
          itemStyle: {
            color: "#F56C6C",
          },
        },
      ],
    };
  });
  // æ–¹æ³•
  const goBack = () => {
    router.back();
  };
  const handleDateChange = () => {
    // å¤„理日期变化
    updateCharts();
  };
  const handleFilterChange = () => {
    // å¤„理筛选条件变化
    updateCharts();
  };
  // åˆå§‹åŒ–图表
  const initCharts = () => {
    // åˆå§‹åŒ–销量趋势图表
    if (salesVolumeChart.value && !salesVolumeChartInstance) {
      salesVolumeChartInstance = echarts.init(salesVolumeChart.value);
    }
    // åˆå§‹åŒ–销售金额趋势图表
    if (salesAmountChart.value && !salesAmountChartInstance) {
      salesAmountChartInstance = echarts.init(salesAmountChart.value);
    }
    // åˆå§‹åŒ–产品类型分布图表
    if (productTypeChart.value && !productTypeChartInstance) {
      productTypeChartInstance = echarts.init(productTypeChart.value);
    }
    // åˆå§‹åŒ–销售区域分布图表
    if (salesAreaChart.value && !salesAreaChartInstance) {
      salesAreaChartInstance = echarts.init(salesAreaChart.value);
    }
    // åˆå§‹åŒ–累计销量趋势图表
    if (cumulativeSalesVolumeChart.value && !cumulativeSalesVolumeChartInstance) {
      cumulativeSalesVolumeChartInstance = echarts.init(
        cumulativeSalesVolumeChart.value
      );
    }
    // åˆå§‹åŒ–累计销售金额趋势图表
    if (cumulativeSalesAmountChart.value && !cumulativeSalesAmountChartInstance) {
      cumulativeSalesAmountChartInstance = echarts.init(
        cumulativeSalesAmountChart.value
      );
    }
    updateCharts();
  };
  // æ›´æ–°å›¾è¡¨
  const updateCharts = () => {
    // æ›´æ–°é”€é‡è¶‹åŠ¿å›¾è¡¨
    if (salesVolumeChartInstance) {
      salesVolumeChartInstance.setOption(salesVolumeChartOption.value);
    }
    // æ›´æ–°é”€å”®é‡‘额趋势图表
    if (salesAmountChartInstance) {
      salesAmountChartInstance.setOption(salesAmountChartOption.value);
    }
    // æ›´æ–°äº§å“ç±»åž‹åˆ†å¸ƒå›¾è¡¨
    if (productTypeChartInstance) {
      productTypeChartInstance.setOption(productTypeChartOption.value);
    }
    // æ›´æ–°é”€å”®åŒºåŸŸåˆ†å¸ƒå›¾è¡¨
    if (salesAreaChartInstance) {
      salesAreaChartInstance.setOption(salesAreaChartOption.value);
    }
    // æ›´æ–°ç´¯è®¡é”€é‡è¶‹åŠ¿å›¾è¡¨
    if (cumulativeSalesVolumeChartInstance) {
      cumulativeSalesVolumeChartInstance.setOption(
        cumulativeSalesVolumeChartOption.value
      );
    }
    // æ›´æ–°ç´¯è®¡é”€å”®é‡‘额趋势图表
    if (cumulativeSalesAmountChartInstance) {
      cumulativeSalesAmountChartInstance.setOption(
        cumulativeSalesAmountChartOption.value
      );
    }
  };
  // ç›‘听窗口大小变化
  const handleResize = () => {
    if (salesVolumeChartInstance) {
      salesVolumeChartInstance.resize();
    }
    if (salesAmountChartInstance) {
      salesAmountChartInstance.resize();
    }
    if (productTypeChartInstance) {
      productTypeChartInstance.resize();
    }
    if (salesAreaChartInstance) {
      salesAreaChartInstance.resize();
    }
    if (cumulativeSalesVolumeChartInstance) {
      cumulativeSalesVolumeChartInstance.resize();
    }
    if (cumulativeSalesAmountChartInstance) {
      cumulativeSalesAmountChartInstance.resize();
    }
  };
  // ç”Ÿå‘½å‘¨æœŸ
  onMounted(() => {
    // è®¾ç½®é»˜è®¤æ—¥æœŸèŒƒå›´ä¸ºæœ€è¿‘3个月
    const endDate = dayjs();
    const startDate = endDate.subtract(3, "month");
    dateRange.value = [
      startDate.format("YYYY-MM-DD"),
      endDate.format("YYYY-MM-DD"),
    ];
    // ç­‰å¾…DOM更新后初始化图表
    nextTick(() => {
      initCharts();
    });
    // æ·»åŠ çª—å£å¤§å°å˜åŒ–ç›‘å¬
    window.addEventListener("resize", handleResize);
  });
  // èŽ·å–äº§å“ç±»åž‹æ ‡ç­¾ç±»åž‹
  const getProductTypeType = type => {
    const typeMap = {
      ç Œå—: "primary",
      æ¿æ: "success",
      åž‹æ: "warning",
    };
    return typeMap[type] || "info";
  };
  // èŽ·å–é”€å”®åŒºåŸŸæ ‡ç­¾ç±»åž‹
  const getSalesAreaType = area => {
    const typeMap = {
      åŽä¸œ: "primary",
      åŽåŒ—: "success",
      åŽå—: "warning",
      è¥¿å—: "danger",
      è¥¿åŒ—: "info",
    };
    return typeMap[area] || "info";
  };
  // ç»„件卸载时销毁图表实例
  onBeforeUnmount(() => {
    if (salesVolumeChartInstance) {
      salesVolumeChartInstance.dispose();
    }
    if (salesAmountChartInstance) {
      salesAmountChartInstance.dispose();
    }
    if (productTypeChartInstance) {
      productTypeChartInstance.dispose();
    }
    if (salesAreaChartInstance) {
      salesAreaChartInstance.dispose();
    }
    if (cumulativeSalesVolumeChartInstance) {
      cumulativeSalesVolumeChartInstance.dispose();
    }
    if (cumulativeSalesAmountChartInstance) {
      cumulativeSalesAmountChartInstance.dispose();
    }
    // ç§»é™¤çª—口大小变化监听
    window.removeEventListener("resize", handleResize);
  });
</script>
<style scoped>
  /* å¤–部容器 - å æ®æ•´ä¸ªè§†å£ */
  .sales-statistics-container {
    position: relative;
    width: 100%;
    /* é¡µé¢åœ¨å¸¸è§„布局下(有顶栏)默认减去 84px,避免内容被裁切 */
    min-height: calc(100vh - 84px);
    background-color: #f5f7fa;
    overflow: hidden;
  }
  /* å†…部内容区域 - è‡ªé€‚应宽度 */
  .data-dashboard {
    position: relative;
    width: 100%;
    min-height: 100%;
    background-color: #ffffff;
    box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
  }
  .filter-area {
    padding: 20px;
    background-color: #ffffff;
    border-bottom: 1px solid #e4e7ed;
    display: flex;
    gap: 40px;
    align-items: center;
    flex-wrap: wrap;
  }
  .filter-section {
    display: flex;
    align-items: center;
    gap: 10px;
  }
  .filter-label {
    font-size: 14px;
    font-weight: 500;
    color: #303133;
    white-space: nowrap;
  }
  .dashboard-content {
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    gap: 20px;
    padding: 20px;
    min-height: 800px;
    overflow: hidden;
  }
  /* è¡Œå¸ƒå±€ */
  .row {
    display: flex;
    gap: 20px;
    align-items: stretch;
  }
  /* ç¬¬ä¸€è¡Œï¼š4个指标卡片 */
  .row-1 {
    height: 180px;
  }
  /* ç¬¬äºŒè¡Œï¼š2个趋势图表 */
  .row-2 {
    height: 350px;
  }
  /* ç¬¬ä¸‰è¡Œï¼šç´¯è®¡æ•°æ®è¶‹åŠ¿ */
  .row-3 {
    height: 350px;
  }
  /* ç¬¬å››è¡Œï¼šè¡¨æ ¼å’Œå›¾è¡¨ */
  .row-4 {
    height: 600px;
  }
  /* å¡ç‰‡æ ·å¼ */
  .panel-card {
    background-color: #ffffff;
    border-radius: 8px;
    border: 1px solid #e4e7ed;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
    transition: all 0.3s ease;
  }
  .panel-card:hover {
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
    transform: translateY(-2px);
  }
  /* å¡ç‰‡å¸ƒå±€ */
  .card-1 {
    flex: 1;
  }
  .card-2 {
    flex: 1;
  }
  .card-3 {
    flex: 1;
  }
  .card-4 {
    flex: 1;
  }
  .card-5 {
    flex: 1;
  }
  .card-6 {
    flex: 1;
  }
  .card-7 {
    flex: 1;
  }
  .card-8 {
    flex: 1;
  }
  .card-9 {
    flex: 1;
  }
  .card-10 {
    flex: 1;
  }
  .card-11 {
    flex: 1;
  }
  .panel-title {
    padding: 15px 20px;
    font-size: 16px;
    font-weight: 500;
    color: #303133;
    border-bottom: 1px solid #e4e7ed;
    background-color: #fafafa;
  }
  .card-1 .panel-title {
    border-left: 4px solid #409eff;
  }
  .card-2 .panel-title {
    border-left: 4px solid #67c23a;
  }
  .card-3 .panel-title {
    border-left: 4px solid #e6a23c;
  }
  .card-4 .panel-title {
    border-left: 4px solid #f56c6c;
  }
  .card-5 .panel-title {
    border-left: 4px solid #409eff;
  }
  .card-6 .panel-title {
    border-left: 4px solid #67c23a;
  }
  .card-7 .panel-title {
    border-left: 4px solid #e6a23c;
  }
  .card-8 .panel-title {
    border-left: 4px solid #f56c6c;
  }
  .card-9 .panel-title {
    border-left: 4px solid #409eff;
  }
  .card-10 .panel-title {
    border-left: 4px solid #67c23a;
  }
  .card-11 .panel-title {
    border-left: 4px solid #e6a23c;
  }
  .chart-container {
    flex: 1;
    padding: 20px;
  }
  .table-container {
    flex: 1;
    padding: 20px;
    overflow: auto;
  }
  .stats-grid {
    flex: 1;
    padding: 15px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .stat-item {
    background-color: #fafafa;
    border-radius: 8px;
    padding: 15px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    border: 1px solid #e4e7ed;
    min-height: 80px;
    width: 100%;
  }
  .stat-value {
    font-size: 24px;
    font-weight: 600;
    color: #303133;
    margin-bottom: 5px;
  }
  .sales-volume-color {
    color: #409eff;
    text-shadow: 0 2px 4px rgba(64, 158, 255, 0.3);
  }
  .sales-amount-color {
    color: #67c23a;
    text-shadow: 0 2px 4px rgba(103, 194, 58, 0.3);
  }
  .new-customer-color {
    color: #e6a23c;
    text-shadow: 0 2px 4px rgba(230, 162, 60, 0.3);
  }
  .total-customer-color {
    color: #f56c6c;
    text-shadow: 0 2px 4px rgba(245, 108, 108, 0.3);
  }
  .stat-unit {
    font-size: 12px;
    color: #909399;
    margin-bottom: 3px;
  }
  .stat-change {
    font-size: 12px;
    color: #67c23a;
  }
  /* è¡¨æ ¼æ ·å¼ */
  :deep(.el-table) {
    border-radius: 8px;
    overflow: hidden;
  }
  :deep(.el-table th) {
    background-color: #fafafa;
    font-weight: 500;
  }
  :deep(.el-table tr:hover > td) {
    background-color: #ecf5ff;
  }
  .data-value {
    font-weight: bold;
    color: #409eff;
  }
  /* ä¸‹æ‹‰é€‰æ‹©æ¡†æ ·å¼ */
  :deep(.el-select) {
    width: 100%;
  }
  :deep(.el-date-picker) {
    width: 100%;
  }
</style>
src/views/reportAnalysis/solidWasteConsumption/index1.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,623 @@
<template>
  <div class="dashboard-container">
    <div class="data-dashboard">
      <!-- ç­›é€‰åŒºåŸŸ -->
      <div class="filter-area">
        <div class="filter-section">
          <span class="filter-label">时间维度:</span>
          <el-radio-group v-model="dateType"
                          @change="handleDateTypeChange"
                          class="radio-group">
            <el-radio-button label="month">月度</el-radio-button>
            <el-radio-button label="year">年度</el-radio-button>
          </el-radio-group>
        </div>
      </div>
      <!-- ä¸»è¦å†…容区域 -->
      <div class="dashboard-content">
        <!-- ç¬¬ä¸€è¡Œï¼šæ ¸å¿ƒæŒ‡æ ‡ -->
        <div class="row row-1">
          <div class="panel-card card-1">
            <div class="panel-title">核心指标</div>
            <div class="stats-grid">
              <div class="stat-item">
                <div class="stat-label">合计量</div>
                <div class="stat-value">{{ totalSolidWaste }}</div>
                <div class="stat-unit">吨</div>
              </div>
              <div class="stat-item">
                <div class="stat-label">2022年至今累计消纳量</div>
                <div class="stat-value">{{ totalSolidWasteSince2022 }}</div>
                <div class="stat-unit">吨</div>
              </div>
            </div>
          </div>
        </div>
        <!-- ç¬¬äºŒè¡Œï¼šå›ºåºŸæ¶ˆçº³è¶‹åŠ¿ -->
        <div class="row row-2">
          <div class="panel-card card-2">
            <div class="panel-title">固废消纳趋势</div>
            <div class="chart-container">
              <div ref="trendChart"
                   style="width: 100%; height: 100%"></div>
            </div>
          </div>
        </div>
        <!-- ç¬¬ä¸‰è¡Œï¼šå›ºåºŸç±»åž‹åˆ†å¸ƒ -->
        <div class="row row-3">
          <div class="panel-card card-3">
            <div class="panel-title">固废类型分布</div>
            <div class="chart-container">
              <div ref="distributionChart"
                   style="width: 100%; height: 100%"></div>
            </div>
          </div>
        </div>
        <!-- ç¬¬å››è¡Œï¼šæ¶ˆçº³é‡æ˜Žç»† -->
        <div class="row row-4">
          <div class="panel-card card-4">
            <div class="panel-title">消纳量明细</div>
            <div class="table-container">
              <el-table :data="wasteTableData"
                        style="width: 100%">
                <el-table-column prop="time"
                                 label="时间"
                                 width="120" />
                <el-table-column prop="type"
                                 label="固废类型"
                                 width="120"
                                 align="center">
                  <template #default="scope">
                    <el-tag :type="getWasteTypeType(scope.row.type)">
                      {{ scope.row.type }}
                    </el-tag>
                  </template>
                </el-table-column>
                <el-table-column prop="quantity"
                                 label="消纳量"
                                 align="right">
                  <template #default="scope">
                    <span class="data-value">{{ scope.row.quantity }}</span>
                  </template>
                </el-table-column>
                <el-table-column prop="unit"
                                 label="单位"
                                 width="80" />
                <el-table-column prop="source"
                                 label="来源" />
              </el-table>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
  import { ref, computed, onMounted, onBeforeUnmount, nextTick } from "vue";
  import * as echarts from "echarts";
  // ç­›é€‰æ¡ä»¶
  const dateType = ref("month"); // month æˆ– year
  // å›¾è¡¨å¼•用
  const trendChart = ref(null);
  const distributionChart = ref(null);
  // å›¾è¡¨å®žä¾‹
  let trendChartInstance = null;
  let distributionChartInstance = null;
  // æ¨¡æ‹Ÿæ•°æ®
  const solidWasteData = ref({
    month: [
      { name: "1月", ç²‰ç…¤ç°: 200, çŸ³è†: 150, çŸ³ç°: 100 },
      { name: "2月", ç²‰ç…¤ç°: 220, çŸ³è†: 160, çŸ³ç°: 110 },
      { name: "3月", ç²‰ç…¤ç°: 190, çŸ³è†: 140, çŸ³ç°: 95 },
      { name: "4月", ç²‰ç…¤ç°: 230, çŸ³è†: 170, çŸ³ç°: 115 },
      { name: "5月", ç²‰ç…¤ç°: 240, çŸ³è†: 180, çŸ³ç°: 120 },
      { name: "6月", ç²‰ç…¤ç°: 225, çŸ³è†: 165, çŸ³ç°: 112 },
    ],
    year: [
      { name: "2022", ç²‰ç…¤ç°: 2300, çŸ³è†: 1700, çŸ³ç°: 1100 },
      { name: "2023", ç²‰ç…¤ç°: 2500, çŸ³è†: 1800, çŸ³ç°: 1200 },
      { name: "2024", ç²‰ç…¤ç°: 2700, çŸ³è†: 1950, çŸ³ç°: 1300 },
      { name: "2025", ç²‰ç…¤ç°: 2900, çŸ³è†: 2100, çŸ³ç°: 1400 },
    ],
  });
  // è®¡ç®—属性
  const totalSolidWaste = computed(() => {
    const data = solidWasteData.value[dateType.value];
    if (dateType.value === "month") {
      return data.reduce(
        (sum, item) => sum + item.粉煤灰 + item.石膏 + item.石灰,
        0
      );
    } else {
      const lastItem = data[data.length - 1];
      return lastItem.粉煤灰 + lastItem.石膏 + lastItem.石灰;
    }
  });
  const totalSolidWasteSince2022 = computed(() => {
    const data = solidWasteData.value.year;
    return data.reduce(
      (sum, item) => sum + item.粉煤灰 + item.石膏 + item.石灰,
      0
    );
  });
  const wasteTableData = computed(() => {
    const data = solidWasteData.value[dateType.value];
    const result = [];
    data.forEach(item => {
      result.push({
        time: item.name,
        type: "粉煤灰",
        quantity: item.粉煤灰,
        unit: "吨",
        source: "生产过程",
      });
      result.push({
        time: item.name,
        type: "石膏",
        quantity: item.石膏,
        unit: "吨",
        source: "生产过程",
      });
      result.push({
        time: item.name,
        type: "石灰",
        quantity: item.石灰,
        unit: "吨",
        source: "生产过程",
      });
    });
    return result;
  });
  // å›¾è¡¨é…ç½®
  const trendChartOption = computed(() => {
    const data = solidWasteData.value[dateType.value];
    return {
      tooltip: {
        trigger: "axis",
        axisPointer: {
          type: "shadow",
        },
      },
      legend: {
        data: ["粉煤灰", "石膏", "石灰"],
        textStyle: {
          color: "#333",
        },
      },
      grid: {
        left: "3%",
        right: "4%",
        bottom: "3%",
        containLabel: true,
      },
      xAxis: {
        type: "category",
        data: data.map(item => item.name),
        axisLabel: {
          color: "#333",
        },
      },
      yAxis: {
        type: "value",
        name: "消纳量 (吨)",
        axisLabel: {
          color: "#333",
        },
      },
      series: [
        {
          name: "粉煤灰",
          type: "bar",
          data: data.map(item => item.粉煤灰),
          itemStyle: {
            color: "#909399",
          },
        },
        {
          name: "石膏",
          type: "bar",
          data: data.map(item => item.石膏),
          itemStyle: {
            color: "#E6A23C",
          },
        },
        {
          name: "石灰",
          type: "bar",
          data: data.map(item => item.石灰),
          itemStyle: {
            color: "#F56C6C",
          },
        },
      ],
    };
  });
  const distributionChartOption = computed(() => {
    const data = solidWasteData.value[dateType.value];
    const lastItem = data[data.length - 1];
    return {
      tooltip: {
        trigger: "item",
        formatter: "{a} <br/>{b}: {c} ({d}%)",
      },
      legend: {
        orient: "vertical",
        left: "left",
        textStyle: {
          color: "#333",
        },
      },
      series: [
        {
          name: "固废类型",
          type: "pie",
          radius: "60%",
          center: ["50%", "50%"],
          data: [
            { value: lastItem.粉煤灰, name: "粉煤灰" },
            { value: lastItem.石膏, name: "石膏" },
            { value: lastItem.石灰, name: "石灰" },
          ],
          emphasis: {
            itemStyle: {
              shadowBlur: 10,
              shadowOffsetX: 0,
              shadowColor: "rgba(0, 0, 0, 0.5)",
            },
          },
          itemStyle: {
            color: function (params) {
              const colors = ["#909399", "#E6A23C", "#F56C6C"];
              return colors[params.dataIndex];
            },
          },
        },
      ],
    };
  });
  // äº‹ä»¶å¤„理
  const handleDateTypeChange = () => {
    updateCharts();
  };
  // åˆå§‹åŒ–图表
  const initCharts = () => {
    if (trendChart.value) {
      trendChartInstance = echarts.init(trendChart.value);
      trendChartInstance.setOption(trendChartOption.value);
    }
    if (distributionChart.value) {
      distributionChartInstance = echarts.init(distributionChart.value);
      distributionChartInstance.setOption(distributionChartOption.value);
    }
  };
  // æ›´æ–°å›¾è¡¨
  const updateCharts = () => {
    if (trendChartInstance) {
      trendChartInstance.setOption(trendChartOption.value);
    }
    if (distributionChartInstance) {
      distributionChartInstance.setOption(distributionChartOption.value);
    }
  };
  // è°ƒæ•´å›¾è¡¨å¤§å°
  const resizeCharts = () => {
    trendChartInstance?.resize();
    distributionChartInstance?.resize();
  };
  // çª—口大小变化处理
  const handleResize = () => {
    // å»¶è¿Ÿæ‰§è¡Œï¼Œç¡®ä¿DOM更新完成
    setTimeout(() => {
      resizeCharts();
    }, 100);
  };
  // èŽ·å–å›ºåºŸç±»åž‹æ ‡ç­¾ç±»åž‹
  const getWasteTypeType = type => {
    const typeMap = {
      ç²‰ç…¤ç°: "info",
      çŸ³è†: "warning",
      çŸ³ç°: "danger",
    };
    return typeMap[type] || "info";
  };
  // ç”Ÿå‘½å‘¨æœŸé’©å­
  onMounted(() => {
    // ä½¿ç”¨nextTick确保DOM完全渲染后再初始化
    nextTick(() => {
      // åˆå§‹åŒ–图表
      initCharts();
    });
    window.addEventListener("resize", handleResize);
  });
  onBeforeUnmount(() => {
    window.removeEventListener("resize", handleResize);
    // é”€æ¯å›¾è¡¨å®žä¾‹
    trendChartInstance?.dispose();
    distributionChartInstance?.dispose();
  });
</script>
<style scoped>
  /* å¤–部容器 - å æ®æ•´ä¸ªè§†å£ */
  .dashboard-container {
    position: relative;
    width: 100%;
    /* é¡µé¢åœ¨å¸¸è§„布局下(有顶栏)默认减去 84px,避免内容被裁切 */
    min-height: calc(100vh - 84px);
    background-color: #f5f7fa;
    overflow: hidden;
  }
  /* å†…部内容区域 - è‡ªé€‚应宽度 */
  .data-dashboard {
    position: relative;
    width: 100%;
    min-height: 100%;
    background-color: #ffffff;
    box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
  }
  .filter-area {
    padding: 20px;
    background-color: #ffffff;
    border-bottom: 1px solid #e4e7ed;
    display: flex;
    gap: 40px;
    align-items: center;
    flex-wrap: wrap;
  }
  .filter-section {
    display: flex;
    align-items: center;
    gap: 10px;
  }
  .filter-label {
    font-size: 14px;
    font-weight: 500;
    color: #303133;
    white-space: nowrap;
  }
  .radio-group {
    display: flex;
    align-items: center;
  }
  /* æŒ‰é’®æ ·å¼ */
  :deep(.el-radio-button__inner) {
    border-radius: 4px;
    padding: 8px 20px;
    font-size: 14px;
    transition: all 0.3s ease;
  }
  :deep(.el-radio-button__orig-radio:checked + .el-radio-button__inner) {
    background-color: #409eff;
    border-color: #409eff;
    color: #ffffff;
    box-shadow: 0 2px 4px rgba(64, 158, 255, 0.3);
  }
  :deep(.el-radio-button__inner:hover) {
    color: #409eff;
    border-color: #c6e2ff;
  }
  :deep(.el-radio-button:first-child .el-radio-button__inner) {
    border-radius: 4px 0 0 4px;
  }
  :deep(.el-radio-button:last-child .el-radio-button__inner) {
    border-radius: 0 4px 4px 0;
  }
  .dashboard-content {
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    gap: 20px;
    padding: 20px;
    min-height: 800px;
    overflow: hidden;
  }
  /* è¡Œå¸ƒå±€ */
  .row {
    display: flex;
    gap: 20px;
    align-items: stretch;
  }
  /* ç¬¬ä¸€è¡Œï¼šæ ¸å¿ƒæŒ‡æ ‡ */
  .row-1 {
    height: 250px;
  }
  /* ç¬¬äºŒè¡Œï¼šå›ºåºŸæ¶ˆçº³è¶‹åŠ¿ */
  .row-2 {
    height: 300px;
  }
  /* ç¬¬ä¸‰è¡Œï¼šå›ºåºŸç±»åž‹åˆ†å¸ƒ */
  .row-3 {
    height: 300px;
  }
  /* ç¬¬å››è¡Œï¼šæ¶ˆçº³é‡æ˜Žç»† */
  .row-4 {
    min-height: 250px;
  }
  /* å¡ç‰‡æ ·å¼ */
  .panel-card {
    background-color: #ffffff;
    border-radius: 8px;
    border: 1px solid #e4e7ed;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
    transition: all 0.3s ease;
  }
  .panel-card:hover {
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
    transform: translateY(-2px);
  }
  /* å¡ç‰‡å¸ƒå±€ */
  .card-1 {
    flex: 1;
  }
  .card-2 {
    flex: 1;
  }
  .card-3 {
    flex: 1;
  }
  .card-4 {
    flex: 1;
  }
  .panel-title {
    padding: 15px 20px;
    font-size: 16px;
    font-weight: 500;
    color: #303133;
    border-bottom: 1px solid #e4e7ed;
    background-color: #fafafa;
  }
  .card-1 .panel-title {
    border-left: 4px solid #409eff;
  }
  .card-2 .panel-title {
    border-left: 4px solid #f56c6c;
  }
  .card-3 .panel-title {
    border-left: 4px solid #e6a23c;
  }
  .card-4 .panel-title {
    border-left: 4px solid #67c23a;
  }
  .chart-container {
    flex: 1;
    padding: 20px;
  }
  .table-container {
    flex: 1;
    padding: 20px;
    overflow: auto;
  }
  .stats-grid {
    flex: 1;
    padding: 15px;
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 15px;
  }
  .stat-item {
    background-color: #ffffff;
    border-radius: 12px;
    padding: 25px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    border: 2px solid #e4e7ed;
    min-height: 120px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
    transition: all 0.3s ease;
  }
  .stat-item:hover {
    box-shadow: 0 8px 24px rgba(64, 158, 255, 0.2);
    border-color: #409eff;
    transform: translateY(-4px);
  }
  .stat-label {
    font-size: 14px;
    font-weight: 500;
    color: #303133;
    margin-bottom: 10px;
  }
  .stat-value {
    font-size: 32px;
    font-weight: 700;
    color: #409eff;
    margin-bottom: 5px;
    text-shadow: 0 2px 4px rgba(64, 158, 255, 0.3);
    transition: all 0.3s ease;
  }
  .stat-value:hover {
    transform: scale(1.05);
  }
  .stat-unit {
    font-size: 12px;
    font-weight: 500;
    color: #606266;
  }
  /* è¡¨æ ¼æ ·å¼ */
  :deep(.el-table) {
    border-radius: 8px;
    overflow: hidden;
  }
  :deep(.el-table th) {
    background-color: #fafafa;
    font-weight: 500;
  }
  :deep(.el-table tr:hover > td) {
    background-color: #ecf5ff;
  }
  .data-value {
    font-weight: bold;
    color: #409eff;
  }
</style>