gaoluyang
2025-11-28 1ab015896fcf64ba99c5271837169a342109adae
src/views/productionManagement/productionDispatching/index.vue
@@ -1,5 +1,32 @@
<template>
   <div class="app-container">
      <!-- 炒机1-4 展示(总量 / 正在生产量 / 空余量) -->
      <div class="machines-grid">
         <div v-for="machine in machines" :key="machine.id" class="machine-card">
            <div class="machine-title">{{ machine.name }}</div>
            <div class="machine-metrics">
            <div class="machine-control">
               <span>总量(kg):</span>
               <el-input-number v-model="machineData[machine.name].workLoad" :min="0" :step="1" size="small" />
            </div>
            <div><span> 预计投入量(kg):</span><span>{{ machineData[machine.name].currentWorkLoad }}</span></div>
            <div><span>空余工作量(kg):</span><span>{{ machineData[machine.name].vacant }}</span></div>
         </div>
         </div>
         <div class="save-button-container">
            <div class="loss-rate-container">
               <span class="loss-rate-label">损耗率(%):</span>
               <el-select v-model="rate" placeholder="请选择损耗率" style="width: 120px" size="small">
                  <el-option label="6" :value="6" />
                  <el-option label="7" :value="7" />
                  <el-option label="8" :value="8" />
                  <el-option label="9" :value="9" />
                  <el-option label="10" :value="10" />
               </el-select>
            </div>
            <el-button type="primary" @click="saveMachineTotals" size="small">保存设置</el-button>
         </div>
      </div>
      <div class="search_form">
         <div>
            <span class="search_title">客户名称:</span>
@@ -22,12 +49,14 @@
            />
            <span class="search_title ml10">录入日期:</span>
            <el-date-picker v-model="searchForm.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
                                    placeholder="请选择" clearable @change="changeDaterange" />
                              placeholder="请选择" clearable @change="changeDaterange" />
            <el-button type="primary" @click="handleQuery" style="margin-left: 10px">搜索</el-button>
         </div>
         <div>
            <el-button type="primary" @click="openForm('add')">生产派工</el-button>
         </div>
         <el-button type="primary" @click="openForm('add')">生产派工</el-button>
         <el-button type="success" @click="openAutoDispatch">自动派工</el-button>
         <el-button @click="handleOut">导出</el-button>
      </div>
      </div>
      <div class="table_list">
         <PIMTable
@@ -43,34 +72,30 @@
         ></PIMTable>
      </div>
      <form-dia ref="formDia" @close="handleQuery"></form-dia>
      <auto-dispatch-dia ref="autoDispatchDia" @close="handleQuery"></auto-dispatch-dia>
   </div>
