<template>
|
<div class="app-container">
|
<div class="search_form mb20">
|
<div class="search-row">
|
<div class="search-item">
|
<span class="search_title">工单编号:</span>
|
<el-input v-model="searchForm.workOrderNo"
|
style="width: 240px"
|
placeholder="请输入"
|
@change="handleQuery"
|
clearable
|
prefix-icon="Search" />
|
</div>
|
<div class="search-item">
|
<span class="search_title">生产订单号:</span>
|
<el-input v-model="searchForm.npsNo"
|
style="width: 240px"
|
placeholder="请输入"
|
@change="handleQuery"
|
clearable
|
prefix-icon="Search" />
|
</div>
|
<div class="search-item">
|
<el-button type="primary"
|
@click="handleQuery">搜索</el-button>
|
</div>
|
</div>
|
</div>
|
<div class="table_list">
|
<el-table :data="tableData"
|
border
|
v-loading="tableLoading"
|
:expand-row-keys="expandedRowKeys"
|
:row-key="(row) => row.productionOrderId"
|
@expand-change="expandChange"
|
height="calc(100vh - 22em)">
|
<!-- 展开行列 -->
|
<el-table-column type="expand"
|
width="60"
|
fixed="left">
|
<template #default="props">
|
<el-table :data="props.row.children || []"
|
border
|
v-loading="childLoading[props.row.productionOrderId]"
|
style="margin: 10px 0;">
|
<el-table-column align="center"
|
label="序号"
|
type="index"
|
width="60" />
|
<el-table-column label="工单类型"
|
prop="workOrderType"
|
width="80" />
|
<el-table-column label="工单编号"
|
prop="workOrderNo"
|
width="140" />
|
<el-table-column label="需求数量"
|
prop="planQuantity"
|
width="140" />
|
<el-table-column label="完成数量"
|
prop="completeQuantity"
|
width="140" />
|
<el-table-column label="完成进度"
|
width="140">
|
<template #default="scope">
|
<el-progress :percentage="toProgressPercentage(scope.row?.completionStatus)"
|
:color="progressColor(toProgressPercentage(scope.row?.completionStatus))"
|
:status="toProgressPercentage(scope.row?.completionStatus) >= 100 ? 'success' : ''" />
|
</template>
|
</el-table-column>
|
<el-table-column label="计划开始时间"
|
prop="planStartTime"
|
width="140" />
|
<el-table-column label="计划结束时间"
|
prop="planEndTime"
|
width="140" />
|
<el-table-column label="实际开始时间"
|
prop="actualStartTime"
|
width="140" />
|
<el-table-column label="实际结束时间"
|
prop="actualEndTime"
|
width="140" />
|
<el-table-column label="指定报工人"
|
prop="userNames"
|
width="180" />
|
<el-table-column label="操作"
|
width="200"
|
align="center"
|
fixed="right">
|
<template #default="scope">
|
<el-button link
|
type="primary"
|
@click="handleEdit(scope.row)">
|
计划时间
|
</el-button>
|
<el-button link
|
type="primary"
|
@click="handleAssignReporter(scope.row)">
|
指定报工人
|
</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
</template>
|
</el-table-column>
|
<!-- 主表列 -->
|
<el-table-column align="center"
|
label="序号"
|
type="index"
|
width="60" />
|
<el-table-column label="生产订单号"
|
prop="npsNo"
|
min-width="140" />
|
<el-table-column label="产品名称"
|
prop="productName"
|
min-width="140" />
|
<el-table-column label="规格"
|
prop="model"
|
min-width="100" />
|
<el-table-column label="单位"
|
prop="unit"
|
min-width="60" />
|
<el-table-column label="工序名称"
|
prop="operationName"
|
min-width="100" />
|
<el-table-column label="工单数量"
|
min-width="80">
|
<template #default="scope">
|
<span>共{{ scope.row.productionTaskCount || 0 }}个</span>
|
</template>
|
</el-table-column>
|
<el-table-column label="计划数量"
|
prop="planQuantity"
|
min-width="100" />
|
<el-table-column label="完成数量"
|
prop="completeQuantity"
|
min-width="100" />
|
<el-table-column label="完成进度"
|
min-width="140">
|
<template #default="scope">
|
<el-progress :percentage="toProgressPercentage(scope.row?.completionStatus)"
|
:color="progressColor(toProgressPercentage(scope.row?.completionStatus))"
|
:status="toProgressPercentage(scope.row?.completionStatus) >= 100 ? 'success' : ''" />
|
</template>
|
</el-table-column>
|
<el-table-column label="报废数量"
|
prop="scrapQty"
|
min-width="80">
|
<template #default="scope">
|
<span :class="{ 'scrap-text': scope.row.scrapQty > 0 }">
|
{{ scope.row.scrapQty || 0 }}
|
</span>
|
</template>
|
</el-table-column>
|
<el-table-column label="状态"
|
min-width="100"
|
align="center">
|
<template #default="scope">
|
<el-tag :type="getStatusTagType(scope.row.status)">
|
{{ getStatusText(scope.row.status) }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
</el-table>
|
<PaginationComp v-show="page.total > 0"
|
:total="page.total"
|
:page="page.current"
|
:limit="page.size"
|
@pagination="pagination" />
|
</div>
|
<el-dialog v-model="editDialogVisible"
|
title="编辑计划时间"
|
width="500px">
|
<el-form :model="editrow"
|
label-width="120px">
|
<el-form-item label="计划开始时间">
|
<el-date-picker v-model="editrow.planStartTime"
|
type="date"
|
placeholder="请选择"
|
value-format="YYYY-MM-DD"
|
style="width: 300px" />
|
</el-form-item>
|
<el-form-item label="计划结束时间">
|
<el-date-picker v-model="editrow.planEndTime"
|
type="date"
|
placeholder="请选择"
|
value-format="YYYY-MM-DD"
|
style="width: 300px" />
|
</el-form-item>
|
</el-form>
|
<template #footer>
|
<span class="dialog-footer">
|
<el-button type="primary"
|
@click="handleUpdate">确定</el-button>
|
<el-button @click="editDialogVisible = false">取消</el-button>
|
</span>
|
</template>
|
</el-dialog>
|
<!-- 指定报工人弹窗 -->
|
<el-dialog v-model="assignReporterDialogVisible"
|
title="指定报工人"
|
width="800px">
|
<div class="assign-reporter-content">
|
<div class="search-box">
|
<el-input
|
v-model="employeeSearchKeyword"
|
placeholder="搜索人员姓名"
|
clearable
|
@input="handleEmployeeSearch"
|
style="width: 350px">
|
<template #prefix>
|
<i class="el-icon-search"></i>
|
</template>
|
</el-input>
|
</div>
|
<div class="selected-tags-box"
|
v-if="selectedEmployeeIds.length > 0">
|
<div class="tags-label">已选择:</div>
|
<div class="tags-list">
|
<el-tag v-for="id in selectedEmployeeIds"
|
:key="id"
|
closable
|
@close="removeEmployeeTag(id)"
|
class="employee-tag">
|
{{ getEmployeeNameById(id) }}
|
</el-tag>
|
</div>
|
</div>
|
<div class="employee-list-container"
|
v-loading="employeeTableLoading">
|
<el-checkbox-group v-model="selectedEmployeeIds">
|
<div class="employee-grid">
|
<div v-for="item in filteredEmployeeList"
|
:key="item.userId"
|
class="employee-item">
|
<el-checkbox :label="item.userId"
|
border>
|
<div class="employee-info">
|
<span class="name">{{ item.nickName }}</span>
|
<span class="dept">{{ item.dept?.deptName }}</span>
|
</div>
|
</el-checkbox>
|
</div>
|
</div>
|
</el-checkbox-group>
|
<div v-if="filteredEmployeeList.length === 0"
|
class="empty-text">
|
{{ employeeSearchKeyword ? '无匹配人员' : '暂无人员数据' }}
|
</div>
|
</div>
|
</div>
|
<template #footer>
|
<span class="dialog-footer">
|
<el-button type="primary"
|
@click="handleSaveReporters">确定</el-button>
|
<el-button @click="assignReporterDialogVisible = false">取消</el-button>
|
</span>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup>
|
import { getCurrentInstance, onMounted, reactive, ref, toRefs, computed } from "vue";
|
import { ElMessageBox } from "element-plus";
|
import {
|
productWorkOrderPage,
|
getWorkOrderListByOrderId,
|
updateProductWorkOrder,
|
assignProductWorkOrder,
|
} from "@/api/productionManagement/workOrder.js";
|
import { listUser } from "@/api/system/user.js";
|
import PaginationComp from "@/components/PIMTable/Pagination.vue";
|
|
const { proxy } = getCurrentInstance();
|
|
const statusMap = {
|
1: '待确认',
|
2: '待生产',
|
3: '生产中',
|
4: '已生产',
|
};
|
|
const getStatusText = (status) => statusMap[status] || '未知';
|
|
const getStatusTagType = (status) => {
|
switch (status) {
|
case 1: return 'info';
|
case 2: return 'warning';
|
case 3: return 'primary';
|
case 4: return 'success';
|
default: return 'info';
|
}
|
};
|
|
const tableData = ref([]);
|
const tableLoading = ref(false);
|
const expandedRowKeys = ref([]);
|
const childLoading = ref({});
|
const editDialogVisible = ref(false);
|
const editrow = ref(null);
|
const page = reactive({
|
current: 1,
|
size: 100,
|
total: 0,
|
});
|
|
// 指定报工人相关
|
const assignReporterDialogVisible = ref(false);
|
const employeeTableLoading = ref(false);
|
const employeeTableData = ref([]);
|
const employeePage = reactive({
|
current: 1,
|
size: 100,
|
total: 0,
|
});
|
const employeeSearchForm = reactive({
|
staffName: "",
|
});
|
const employeeSearchKeyword = ref("");
|
const selectedEmployeeIds = ref([]);
|
const currentWorkOrder = ref(null);
|
|
const filteredEmployeeList = computed(() => {
|
const keyword = employeeSearchKeyword.value.trim().toLowerCase();
|
if (!keyword) {
|
return employeeTableData.value;
|
}
|
return employeeTableData.value.filter(item => {
|
const name = (item.nickName || "").toLowerCase();
|
const dept = (item.dept?.deptName || "").toLowerCase();
|
return name.includes(keyword) || dept.includes(keyword);
|
});
|
});
|
|
const handleEmployeeSearch = () => {
|
};
|
|
const data = reactive({
|
searchForm: {
|
workOrderNo: "",
|
npsNo: "",
|
},
|
});
|
const { searchForm } = toRefs(data);
|
|
const toProgressPercentage = val => {
|
const n = Number(val);
|
if (!Number.isFinite(n)) return 0;
|
if (n <= 0) return 0;
|
if (n >= 100) return 100;
|
return Math.round(n);
|
};
|
|
const progressColor = percentage => {
|
const p = toProgressPercentage(percentage);
|
if (p < 30) return "#f56c6c";
|
if (p < 50) return "#e6a23c";
|
if (p < 80) return "#409eff";
|
return "#67c23a";
|
};
|
|
const handleQuery = () => {
|
page.current = 1;
|
getList();
|
};
|
|
const pagination = obj => {
|
page.current = obj.page;
|
page.size = obj.limit;
|
getList();
|
};
|
|
const getList = () => {
|
tableLoading.value = true;
|
expandedRowKeys.value = [];
|
const params = { ...searchForm.value, ...page };
|
productWorkOrderPage(params)
|
.then(res => {
|
tableLoading.value = false;
|
tableData.value = res.data.records.map(item => ({
|
...item,
|
children: [],
|
}));
|
page.total = res.data.total;
|
})
|
.catch(() => {
|
tableLoading.value = false;
|
});
|
};
|
|
const expandChange = (row, expandedRows) => {
|
if (expandedRows.length > 0) {
|
expandedRowKeys.value = [];
|
const orderId = row.productionOrderId;
|
|
if (row.children && row.children.length > 0) {
|
expandedRowKeys.value.push(orderId);
|
return;
|
}
|
|
childLoading.value[orderId] = true;
|
|
getWorkOrderListByOrderId(orderId)
|
.then(res => {
|
childLoading.value[orderId] = false;
|
const index = tableData.value.findIndex(
|
item => item.productionOrderId === orderId
|
);
|
if (index > -1) {
|
tableData.value[index].children = res.data || [];
|
}
|
expandedRowKeys.value.push(orderId);
|
})
|
.catch(error => {
|
childLoading.value[orderId] = false;
|
console.error('加载工单明细失败:', error);
|
});
|
} else {
|
expandedRowKeys.value = [];
|
}
|
};
|
|
const handleEdit = row => {
|
editrow.value = JSON.parse(JSON.stringify(row));
|
editDialogVisible.value = true;
|
};
|
|
const handleUpdate = () => {
|
updateProductWorkOrder(editrow.value)
|
.then(() => {
|
proxy.$modal.msgSuccess("提交成功");
|
editDialogVisible.value = false;
|
getList();
|
})
|
.catch(() => {
|
ElMessageBox.alert("修改失败", "提示", {
|
confirmButtonText: "确定",
|
});
|
});
|
};
|
|
const handleAssignReporter = row => {
|
currentWorkOrder.value = row;
|
assignReporterDialogVisible.value = true;
|
// 回显已勾选的人员
|
if (row.userIds) {
|
try {
|
selectedEmployeeIds.value = JSON.parse(row.userIds);
|
} catch (e) {
|
selectedEmployeeIds.value = [];
|
}
|
} else {
|
selectedEmployeeIds.value = [];
|
}
|
employeeSearchForm.staffName = "";
|
getEmployeeList();
|
};
|
|
const getEmployeeList = () => {
|
employeeTableLoading.value = true;
|
const params = {
|
pageNum: 1,
|
pageSize: 100,
|
};
|
listUser(params)
|
.then(res => {
|
employeeTableLoading.value = false;
|
employeeTableData.value = res.rows;
|
employeePage.total = res.total;
|
})
|
.catch(() => {
|
employeeTableLoading.value = false;
|
});
|
};
|
|
const getEmployeeNameById = id => {
|
const employee = employeeTableData.value.find(item => item.userId === id);
|
return employee ? employee.nickName : id;
|
};
|
|
const removeEmployeeTag = id => {
|
selectedEmployeeIds.value = selectedEmployeeIds.value.filter(
|
item => item !== id
|
);
|
};
|
|
const handleSaveReporters = () => {
|
if (selectedEmployeeIds.value.length === 0) {
|
proxy.$modal.msgWarning("请选择报工人");
|
return;
|
}
|
|
const updateData = {
|
id: currentWorkOrder.value.id,
|
userIds: JSON.stringify(selectedEmployeeIds.value),
|
};
|
console.log(updateData, "updateData");
|
|
assignProductWorkOrder(updateData)
|
.then(() => {
|
proxy.$modal.msgSuccess("指定成功");
|
assignReporterDialogVisible.value = false;
|
getList();
|
})
|
.catch(() => {
|
proxy.$modal.msgError("指定失败");
|
});
|
};
|
|
onMounted(() => {
|
getList();
|
});
|
</script>
|
|
<style scoped lang="scss">
|
.search-row {
|
display: flex;
|
align-items: center;
|
gap: 12px;
|
}
|
|
.search-item {
|
display: flex;
|
align-items: center;
|
}
|
|
.search_title {
|
margin-right: 8px;
|
font-size: 14px;
|
color: #606266;
|
}
|
|
:deep(.el-table__expanded-cell) {
|
padding: 10px 30px !important;
|
background-color: #fafafa;
|
}
|
|
.scrap-text {
|
color: #f56c6c;
|
}
|
|
.assign-reporter-content {
|
.search-box {
|
margin-bottom: 16px;
|
display: flex;
|
justify-content: center;
|
}
|
|
.selected-tags-box {
|
margin-bottom: 16px;
|
padding: 12px;
|
background-color: #f5f7fa;
|
border-radius: 4px;
|
display: flex;
|
align-items: flex-start;
|
|
.tags-label {
|
font-size: 14px;
|
color: #606266;
|
margin-right: 8px;
|
white-space: nowrap;
|
margin-top: 4px;
|
}
|
|
.tags-list {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 8px;
|
|
.employee-tag {
|
margin-bottom: 4px;
|
}
|
}
|
}
|
|
.employee-list-container {
|
max-height: 400px;
|
overflow-y: auto;
|
padding: 10px;
|
border: 1px solid #f0f0f0;
|
border-radius: 4px;
|
|
.employee-grid {
|
display: grid;
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
gap: 12px;
|
}
|
|
.employee-item {
|
:deep(.el-checkbox) {
|
width: 100%;
|
margin-right: 0;
|
height: auto;
|
padding: 8px;
|
|
.el-checkbox__label {
|
width: 100%;
|
}
|
}
|
|
.employee-info {
|
display: flex;
|
flex-direction: column;
|
gap: 4px;
|
|
.name {
|
font-weight: bold;
|
font-size: 14px;
|
color: #303133;
|
}
|
|
.dept {
|
font-size: 12px;
|
color: #909399;
|
}
|
}
|
}
|
|
.empty-text {
|
text-align: center;
|
color: #909399;
|
padding: 20px;
|
}
|
}
|
}
|
</style>
|