<template>
|
<div class="app-container">
|
<div class="search_form">
|
<el-form :model="searchForm" :inline="true">
|
<el-form-item label="客户名称">
|
<el-input
|
v-model="searchForm.customerName"
|
placeholder="请输入"
|
clearable
|
:prefix-icon="Search"
|
style="width: 160px;"
|
@change="handleQuery"
|
/>
|
</el-form-item>
|
<el-form-item label="销售合同号">
|
<el-input
|
v-model="searchForm.salesContractNo"
|
placeholder="请输入"
|
clearable
|
:prefix-icon="Search"
|
style="width: 160px;"
|
@change="handleQuery"
|
/>
|
</el-form-item>
|
<el-form-item label="产品名称">
|
<el-input
|
v-model="searchForm.productCategory"
|
placeholder="请输入"
|
clearable
|
:prefix-icon="Search"
|
style="width: 160px;"
|
@change="handleQuery"
|
/>
|
</el-form-item>
|
<el-form-item label="规格型号">
|
<el-input
|
v-model="searchForm.specificationModel"
|
placeholder="请输入"
|
clearable
|
:prefix-icon="Search"
|
style="width: 160px;"
|
@change="handleQuery"
|
/>
|
</el-form-item>
|
<el-form-item>
|
<el-button type="primary" @click="handleQuery">查询</el-button>
|
</el-form-item>
|
</el-form>
|
<div>
|
<el-button type="primary" @click="isShowNewModal = true">新增</el-button>
|
<el-button type="danger" @click="handleDelete">删除</el-button>
|
<el-button @click="handleOut">导出</el-button>
|
</div>
|
</div>
|
|
<div class="table_list">
|
<PIMTable
|
rowKey="id"
|
:column="tableColumn"
|
:tableData="tableData"
|
:page="page"
|
:tableLoading="tableLoading"
|
:row-class-name="tableRowClassName"
|
:isSelection="true"
|
@selection-change="handleSelectionChange"
|
@pagination="pagination"
|
>
|
<template #completionStatus="{ row }">
|
<el-progress
|
:percentage="toProgressPercentage(row?.completionStatus)"
|
:color="progressColor(toProgressPercentage(row?.completionStatus))"
|
:status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''"
|
/>
|
</template>
|
</PIMTable>
|
</div>
|
|
<el-dialog
|
v-model="bindRouteDialogVisible"
|
title="绑定工艺路线"
|
width="700px"
|
>
|
<el-form label-width="90px">
|
<el-form-item label="工艺路线">
|
<el-select
|
v-model="bindForm.routeId"
|
placeholder="请选择工艺路线"
|
style="width: 100%;"
|
:loading="bindRouteLoading"
|
@change="handleBindRouteChange"
|
>
|
<el-option
|
v-for="item in routeOptions"
|
:key="item.id"
|
:label="item.processRouteCode || ''"
|
:value="item.id"
|
/>
|
</el-select>
|
</el-form-item>
|
<el-form-item v-if="bindProcessList.length" label="报工人员">
|
<div class="process-user-list">
|
<div
|
v-for="(item, index) in bindProcessList"
|
:key="item.id || `${item.processId}-${index}`"
|
class="process-user-item"
|
>
|
<div class="process-user-header">
|
<div class="process-user-name">
|
{{ item.name || item.processName || item.no || `工序${index + 1}` }}
|
</div>
|
<el-button
|
type="danger"
|
link
|
class="process-user-remove"
|
@click="removeBindProcessItem(index)"
|
>
|
删除
|
</el-button>
|
</div>
|
<el-select
|
v-model="bindForm.processUserList[index].userIds"
|
class="process-user-select"
|
placeholder="请选择报工人员"
|
filterable
|
clearable
|
multiple
|
collapse-tags
|
collapse-tags-tooltip
|
:max-collapse-tags="3"
|
@change="handleBindProcessUserChange(index, $event)"
|
>
|
<el-option
|
v-for="user in userOptions"
|
:key="user.userId"
|
:label="user.nickName"
|
:value="user.userId"
|
/>
|
</el-select>
|
</div>
|
</div>
|
</el-form-item>
|
</el-form>
|
<template #footer>
|
<span class="dialog-footer">
|
<el-button
|
type="primary"
|
:loading="bindRouteSaving"
|
@click="handleBindRouteConfirm"
|
>
|
确认
|
</el-button>
|
<el-button @click="bindRouteDialogVisible = false">取消</el-button>
|
</span>
|
</template>
|
</el-dialog>
|
|
<new-product-order
|
v-if="isShowNewModal"
|
v-model:visible="isShowNewModal"
|
@completed="handleQuery"
|
/>
|
</div>
|
</template>
|
|
<script setup>
|
import { defineAsyncComponent, getCurrentInstance, onMounted, reactive, ref, toRefs } from "vue";
|
import { ElMessageBox } from "element-plus";
|
import { Search } from "@element-plus/icons-vue";
|
import dayjs from "dayjs";
|
import { useRouter } from "vue-router";
|
import {
|
bindingRoute,
|
delProductOrder,
|
listProcessRoute,
|
productOrderListPage,
|
} from "@/api/productionManagement/productionOrder.js";
|
import { listMain as getOrderProcessRouteMain } from "@/api/productionManagement/productProcessRoute.js";
|
import { processList } from "@/api/productionManagement/productionProcess.js";
|
import { userListNoPageByTenantId } from "@/api/system/user.js";
|
import PIMTable from "@/components/PIMTable/PIMTable.vue";
|
import {findProcessRouteItemList} from "@/api/productionManagement/processRouteItem.js";
|
|
const NewProductOrder = defineAsyncComponent(() => import("@/views/productionManagement/productionOrder/New.vue"));
|
|
const { proxy } = getCurrentInstance();
|
const router = useRouter();
|
const isShowNewModal = ref(false);
|
|
const tableColumn = ref([
|
{
|
label: "生产订单号",
|
prop: "npsNo",
|
width: "120px",
|
},
|
{
|
label: "销售合同号",
|
prop: "salesContractNo",
|
width: "150px",
|
},
|
{
|
label: "客户名称",
|
prop: "customerName",
|
width: "200px",
|
},
|
{
|
label: "产品名称",
|
prop: "productCategory",
|
width: "120px",
|
},
|
{
|
label: "规格型号",
|
prop: "specificationModel",
|
width: "120px",
|
},
|
{
|
label: "工艺路线编号",
|
prop: "processRouteCode",
|
width: "200px",
|
},
|
{
|
label: "需求数量",
|
prop: "quantity",
|
},
|
{
|
label: "完成数量",
|
prop: "completeQuantity",
|
},
|
{
|
dataType: "slot",
|
label: "完成进度",
|
prop: "completionStatus",
|
slot: "completionStatus",
|
width: 180,
|
},
|
{
|
label: "开始日期",
|
prop: "startTime",
|
formatData: val => (val ? dayjs(val).format("YYYY-MM-DD") : ""),
|
width: 120,
|
},
|
{
|
label: "结束日期",
|
prop: "endTime",
|
formatData: val => (val ? dayjs(val).format("YYYY-MM-DD") : ""),
|
width: 120,
|
},
|
{
|
label: "交付日期",
|
prop: "deliveryDate",
|
formatData: val => (val ? dayjs(val).format("YYYY-MM-DD") : ""),
|
width: 120,
|
},
|
{
|
dataType: "action",
|
label: "操作",
|
align: "center",
|
fixed: "right",
|
width: 200,
|
operation: [
|
{
|
name: "工艺路线",
|
type: "text",
|
clickFun: row => {
|
showRouteItemModal(row);
|
},
|
},
|
{
|
name: "绑定工艺路线",
|
type: "text",
|
showHide: row => !row.processRouteCode,
|
clickFun: row => {
|
openBindRouteDialog(row);
|
},
|
},
|
{
|
name: "产品结构",
|
type: "text",
|
clickFun: row => {
|
showProductStructure(row);
|
},
|
},
|
],
|
},
|
]);
|
|
const tableData = ref([]);
|
const tableLoading = ref(false);
|
const page = reactive({
|
current: 1,
|
size: 100,
|
total: 0,
|
});
|
const selectedRows = ref([]);
|
|
const data = reactive({
|
searchForm: {
|
customerName: "",
|
salesContractNo: "",
|
projectName: "",
|
productCategory: "",
|
specificationModel: "",
|
},
|
});
|
const { searchForm } = toRefs(data);
|
|
const toProgressPercentage = val => {
|
const n = Number(val);
|
if (!Number.isFinite(n) || 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 tableRowClassName = ({ row }) => {
|
if (!row.deliveryDate || row.isFh) return "";
|
|
const diff = row.deliveryDaysDiff;
|
if (diff === 15) return "yellow";
|
if (diff === 10) return "pink";
|
if (diff === 2) return "purple";
|
if (diff < 2) return "red";
|
return "";
|
};
|
|
const bindRouteDialogVisible = ref(false);
|
const bindRouteLoading = ref(false);
|
const bindRouteSaving = ref(false);
|
const routeOptions = ref([]);
|
const bindProcessList = ref([]);
|
const userOptions = ref([]);
|
const userLoading = ref(false);
|
const bindForm = reactive({
|
orderId: null,
|
productId: null,
|
productModelId: null,
|
routeId: null,
|
processUserList: [],
|
});
|
|
const resetBindProcessUsers = () => {
|
bindProcessList.value = [];
|
bindForm.processUserList = [];
|
};
|
|
const ensureUserOptions = () => {
|
if (userOptions.value.length || userLoading.value) return;
|
|
userLoading.value = true;
|
userListNoPageByTenantId()
|
.then(res => {
|
userOptions.value = res.data || [];
|
})
|
.finally(() => {
|
userLoading.value = false;
|
});
|
};
|
|
const createBindProcessUserList = list =>
|
list.map(item => ({
|
processId: item.id,
|
processName: item.name || item.processName || item.no || "",
|
userIds: [],
|
userNames: "",
|
}));
|
|
const buildBindProcessRouteItems = () =>
|
bindProcessList.value.map((item, index) => {
|
const processUser = bindForm.processUserList[index] || {};
|
return {
|
productOrderId: bindForm.orderId,
|
productRouteId: bindForm.routeId,
|
processId: item.id,
|
productModelId: bindForm.productModelId,
|
dragSort: item.dragSort ?? index + 1,
|
isQuality: item.isQuality ?? false,
|
reportUserIds: Array.isArray(processUser.userIds) ? processUser.userIds.join(",") : "",
|
};
|
});
|
|
const fetchBindProcessList = async routeId => {
|
if (!routeId) {
|
resetBindProcessUsers();
|
return;
|
}
|
|
try {
|
const res = await findProcessRouteItemList({
|
routeId,
|
productModelId: bindForm.productModelId,
|
});
|
bindProcessList.value = res.data || [];
|
bindForm.processUserList = createBindProcessUserList(bindProcessList.value);
|
ensureUserOptions();
|
} catch (error) {
|
console.error("获取工序列表失败", error);
|
proxy.$modal.msgError("获取工序列表失败");
|
resetBindProcessUsers();
|
}
|
};
|
|
const handleBindRouteChange = routeId => {
|
fetchBindProcessList(routeId);
|
};
|
|
const handleBindProcessUserChange = (index, userIds) => {
|
const selectedUsers = userOptions.value.filter(user => Array.isArray(userIds) && userIds.includes(user.userId));
|
bindForm.processUserList[index].userNames = selectedUsers.map(user => user.nickName).join(",");
|
};
|
|
const removeBindProcessItem = index => {
|
bindProcessList.value.splice(index, 1);
|
bindForm.processUserList.splice(index, 1);
|
};
|
|
const openBindRouteDialog = async row => {
|
bindForm.orderId = row.id;
|
bindForm.productId = row.id ?? null;
|
bindForm.productModelId = row.productModelId ?? null;
|
bindForm.routeId = null;
|
bindForm.processUserList = [];
|
bindRouteDialogVisible.value = true;
|
routeOptions.value = [];
|
resetBindProcessUsers();
|
|
if (!row.productModelId) {
|
proxy.$modal.msgWarning("当前订单缺少产品型号,无法查询工艺路线");
|
bindRouteDialogVisible.value = false;
|
return;
|
}
|
|
bindRouteLoading.value = true;
|
try {
|
const res = await listProcessRoute({
|
productId: bindForm.productId,
|
productModelId: row.productModelId,
|
});
|
routeOptions.value = res.data || [];
|
} catch (error) {
|
console.error("获取工艺路线列表失败", error);
|
proxy.$modal.msgError("获取工艺路线列表失败");
|
} finally {
|
bindRouteLoading.value = false;
|
}
|
};
|
|
const handleBindRouteConfirm = async () => {
|
if (!bindForm.routeId) {
|
proxy.$modal.msgWarning("请选择工艺路线");
|
return;
|
}
|
if (!bindForm.processUserList.length) {
|
proxy.$modal.msgWarning("当前工艺路线下没有工序");
|
return;
|
}
|
if (bindForm.processUserList.some(item => !Array.isArray(item.userIds) || item.userIds.length === 0)) {
|
proxy.$modal.msgWarning("请为每道工序选择报工人员");
|
return;
|
}
|
|
bindRouteSaving.value = true;
|
try {
|
await bindingRoute({
|
id: bindForm.orderId,
|
routeId: bindForm.routeId,
|
processRouteItems: buildBindProcessRouteItems(),
|
processUserList: bindForm.processUserList.map(item => ({
|
...item,
|
userIds: item.userIds.join(","),
|
})),
|
});
|
proxy.$modal.msgSuccess("绑定成功");
|
bindRouteDialogVisible.value = false;
|
getList();
|
} catch (error) {
|
console.error("绑定工艺路线失败", error);
|
proxy.$modal.msgError("绑定工艺路线失败");
|
} finally {
|
bindRouteSaving.value = false;
|
}
|
};
|
|
const handleQuery = () => {
|
page.current = 1;
|
getList();
|
};
|
|
const pagination = obj => {
|
page.current = obj.page;
|
page.size = obj.limit;
|
getList();
|
};
|
|
const getList = () => {
|
tableLoading.value = true;
|
const params = { ...searchForm.value, ...page };
|
params.entryDate = undefined;
|
|
productOrderListPage(params)
|
.then(res => {
|
tableData.value = res.data.records;
|
page.total = res.data.total;
|
})
|
.finally(() => {
|
tableLoading.value = false;
|
});
|
};
|
|
const showRouteItemModal = async row => {
|
const orderId = row.id;
|
try {
|
const res = await getOrderProcessRouteMain(orderId);
|
const detail = res.data || {};
|
if (!detail.id) {
|
proxy.$modal.msgWarning("未找到关联的工艺路线");
|
return;
|
}
|
|
router.push({
|
path: "/productionManagement/processRouteItem",
|
query: {
|
id: detail.id,
|
processRouteCode: detail.processRouteCode || "",
|
productName: detail.productName || "",
|
model: detail.model || "",
|
bomNo: detail.bomNo || "",
|
description: detail.description || "",
|
orderId,
|
type: "order",
|
},
|
});
|
} catch (error) {
|
console.error("获取工艺路线信息失败", error);
|
proxy.$modal.msgError("获取工艺路线信息失败");
|
}
|
};
|
|
const showProductStructure = row => {
|
router.push({
|
path: "/productionManagement/productStructureDetail",
|
query: {
|
id: row.id,
|
bomNo: row.bomNo || "",
|
productName: row.productCategory || "",
|
productModelName: row.specificationModel || "",
|
orderId: row.id,
|
type: "order",
|
},
|
});
|
};
|
|
const handleSelectionChange = selection => {
|
selectedRows.value = selection;
|
};
|
|
const handleDelete = () => {
|
if (!selectedRows.value.length) {
|
proxy.$modal.msgWarning("请选择数据");
|
return;
|
}
|
|
const ids = selectedRows.value.map(item => item.id);
|
ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "提示", {
|
confirmButtonText: "确认",
|
cancelButtonText: "取消",
|
type: "warning",
|
})
|
.then(() => delProductOrder(ids))
|
.then(() => {
|
proxy.$modal.msgSuccess("删除成功");
|
getList();
|
})
|
.catch(() => {
|
proxy.$modal.msg("已取消删除");
|
});
|
};
|
|
const handleOut = () => {
|
ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "提示", {
|
confirmButtonText: "确认",
|
cancelButtonText: "取消",
|
type: "warning",
|
})
|
.then(() => {
|
proxy.download("/productOrder/export", { ...searchForm.value }, "生产订单.xlsx");
|
})
|
.catch(() => {
|
proxy.$modal.msg("已取消导出");
|
});
|
};
|
|
onMounted(() => {
|
getList();
|
});
|
</script>
|
|
<style scoped lang="scss">
|
.search_form {
|
align-items: start;
|
}
|
|
:deep(.yellow) {
|
background-color: #faf0de;
|
}
|
|
:deep(.pink) {
|
background-color: #fae1de;
|
}
|
|
:deep(.red) {
|
background-color: #f80202;
|
}
|
|
:deep(.purple) {
|
background-color: #f4defa;
|
}
|
|
.process-user-list {
|
width: 100%;
|
display: flex;
|
flex-direction: column;
|
gap: 14px;
|
padding: 14px;
|
border-radius: 12px;
|
background: #f7f9fc;
|
border: 1px solid #e8eef5;
|
}
|
|
.process-user-item {
|
display: grid;
|
grid-template-columns: minmax(0, 1fr);
|
gap: 16px;
|
padding: 12px 14px;
|
border-radius: 10px;
|
background: #fff;
|
border: 1px solid #edf2f7;
|
}
|
|
.process-user-header {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 12px;
|
}
|
|
.process-user-name {
|
color: #1f2d3d;
|
font-weight: 500;
|
line-height: 1.4;
|
}
|
|
.process-user-remove {
|
flex-shrink: 0;
|
padding: 0;
|
}
|
|
.process-user-select {
|
width: 100%;
|
}
|
|
@media (max-width: 768px) {
|
.process-user-item {
|
grid-template-columns: 1fr;
|
gap: 10px;
|
}
|
}
|
</style>
|