</template>
<script setup>
import {onMounted, ref} from "vue";
import {onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick, computed, watch} from "vue";
import FormDia from "@/views/productionManagement/productionDispatching/components/formDia.vue";
import AutoDispatchDia from "@/views/productionManagement/productionDispatching/components/autoDispatchDia.vue";
import dayjs from "dayjs";
import {schedulingListPage} from "@/api/productionManagement/productionOrder.js";
import {schedulingListPage, schedulingList, addSpeculatTrading, updateSpeculatTrading, getLossRate, addLossRate, updateLossRate} from "@/api/productionManagement/productionOrder.js";
import { ElMessageBox } from "element-plus";
const data = reactive({
   searchForm: {
      customerName: "",
      projectName: "",
      entryDate: [
         dayjs().format("YYYY-MM-DD"),
         dayjs().add(1, "day").format("YYYY-MM-DD"),
      ], // 录入日期
      status: "",
      entryDate: [dayjs().format("YYYY-MM-DD"), dayjs().format("YYYY-MM-DD")], // 录入日期,默认当天
      entryDateStart: dayjs().format("YYYY-MM-DD"),
      entryDateEnd: dayjs().add(1, "day").format("YYYY-MM-DD"),
      entryDateEnd: dayjs().format("YYYY-MM-DD"),
   },
});
const { searchForm } = toRefs(data);
const tableColumn = ref([
   {
      label: "录入日期",
      prop: "entryDate",
      width: 120,
   },
   {
      label: "合同号",
      prop: "salesContractNo",
@@ -99,12 +124,22 @@
   {
      label: "规格型号",
      prop: "specificationModel",
      width: 120,
   },
   {
      label: "绑定机器",
      prop: "speculativeTradingName",
      width: 220,
   },
   {
      label: "单位",
      prop: "unit",
      width:90
   },
   {
      label: "录入日期",
      prop: "entryDate",
      width: 120,
   },
   {
      label: "数量",
@@ -130,7 +165,140 @@
   total: 0,
});
const formDia = ref()
const autoDispatchDia = ref()
const { proxy } = getCurrentInstance()
// 炒机数据
const machineData = reactive({
   "炒机1": { workLoad: 0, currentWorkLoad: 0, vacant: 0 },
   "炒机2": { workLoad: 0, currentWorkLoad: 0, vacant: 0 },
   "炒机3": { workLoad: 0, currentWorkLoad: 0, vacant: 0 },
   "炒机4": { workLoad: 0, currentWorkLoad: 0, vacant: 0 }
})
// 炒机配置数组
const machines = [
   { id: 1, name: '炒机1' },
   { id: 2, name: '炒机2' },
   { id: 3, name: '炒机3' },
   { id: 4, name: '炒机4' }
]
// 保存炒机总量设置
const saveMachineTotals = () => {
   // 验证损耗率是否已选择
   if (rate.value === null || rate.value === undefined || isNaN(rate.value)) {
      proxy.$message.warning('请选择损耗率');
      return;
   }
   // 构造保存数据数组,使用machines数组循环构建
   const saveData = machines.map(machine => {
      const saveItem = {
         name: machine.name, // 炒机名称
         workLoad: machineData[machine.name].workLoad,  // 总量
         currentWorkLoad: machineData[machine.name].currentWorkLoad, //  预计投入量
         vacant: machineData[machine.name].vacant   // 空余量
      };
      // 如果是修改操作,需要传递id字段
      if (hasQueryData.value) {
         const queryData = getMachineQueryData(machine.id);
         if (queryData && queryData.id) {
            saveItem.id = queryData.id;
         }
      }
      return saveItem;
   });
   // 构造损耗率数据
   const rateData = {
      rate: rate.value
   };
   // 如果有ID,说明是修改操作
   if (rateId.value) {
      rateData.id = rateId.value;
   }
   // 根据是否有查询数据决定调用新增接口还是修改接口
   const saveApi = hasQueryData.value ? updateSpeculatTrading : addSpeculatTrading;
   const successMessage = hasQueryData.value ? '炒机设置修改成功' : '炒机设置新增成功';
   // 根据是否有ID决定调用新增接口还是修改接口
   const rateApi = rateId.value ? updateLossRate : addLossRate;
   const rateSuccessMessage = rateId.value ? '损耗率修改成功' : '损耗率新增成功';
   // 并行调用两个接口
   Promise.all([
      saveApi(saveData),
      rateApi(rateData)
   ]).then(([saveRes, rateRes]) => {
      proxy.$message.success(successMessage);
      proxy.$message.success(rateSuccessMessage);
      // 保存成功后,设置hasQueryData为true,下次保存将调用修改接口
      if (!hasQueryData.value) {
         hasQueryData.value = true;
      }
      // 如果返回了ID,保存起来
      if (rateRes && rateRes.data && rateRes.data.id) {
         rateId.value = rateRes.data.id;
      }
      // 保存成功后重新调用查询页面
      getList();
   }).catch(err => {
      proxy.$message.error('保存失败');
      console.error('保存失败:', err);
   });
}
// 获取炒机查询数据
const machineQueryData = ref([]);
const getMachineQueryData = (machineId) => {
   return machineQueryData.value.find(item => item.id === machineId);
};
const getMachineIndex = (item) => {
   // 兼容多种字段命名,返回 1-4 之一,否则返回 0(未知)
   const candidates = [item.machineId, item.machineNo, item.machine, item.deviceNo, item.deviceId]
   for (const v of candidates) {
      if (v === undefined || v === null) continue
      const n = Number(String(v).replace(/[^\d]/g, "")) // 抽取数字
      if ([1,2,3,4].includes(n)) return n
   }
   return 0
}
const computeTodaySummary = () => {
   const todayStr = dayjs().format("YYYY-MM-DD")
   // 重置所有炒机数据
   machines.forEach(machine => {
      machineData[machine.name] = { workLoad: 0, currentWorkLoad: 0, vacant: 0 }
   })
   tableData.value.forEach(item => {
      // 仅统计当天
      const isToday = dayjs(item.entryDate).format("YYYY-MM-DD") === todayStr
      if (!isToday) return
      // 使用正确的字段名:workLoad(炒机工作量), currentWorkLoad(炒机正在工作量)
      const workLoad = Number(item.workLoad) || 0
      const currentWorkLoad = Number(item.currentWorkLoad) || 0
      const machineName = item.speculativeTradingName || '炒机1'
      if (machineData[machineName]) {
         machineData[machineName].workLoad += workLoad
         machineData[machineName].currentWorkLoad += currentWorkLoad
         machineData[machineName].vacant = machineData[machineName].workLoad - machineData[machineName].currentWorkLoad
      }
   })
}
// 查询列表
/** 搜索按钮操作 */
@@ -138,6 +306,56 @@
   page.current = 1;
   getList();
};
// 是否有查询数据
const hasQueryData = ref(false)
// 损耗率
const rate = ref(6)
// 损耗率ID
const rateId = ref(null)
// 获取炒机正在工作量数据
const getMachineProductionData = () => {
   schedulingList().then((res) => {
      // 处理炒机正在工作量数据
      if (res.data && Array.isArray(res.data)) {
         // 设置是否有查询数据
         hasQueryData.value = res.data.length > 0
         // 保存查询数据到machineQueryData
         machineQueryData.value = res.data;
         // 重置所有炒机数据
         machines.forEach(machine => {
            machineData[machine.name] = { workLoad: 0, currentWorkLoad: 0, vacant: 0 }
         });
         // 遍历数据,根据查询返回的数据结构处理
         res.data.forEach(item => {
            // 根据name字段确定炒机
            const machineName = item.name || '炒机1';
            if (machineData[machineName]) {
               // 如果查询数据中有workLoad,则初始化炒机总量
               if (item.workLoad !== null && item.workLoad !== undefined) {
                  machineData[machineName].workLoad = Number(item.workLoad) || 0;
               }
               // 如果查询数据中有currentWorkLoad,则设置正在工作量
               if (item.currentWorkLoad !== null && item.currentWorkLoad !== undefined) {
                  machineData[machineName].currentWorkLoad = Number(item.currentWorkLoad) || 0;
               }
               // 计算空余工作量
               machineData[machineName].vacant = machineData[machineName].workLoad - machineData[machineName].currentWorkLoad;
            }
         });
      }
   }).catch(err => {
      console.error('获取炒机正在工作量数据失败:', err);
   });
};
const changeDaterange = (value) => {
   if (value) {
      searchForm.value.entryDateStart = value[0];
@@ -166,9 +384,33 @@
         pendingQuantity: (Number(item.quantity) || 0) - (Number(item.schedulingNum) || 0)
      }));
      page.total = res.data.total;
      computeTodaySummary()
      // 同时获取炒机正在工作量数据
      getMachineProductionData();
      // 获取损耗率数据
      getLossRateData();
   }).catch(() => {
      tableLoading.value = false;
   })
};
// 获取损耗率数据
const getLossRateData = () => {
   getLossRate().then((res) => {
      const data = res.data || res;
      if (data && data.rate !== undefined && data.rate !== null) {
         rate.value = Number(data.rate); // 确保转换为数字
         rateId.value = data.id || null;
      } else {
         rate.value = 6;
         rateId.value = null;
      }
   }).catch(err => {
      console.error('获取损耗率数据失败:', err);
      rate.value = 6;
      rateId.value = null;
   });
};
// 表格选择数据
const handleSelectionChange = (selection) => {
@@ -190,15 +432,159 @@
   })
};
// 打开自动派工弹框
const openAutoDispatch = () => {
   if (selectedRows.value.length === 0) {
      proxy.$message.error("请选择至少一条数据");
      return;
   }
   // 过滤掉待排产数量为0的数据
   const validRows = selectedRows.value.filter(row => row.pendingQuantity > 0);
   if (validRows.length === 0) {
      proxy.$message.warning("选中的数据无需派工");
      return;
   }
   nextTick(() => {
      autoDispatchDia.value?.openDialog('auto', validRows)
   })
};
// 导出
const handleOut = () => {
   ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
   })
      .then(() => {
         proxy.download("/salesLedger/scheduling/exportOne", {}, "生产派工.xlsx");
      })
      .catch(() => {
         proxy.$modal.msg("已取消");
      });
};
onMounted(() => {
   searchForm.value.entryDate = [
      dayjs().format("YYYY-MM-DD"),
      dayjs().add(1, "day").format("YYYY-MM-DD"),
   ]
   searchForm.value.entryDateStart = dayjs().format("YYYY-MM-DD")
   searchForm.value.entryDateEnd = dayjs().add(1, "day").format("YYYY-MM-DD")
   getList();
   getLossRateData();
});
</script>
<style scoped></style>
<style scoped>
.summary-bar{
   display: flex;
   gap: 16px;
   margin: 10px 0 16px 0;
}
.summary-item{
   background: #f5f7fa;
   border: 1px solid #ebeef5;
   border-radius: 6px;
   padding: 10px 16px;
   min-width: 160px;
}
.summary-label{
   color: #909399;
   font-size: 12px;
   margin-bottom: 6px;
}
.summary-value{
   color: #303133;
   font-size: 20px;
   font-weight: 600;
}
.summary-control{
   display: flex;
   align-items: center;
   height: 28px;
}
.machines-grid{
   display: grid;
   grid-template-columns: repeat(4, 1fr);
   gap: 16px;
   margin-bottom: 20px;
   padding: 16px;
   background: #f8f9fa;
   border-radius: 8px;
   border: 1px solid #e9ecef;
}
.machine-card{
   border: 1px solid #dee2e6;
   border-radius: 8px;
   padding: 16px;
   background: #fff;
   box-shadow: 0 2px 4px rgba(0,0,0,0.05);
   transition: all 0.3s ease;
}
.machine-card:hover{
   transform: translateY(-2px);
   box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.machine-title{
   font-weight: 600;
   font-size: 16px;
   margin-bottom: 12px;
   color: #2c3e50;
   text-align: center;
   padding-bottom: 8px;
   border-bottom: 2px solid #3498db;
}
.machine-metrics{
   display: flex;
   flex-direction: column;
   gap: 10px;
   color: #495057;
}
.machine-control{
   display: flex;
   align-items: center;
   justify-content: space-between;
   gap: 8px;
   padding: 8px 0;
   border-bottom: 1px solid #f1f3f4;
}
.machine-control span{
   font-size: 14px;
   white-space: nowrap;
   color: #6c757d;
   font-weight: 500;
}
.machine-metrics > div:not(.machine-control) {
   display: flex;
   justify-content: space-between;
   align-items: center;
   padding: 4px 0;
   font-size: 14px;
}
.machine-metrics > div:not(.machine-control) span:first-child {
   color: #6c757d;
}
.machine-metrics > div:not(.machine-control) span:last-child {
   font-weight: 600;
   color: #2c3e50;
}
.save-button-container{
   grid-column: 1 / -1;
   display: flex;
   justify-content: center;
   align-items: center;
   gap: 16px;
   margin-top: 16px;
   padding-top: 16px;
   border-top: 1px solid #e9ecef;
}
.loss-rate-container{
   display: flex;
   align-items: center;
   gap: 8px;
}
.loss-rate-label{
   font-size: 14px;
   color: #6c757d;
   font-weight: 500;
   white-space: nowrap;
}
</style>