zhang_12370
2025-07-23 2c9fbc6f1a3ccd1418efa9c348ec12faf1ab3258
src/views/calculator/index.vue
@@ -403,7 +403,9 @@
            <!-- 混合煤属性 -->
            <div class="props-section">
              <div class="props-title">📊 混合煤属性</div>
              <div class="props-grid">
              <!-- 基础属性展示区 -->
              <div class="props-display-grid">
                <div class="prop-item">
                  <span class="prop-label">发热量:</span>
                  <span class="prop-value"
@@ -434,19 +436,45 @@
                    >{{ result.optimal.props.cost.toFixed(2) }} 元/吨</span
                  >
                </div>
                <div class="prop-item">
                  <span class="prop-label">生成:</span>
                  <el-autocomplete
                    v-model="result.optimal.props.createCoal"
                    :fetch-suggestions="querySearch"
                    clearable
                    size="small"
                    class="inline-input red-border"
                    style="width: 180px; min-height: 24px !important"
                    placeholder="请输入生成煤种"
                    @blur="handleSelect($event)"
                    @select="handleSelect($event)"
                  />
              </div>
              <!-- 输入操作区 -->
              <div class="props-input-section">
                <div class="input-row">
                  <div class="input-item">
                    <label class="input-label">生成煤种:</label>
                    <el-autocomplete
                      v-model="result.optimal.props.createCoal"
                      :fetch-suggestions="querySearch"
                      clearable
                      size="small"
                      class="coal-input"
                      placeholder="请输入/选择生成煤种"
                      @blur="handleSelect($event)"
                      @select="handleSelect($event)"
                    />
                  </div>
                  <div class="input-item">
                    <label class="input-label">生产数量:</label>
                    <el-input
                      type="number"
                      clearable
                      size="small"
                      class="quantity-input"
                      v-model="result.optimal.props.createCoalQuantity"
                      placeholder="请输入生产数量"
                      :min="0"
                      :max="999999"
                      :step="0.1"
                      :precision="2"
                      @input="handleQuantityInput"
                      @blur="handleQuantityBlur"
                    >
                      <template #suffix>
                        <span style="color: #909399; font-size: 12px;">吨</span>
                      </template>
                    </el-input>
                  </div>
                </div>
              </div>
            </div>
