<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>
|
<el-input
|
v-model="searchForm.customerName"
|
style="width: 240px"
|
placeholder="请输入"
|
@change="handleQuery"
|
clearable
|
prefix-icon="Search"
|
/>
|
<span class="search_title ml10">项目名称:</span>
|
<el-input
|
v-model="searchForm.projectName"
|
style="width: 240px"
|
placeholder="请输入"
|
@change="handleQuery"
|
clearable
|
prefix-icon="Search"
|
/>
|
<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" />
|
<el-button type="primary" @click="handleQuery" style="margin-left: 10px">搜索</el-button>
|
</div>
|
<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
|
rowKey="id"
|
:column="tableColumn"
|
:tableData="tableData"
|
:page="page"
|
:isSelection="true"
|
@selection-change="handleSelectionChange"
|
:tableLoading="tableLoading"
|
@pagination="pagination"
|
:total="page.total"
|
></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, 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, schedulingList, addSpeculatTrading, updateSpeculatTrading, getLossRate, addLossRate, updateLossRate} from "@/api/productionManagement/productionOrder.js";
|
import { ElMessageBox } from "element-plus";
|
|
const data = reactive({
|
searchForm: {
|
customerName: "",
|
projectName: "",
|
status: "",
|
entryDate: [dayjs().format("YYYY-MM-DD"), dayjs().format("YYYY-MM-DD")], // 录入日期,默认当天
|
entryDateStart: dayjs().format("YYYY-MM-DD"),
|
entryDateEnd: dayjs().format("YYYY-MM-DD"),
|
},
|
});
|
const { searchForm } = toRefs(data);
|
const tableColumn = ref([
|
{
|
label: "合同号",
|
prop: "salesContractNo",
|
width: 220,
|
},
|
{
|
label: "客户合同号",
|
prop: "customerContractNo",
|
width: 250,
|
},
|
{
|
label: "客户名称",
|
prop: "customerName",
|
width: 250,
|
},
|
{
|
label: "项目名称",
|
prop: "projectName",
|
width:300
|
},
|
{
|
label: "产品大类",
|
prop: "productCategory",
|
width: 160,
|
},
|
{
|
label: "规格型号",
|
prop: "specificationModel",
|
width: 120,
|
},
|
{
|
label: "绑定机器",
|
prop: "speculativeTradingName",
|
width: 220,
|
},
|
{
|
label: "单位",
|
prop: "unit",
|
width:90
|
},
|
{
|
label: "录入日期",
|
prop: "entryDate",
|
width: 120,
|
},
|
{
|
label: "数量",
|
prop: "quantity",
|
},
|
{
|
label: "排产数量",
|
prop: "schedulingNum",
|
width: 100,
|
},
|
{
|
label: "待排数量",
|
prop: "pendingQuantity",
|
width: 100,
|
},
|
]);
|
const tableData = ref([]);
|
const selectedRows = ref([]);
|
const tableLoading = ref(false);
|
const page = reactive({
|
current: 1,
|
size: 100,
|
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
|
}
|
})
|
}
|
|
// 查询列表
|
/** 搜索按钮操作 */
|
const handleQuery = () => {
|
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];
|
searchForm.value.entryDateEnd = value[1];
|
} else {
|
searchForm.value.entryDateStart = undefined;
|
searchForm.value.entryDateEnd = undefined;
|
}
|
handleQuery();
|
};
|
const pagination = (obj) => {
|
page.current = obj.page;
|
page.size = obj.limit;
|
getList();
|
};
|
const getList = () => {
|
tableLoading.value = true;
|
// 构造一个新的对象,不包含entryDate字段
|
const params = { ...searchForm.value, ...page };
|
params.entryDate = undefined
|
schedulingListPage(params).then((res) => {
|
tableLoading.value = false;
|
// 处理每条数据,增加pendingQuantity字段
|
tableData.value = res.data.records.map(item => ({
|
...item,
|
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) => {
|
selectedRows.value = selection;
|
};
|
|
// 打开弹框
|
const openForm = (type) => {
|
if (selectedRows.value.length !== 1) {
|
proxy.$message.error("请选择一条数据");
|
return;
|
}
|
if (selectedRows.value[0].pendingQuantity == 0) {
|
proxy.$message.warning("无需再派工");
|
return;
|
}
|
nextTick(() => {
|
formDia.value?.openDialog(type, selectedRows.value[0])
|
})
|
};
|
|
// 打开自动派工弹框
|
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(() => {
|
getList();
|
getLossRateData();
|
});
|
</script>
|
|
<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>
|