@@ -581,92 +609,98 @@
const handleCoalSelectChange = (index, coalId) => {
  const selectedCoal = infoCoals.value.find(item => item.key === coalId);
  
  if (selectedCoal && selectedCoal.item && selectedCoal.item.coalValues) {
    console.log('选中的煤种:', selectedCoal);
    // 安全地获取各项煤质参数
    const getCoalValue = (fieldName) => {
      const coalValue = selectedCoal.item.coalValues.find(
        (value) => value.fieldName === fieldName
      );
      return coalValue ? coalValue.coalValue : '';
    };
    // 更新表单数据
    data.coalForms[index].price = selectedCoal.item.priceExcludingTax || 0;
    data.coalForms[index].cv = getCoalValue("发热量") || 0;
    data.coalForms[index].sulfur = getCoalValue("硫分") || 0;
    data.coalForms[index].ash = getCoalValue("灰分") || 0;
    data.coalForms[index].moisture = getCoalValue("水分") || 0;
    // 设置价格(如果有的话)
    if (selectedCoal.item.priceExcludingTax) {
      data.coalForms[index].price = selectedCoal.item.priceExcludingTax;
    }
    console.log('更新后的表单数据:', data.coalForms[index]);
  } else {
  if (!selectedCoal?.item?.coalValues) {
    console.warn('未找到选中的煤种数据或数据格式不正确');
    return;
  }
  // 获取煤质参数的通用函数
  const getCoalValue = (fieldName) => {
    return selectedCoal.item.coalValues.find(
      value => value.fieldName === fieldName
    )?.coalValue || 0;
  };
  // 批量更新表单数据
  Object.assign(data.coalForms[index], {
    price: selectedCoal.item.priceExcludingTax || 0,
    cv: getCoalValue("发热量"),
    sulfur: getCoalValue("硫分"),
    ash: getCoalValue("灰分"),
    moisture: getCoalValue("水分")
  });
};
const coalInfoList = ref([]);
// onMounted
// 获取煤种信息
const getCoalInfo = async () => {
  let result = await getCoalInfoList();
  if (result.code === 200) {
    result.data.forEach((item) => {
      let obj = {
  try {
    const result = await getCoalInfoList();
    if (result.code === 200) {
      coalInfoList.value = result.data.map(item => ({
        value: item.coal,
        key: item.id,
      };
      coalInfoList.value.push(obj);
    });
  } else {
      }));
    } else {
      ElMessage.error("获取煤种信息失败,请稍后重试");
    }
  } catch (error) {
    ElMessage.error("获取煤种信息失败,请稍后重试");
  }
};
// 表格展示用:优先用 key 匹配中文名,再用 value 匹配,最后原样返回
// 表格展示用:匹配煤种名称
const matchCoalName = (name) => {
  if (
    !name ||
    !Array.isArray(coalInfoList.value) ||
    coalInfoList.value.length === 0
  )
    return name;
  // key 匹配
  const byKey = coalInfoList.value.find(
    (item) => String(item.key) === String(name)
  if (!name || !coalInfoList.value?.length) return name;
  // 优先按 key 匹配,再按 value 匹配
  const foundCoal = coalInfoList.value.find(item =>
    String(item.key) === String(name) || item.value === name
  );
  if (byKey) return byKey.value;
  // value 匹配
  const byValue = coalInfoList.value.find((item) => item.value === name);
  if (byValue) return byValue.value;
  // 原样返回
  return name;
  return foundCoal?.value || name;
};
// 自动补全搜索
const querySearch = (q, cb) => {
  const res = q
    ? coalInfoList.value.filter((c) => c.value.includes(q))
  const results = q
    ? coalInfoList.value.filter(c => c.value.includes(q))
    : coalInfoList.value;
  cb(res);
  cb(results);
};
// 选择/失焦时,优先存 key,找不到则存原值
// 选择/失焦处理
const handleSelect = (item) => {
  const val = item.value || (item.target && item.target.value) || "";
  const found = coalInfoList.value.find(
    (c) => c.value === val || c.key === val
  );
  const val = item.value || item.target?.value || "";
  const found = coalInfoList.value.find(c => c.value === val || c.key === val);
  result.value.optimal.props.createCoal = found ? found.key : val;
  // 新增:如果匹配成功,留一个id字段
  if (found) {
    result.value.optimal.props.coalId = found.key;
  } else {
    result.value.optimal.props.coalId = null;
  result.value.optimal.props.coalId = found?.key || null;
  // 更新显示名称
  const matchedName = matchCoalName(result.value.optimal.props.createCoal);
  if (matchedName !== result.value.optimal.props.createCoal) {
    result.value.optimal.props.createCoal = matchedName;
  }
  let match = matchCoalName(result.value.optimal.props.createCoal);
  if (match && match !== result.value.optimal.props.createCoal) {
    result.value.optimal.props.createCoal = match;
};
// 处理数量输入
const handleQuantityInput = (value) => {
  // 限制输入范围和精度
  if (value !== null && value !== undefined && value !== '') {
    const numValue = parseFloat(value);
    if (numValue < 0) {
      result.value.optimal.props.createCoalQuantity = 0;
    } else if (numValue > 999999) {
      result.value.optimal.props.createCoalQuantity = 999999;
    }
  }
};
// 处理数量失焦
const handleQuantityBlur = (event) => {
  const value = event.target.value;
  if (value && !isNaN(value)) {
    // 格式化为两位小数
    const formatted = parseFloat(value).toFixed(2);
    result.value.optimal.props.createCoalQuantity = formatted;
  }
};
onMounted(async () => {
@@ -676,67 +710,51 @@
const infoCoals = ref([]);
// 初始化煤种字段
const geInfoCoals = async () => {
  let res = await getCoalBlendingList();
  if (res.code === 200) {
    infoCoals.value = res.data.map((item) => ({
      value: item.supplierCoal,
      key: item.coalId,
      item,
    }));
    console.log(infoCoals.value);
  } else {
  try {
    const res = await getCoalBlendingList();
    if (res.code === 200) {
      console.log("获取煤种信息成功", res.data);
      infoCoals.value = res.data.map(item => ({
        value: item.supplierCoal,
        key: item.coalId,
        item,
      }));
    } else {
      ElMessage.error("获取煤种信息失败,请稍后重试");
    }
  } catch (error) {
    ElMessage.error("获取煤种信息失败,请稍后重试");
  }
};
// 线性规划求解函数
const solveBlend = (coals, constraints) => {
  // 数据验证
  if (constraints.maxSulfur) {
    let missingSulfur = coals.some((coal) => !coal.sulfur && coal.sulfur !== 0);
    if (missingSulfur) {
      throw new Error(
        "如果设置了最大硫分约束,则所有参与配比的煤种都必须提供硫分数据。"
      );
  const validateConstraint = (constraintValue, fieldName, coalField) => {
    if (constraintValue && coals.some(coal => !coal[coalField] && coal[coalField] !== 0)) {
      throw new Error(`如果设置了${fieldName}约束,则所有煤种都必须提供${fieldName}数据。`);
    }
  }
  if (constraints.maxAsh) {
    let missingAsh = coals.some((coal) => !coal.ash && coal.ash !== 0);
    if (missingAsh) {
      throw new Error("如果设置了最大灰分约束,则所有煤种都必须提供灰分数据。");
    }
  }
  if (constraints.maxMoisture) {
    let missingMoisture = coals.some(
      (coal) => !coal.moisture && coal.moisture !== 0
    );
    if (missingMoisture) {
      throw new Error("如果设置了最大水分约束,则所有煤种都必须提供水分数据。");
    }
  }
  };
  // 简单的线性规划求解(最小化成本)
  // 这里使用简化的算法,实际项目中可以集成更专业的求解器
  validateConstraint(constraints.maxSulfur, '最大硫分', 'sulfur');
  validateConstraint(constraints.maxAsh, '最大灰分', 'ash');
  validateConstraint(constraints.maxMoisture, '最大水分', 'moisture');
  try {
    // 模拟求解过程
    let totalCoals = coals.length;
    let ratios = new Array(totalCoals).fill(0);
    // 简单的等权重分配作为初始解
    let avgRatio = 1 / totalCoals;
    ratios = ratios.map(() => avgRatio);
    let ratios = new Array(coals.length).fill(1 / coals.length);
    // 验证约束条件
    // 验证约束条件并调整配比
    let blendProps = calcBlendProps(coals, ratios);
    if (constraints.minCalorific && blendProps.cv < constraints.minCalorific) {
      // 调整配比以满足最低发热量
      let highCvCoals = coals
      // 优先使用高发热量煤种
      const sortedCoals = coals
        .map((coal, i) => ({ index: i, cv: coal.cv }))
        .sort((a, b) => b.cv - a.cv);
      ratios = new Array(totalCoals).fill(0);
      ratios[highCvCoals[0].index] = 0.6;
      ratios[highCvCoals[1] ? highCvCoals[1].index : 0] = 0.4;
      ratios = new Array(coals.length).fill(0);
      ratios[sortedCoals[0].index] = 0.6;
      if (sortedCoals[1]) ratios[sortedCoals[1].index] = 0.4;
    }
    return ratios;
@@ -830,77 +848,60 @@
  ElMessage.success("表单已重置");
};
const addWarehoused = () => {
  // 验证前置条件
  const validationChecks = [
    { condition: !result.value, message: "请先计算最优配比后再添加至待入库" },
    { condition: !result.value.optimal, message: "请先计算最优配比" },
    { condition: !result.value.optimal.props.createCoal, message: "请先选择生成煤种" },
    { condition: !result.value.optimal.props.createCoalQuantity || result.value.optimal.props.createCoalQuantity <= 0, message: "请输入有效的生产数量" }
  ];
  if (!result.value) {
    ElMessage.error("请先计算最优配比后再添加至待入库");
    return;
  }
  if (result.value.optimal === null) {
    ElMessage.error("请先计算最优配比");
    return;
  }
  if (!result.value.optimal.props.createCoal) {
    ElMessage.error("请先选择生成煤种");
    return;
  }
  const coals = result.value.optimal.instructions.map((item) => item.coalId);
  let allFound = true;
  for (const element of coals) {
    let found = false;
    for (const item of coalInfoList.value) {
      if (item.key === element) {
        found = true;
        break;
      }
    }
    if (!found) {
      allFound = false;
      break;
  for (const check of validationChecks) {
    if (check.condition) {
      ElMessage.error(check.message);
      return;
    }
  }
  if (!allFound) {
  // 验证配比中的煤种
  const coals = result.value.optimal.instructions.map(item => item.coalId);
  const allCoalsFound = coals.every(coalId =>
    coalInfoList.value.some(item => item.key === coalId)
  );
  if (!allCoalsFound) {
    ElMessage.error("配比中包含未知煤种,请先添加至煤种列表");
    return;
  }
  let createCoalFound = false;
  for (const item of coalInfoList.value) {
    if (item.key === result.value.optimal.props.coalId) {
      createCoalFound = true;
      break;
    }
  }
  if (!createCoalFound) {
    ElMessage.warning("生成煤种是未知煤种,无法添加至待入库");
    return;
  }
  // result.value.optimal.props fieldsResultList
  // result.value.optimal.instructions 表格数据coalResultList
  console.log("添加至待入库数据:", result.value.optimal);
  // cost保留两位小数
  result.value.optimal.props.totalTonnage = formInline.value.totalTonnage;
  result.value.optimal.instructions.forEach((item) => {
    item.officialId = item.coalId; // 添加官方ID字段
  });
  result.value.optimal.props.cost = parseFloat(
    result.value.optimal.props.cost.toFixed(2)
  // 验证生成煤种
  const createCoalExists = coalInfoList.value.some(
    item => item.key === result.value.optimal.props.coalId
  );
  result.value.optimal.props.totalTonnage = formInline.value.totalTonnage;
  let arr = [result.value.optimal.props, [...result.value.optimal.instructions]];
  let params = {
    fieldsResultList: arr[0],
    coalResultList: arr[1],
  // 准备数据
  const optimalData = result.value.optimal;
  optimalData.props.cost = parseFloat(optimalData.props.cost.toFixed(2));
  // 添加官方ID
  optimalData.instructions.forEach(item => {
    item.officialId = item.coalId;
  });
  const params = {
    fieldsResultList: optimalData.props,
    coalResultList: optimalData.instructions,
  };
 addPendingInventory(params).then((res)=>{
   if(res.code === 200 && res.data== true){
     ElMessage.success("添加至待入库成功");
   }
 })
  addPendingInventory(params).then(res => {
    if (res.code === 200 && res.data === true) {
      ElMessage.success("添加至待入库成功");
    }
  });
};
const submitForm = () => {
  console.log("煤种信息:", coalForms.value);
  // 数据验证
  let validCoals = coalForms.value.filter(
    (coal) => coal.coalId && coal.cv && coal.price
  const validCoals = coalForms.value.filter(
    coal => coal.coalId && coal.cv && coal.price
  );
  if (validCoals.length < 2) {
@@ -910,7 +911,7 @@
  try {
    // 求解最优配比
    let ratios = solveBlend(validCoals, constraints.value);
    const ratios = solveBlend(validCoals, constraints.value);
    if (!ratios) {
      data.result.error = "无可行解,请检查约束条件或煤种数据";
      data.result.show = true;
@@ -918,21 +919,21 @@
    }
    // 计算结果
    let props = calcBlendProps(validCoals, ratios);
    let instructions = genInstructions(
    const props = calcBlendProps(validCoals, ratios);
    const instructions = genInstructions(
      validCoals,
      ratios,
      formInline.value.totalTonnage,
      formInline.value.scoopWeight
    );
    // 初始化扩展属性
    props.createCoal = "";
    props.createCoalQuantity = 0;
    props.coalId = null;
    data.result = {
      show: true,
      optimal: {
        ratios,
        props,
        instructions,
      },
      optimal: { ratios, props, instructions },
      alternatives: [],
      error: null,
    };
@@ -950,57 +951,42 @@
const generateAlternatives = (coals) => {
  const altList = [
    {
      desc: "发热量降1%",
      mod: { minCalorific: constraints.value.minCalorific * 0.99 },
    },
    {
      desc: "发热量降2%",
      mod: { minCalorific: constraints.value.minCalorific * 0.98 },
    },
    {
      desc: "硫分升1%",
      mod: { maxSulfur: constraints.value.maxSulfur * 1.01 },
    },
    {
      desc: "硫分升2%",
      mod: { maxSulfur: constraints.value.maxSulfur * 1.02 },
    },
    {
      desc: "发热量降0.5%且硫分升0.5%",
    { desc: "发热量降1%", mod: { minCalorific: constraints.value.minCalorific * 0.99 } },
    { desc: "发热量降2%", mod: { minCalorific: constraints.value.minCalorific * 0.98 } },
    { desc: "硫分升1%", mod: { maxSulfur: constraints.value.maxSulfur * 1.01 } },
    { desc: "硫分升2%", mod: { maxSulfur: constraints.value.maxSulfur * 1.02 } },
    {
      desc: "发热量降0.5%且硫分升0.5%",
      mod: {
        minCalorific: constraints.value.minCalorific * 0.995,
        maxSulfur: constraints.value.maxSulfur * 1.005,
      },
      }
    },
  ];
  data.result.alternatives = [];
  for (let alt of altList) {
  data.result.alternatives = altList.reduce((alternatives, alt) => {
    try {
      let altConstraints = Object.assign({}, constraints.value, alt.mod);
      let altRatios = solveBlend(coals, altConstraints);
      if (!altRatios) continue;
      const altConstraints = { ...constraints.value, ...alt.mod };
      const altRatios = solveBlend(coals, altConstraints);
      if (altRatios) {
        const altProps = calcBlendProps(coals, altRatios);
        const altInstructions = genInstructions(
          coals, altRatios, formInline.value.scoopWeight
        );
      let altProps = calcBlendProps(coals, altRatios);
      let altInstructions = genInstructions(
        coals,
        altRatios,
        formInline.value.totalTonnage,
        formInline.value.scoopWeight
      );
      data.result.alternatives.push({
        desc: alt.desc,
        ratios: altRatios,
        props: altProps,
        instructions: altInstructions,
      });
        alternatives.push({
          desc: alt.desc,
          ratios: altRatios,
          props: altProps,
          instructions: altInstructions,
        });
      }
    } catch (error) {
      console.warn(`备选方案 ${alt.desc} 计算失败:`, error);
    }
  }
    return alternatives;
  }, []);
};
const { formInline, constraints, coalForms, result } = toRefs(data);
@@ -1012,29 +998,26 @@
    return;
  }
  // 如果当前数组长度大于所需数量,截断
  // 截断多余的或填充不足的煤种
  if (coalForms.value.length > num) {
    coalForms.value = coalForms.value.slice(0, num);
    return;
  }
  // 否则,填充新的空对象
  while (coalForms.value.length < num) {
    coalForms.value.push({
      type: "未知煤",
      coalId: `煤${String.fromCharCode(65 + coalForms.value.length)}`,
      cv: 0,
      price: 0,
      sulfur: "",
      ash: "",
      moisture: "",
    });
  } else {
    while (coalForms.value.length < num) {
      coalForms.value.push({
        type: "未知煤",
        coalId: `煤${String.fromCharCode(65 + coalForms.value.length)}`,
        cv: 0,
        price: 0,
        sulfur: "",
        ash: "",
        moisture: "",
      });
    }
  }
};
// 处理煤种类型变化
const handleCoalTypeChange = (index) => {
  // 当煤种类型改变时,清空煤种名称,避免数据混乱
  coalForms.value[index].coalId = "";
};
</script>
@@ -1258,6 +1241,66 @@
  margin-bottom: 10px;
}
/* 基础属性展示网格 */
.props-display-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 8px;
  margin-bottom: 15px;
}
/* 输入操作区域 */
.props-input-section {
  border-top: 1px solid #ebeef5;
  padding-top: 15px;
}
.input-row {
  display: flex;
  gap: 15px;
  align-items: flex-end;
  flex-wrap: wrap;
}
.input-item {
  flex: 1;
  min-width: 200px;
  display: flex;
  flex-direction: column;
}
.input-label {
  font-size: 13px;
  color: #606266;
  margin-bottom: 6px;
  font-weight: 500;
}
.coal-input,
.quantity-input {
  width: 100%;
  border-radius: 4px;
}
.coal-input :deep(.el-input__wrapper),
.quantity-input :deep(.el-input__wrapper) {
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  transition: all 0.2s;
  min-height: 32px;
}
.coal-input :deep(.el-input__wrapper):hover,
.quantity-input :deep(.el-input__wrapper):hover {
  border-color: #c0c4cc;
}
.coal-input :deep(.el-input__wrapper.is-focus),
.quantity-input :deep(.el-input__wrapper.is-focus) {
  border-color: #409eff;
  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
.props-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
@@ -1418,8 +1461,18 @@
}
@media (max-width: 768px) {
  .props-grid {
  .props-display-grid {
    grid-template-columns: 1fr;
    gap: 6px;
  }
  .input-row {
    flex-direction: column;
    gap: 12px;
  }
  .input-item {
    min-width: 100%;
  }
  .table-container {
@@ -1429,6 +1482,21 @@
  :deep(.el-table .cell) {
    padding: 4px 8px;
  }
  .right-card {
    width: 100%;
    min-width: auto;
  }
}
@media (max-width: 1024px) {
  .props-display-grid {
    grid-template-columns: repeat(2, 1fr);
  }
  .input-row {
    gap: 12px;
  }
}
:deep(.el-input__wrapper) {
  min-height: 24px !important;
@@ -1436,4 +1504,22 @@
:deep(.el-input__inner) {
  min-height: 24px !important;
}
/* 数量输入框样式优化 */
.coal-input :deep(.el-input__suffix),
.quantity-input :deep(.el-input__suffix) {
  display: flex;
  align-items: center;
  padding-right: 8px;
}
/* 输入框的后缀单位样式 */
.coal-input :deep(.el-input__suffix-inner),
.quantity-input :deep(.el-input__suffix-inner) {
  display: flex;
  align-items: center;
  font-style: normal;
  color: #909399;
  font-size: 12px;
}
</style>