<template>
|
<div class="app-container production-order-detail">
|
<PageHeader content="生产详情">
|
</PageHeader>
|
|
<el-card shadow="never" class="mb12">
|
<div class="header">
|
<div class="title">基础信息</div>
|
<div class="sub">
|
<span class="mr12">生产订单号:{{ header.npsNo || "-" }}</span>
|
<span class="mr12">生产批号:{{ header.lotNo || "-" }}</span>
|
<span class="mr12">产品名称:{{ header.productCategory || "-" }}</span>
|
<span class="mr12">规格:{{ header.specificationModel || "-" }}</span>
|
</div>
|
</div>
|
</el-card>
|
|
<el-card shadow="never" class="mb12">
|
<div class="steps-head">
|
<div class="steps-title">工序执行进度</div>
|
</div>
|
<div class="steps-body">
|
<div class="steps-left">
|
<div class="steps-wrap">
|
<el-steps
|
class="process-steps"
|
:active="active"
|
finish-status="success"
|
direction="vertical"
|
>
|
<el-step
|
v-for="(p, idx) in processes"
|
:key="p.processCode || idx"
|
>
|
<template #title>
|
<div
|
class="step-title"
|
:class="{ selected: idx === selectedIndex }"
|
@click="selectProcess(idx)"
|
>
|
{{ `${idx + 1}. ${p.processName || "-"}` }}
|
</div>
|
</template>
|
<template #description>
|
<div class="step-panel">
|
<div v-if="idx === active" class="current-progress">
|
<div class="current-progress-head">
|
<span class="current-progress-title">当前工序进度</span>
|
<!-- <span class="current-progress-value">{{ currentProcessPercentage }}%</span> -->
|
</div>
|
<el-progress
|
:percentage="currentProcessPercentage"
|
:status="currentProcessPercentage >= 100 ? 'success' : ''"
|
:stroke-width="10"
|
/>
|
</div>
|
<div class="step-meta">
|
<span class="meta-item">
|
<span class="meta-label">工序编号</span>
|
<span class="meta-value">{{ p.processCode || "-" }}</span>
|
</span>
|
<span class="meta-item">
|
<span class="meta-label">不良率</span>
|
<span class="meta-value danger">{{ defectRateText(p) }}</span>
|
</span>
|
</div>
|
<div class="step-grid">
|
<div class="grid-item">
|
<div class="grid-label">投入数量</div>
|
<div class="grid-value">{{ p.inputQty ?? 0 }}</div>
|
</div>
|
<div class="grid-item">
|
<div class="grid-label">产出数量</div>
|
<div class="grid-value">{{ p.outputQty ?? 0 }}</div>
|
</div>
|
<div class="grid-item">
|
<div class="grid-label">合格数量</div>
|
<div class="grid-value success">{{ p.qualifiedQty ?? 0 }}</div>
|
</div>
|
<div class="grid-item">
|
<div class="grid-label">不良数量</div>
|
<div class="grid-value danger">{{ p.badQty ?? 0 }}</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
</el-step>
|
</el-steps>
|
</div>
|
</div>
|
|
<div class="steps-right">
|
<div class="right-panel">
|
<div class="right-panel-head">
|
<div class="right-title">报工信息</div>
|
<div class="right-sub" v-if="selectedProcess">
|
当前工序:{{ selectedProcess.processName }}({{ selectedProcess.processCode }})
|
</div>
|
</div>
|
|
<div v-if="!selectedProcess" class="right-empty">
|
点击左侧某个工序,右侧展示该工序的报工明细。
|
</div>
|
|
<div v-else class="right-content">
|
<el-table :data="mockReports" border height="420">
|
<el-table-column label="序号" type="index" width="60" align="center" />
|
<el-table-column label="报工单号" prop="reportNo" min-width="140" show-overflow-tooltip />
|
<el-table-column label="报工人员" prop="reportUser" min-width="120" show-overflow-tooltip />
|
<el-table-column label="报工时间" prop="reportTime" min-width="160" show-overflow-tooltip />
|
<el-table-column label="产出数量" prop="outputQty" min-width="110" />
|
<el-table-column label="不良数量" prop="badQty" min-width="110" />
|
<el-table-column label="备注" prop="remark" min-width="160" show-overflow-tooltip />
|
</el-table>
|
</div>
|
</div>
|
</div>
|
</div>
|
</el-card>
|
</div>
|
</template>
|
|
<script setup>
|
import { computed, ref } from "vue";
|
import { useRoute, useRouter } from "vue-router";
|
|
const route = useRoute();
|
|
const header = computed(() => ({
|
orderId: route.query.orderId,
|
npsNo: route.query.npsNo,
|
lotNo: route.query.lotNo,
|
productCategory: route.query.productCategory,
|
specificationModel: route.query.specificationModel,
|
}));
|
|
// 模拟工序数据(后续用接口替换)
|
const processes = computed(() => [
|
{
|
processCode: "GX-001",
|
processName: "备料",
|
inputQty: 1000,
|
outputQty: 980,
|
qualifiedQty: 970,
|
badQty: 10,
|
status: "success",
|
},
|
{
|
processCode: "GX-002",
|
processName: "成型",
|
inputQty: 980,
|
outputQty: 960,
|
qualifiedQty: 948,
|
badQty: 12,
|
status: "process",
|
},
|
{
|
processCode: "GX-003",
|
processName: "烘干",
|
inputQty: 960,
|
outputQty: 950,
|
qualifiedQty: 948,
|
badQty: 2,
|
status: "wait",
|
},
|
{
|
processCode: "GX-004",
|
processName: "包装入库",
|
inputQty: 950,
|
outputQty: 920,
|
qualifiedQty: 918,
|
badQty: 2,
|
status: "wait",
|
},
|
]);
|
|
const selectedIndex = ref(null);
|
|
const selectProcess = (idx) => {
|
selectedIndex.value = idx;
|
};
|
|
const selectedProcess = computed(() => {
|
if (selectedIndex.value === null || selectedIndex.value === undefined) return null;
|
return (processes.value || [])[selectedIndex.value] || null;
|
});
|
|
// 模拟报工信息(后续用接口替换)
|
const mockReports = computed(() => {
|
const p = selectedProcess.value;
|
if (!p) return [];
|
const code = p.processCode || "GX";
|
return [
|
{
|
reportNo: `${code}-BG-0001`,
|
reportUser: "张三",
|
reportTime: "2026-03-14 09:20",
|
outputQty: Math.floor((p.outputQty ?? 0) * 0.4),
|
badQty: Math.floor((p.badQty ?? 0) * 0.4),
|
remark: "正常报工",
|
},
|
{
|
reportNo: `${code}-BG-0002`,
|
reportUser: "李四",
|
reportTime: "2026-03-14 13:45",
|
outputQty: Math.floor((p.outputQty ?? 0) * 0.35),
|
badQty: Math.floor((p.badQty ?? 0) * 0.35),
|
remark: "设备调试后恢复",
|
},
|
{
|
reportNo: `${code}-BG-0003`,
|
reportUser: "王五",
|
reportTime: "2026-03-14 17:10",
|
outputQty: Math.max(0, (p.outputQty ?? 0) - Math.floor((p.outputQty ?? 0) * 0.75)),
|
badQty: Math.max(0, (p.badQty ?? 0) - Math.floor((p.badQty ?? 0) * 0.75)),
|
remark: "收尾",
|
},
|
];
|
});
|
|
const clampPercentage = (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);
|
};
|
|
// el-steps: active 为当前进行中的步骤下标(模拟)
|
const active = computed(() => {
|
const list = processes.value || [];
|
const idx = list.findIndex((p) => p.status === "process");
|
return idx >= 0 ? idx : 0;
|
});
|
|
const currentProcess = computed(() => {
|
const list = processes.value || [];
|
return list[active.value] || null;
|
});
|
|
// 当前工序进度:用产出/投入估算(UI 先跑通,后续按真实规则替换)
|
const currentProcessPercentage = computed(() => {
|
const p = currentProcess.value;
|
if (!p) return 0;
|
const input = Number(p.inputQty ?? 0);
|
const output = Number(p.outputQty ?? 0);
|
if (!Number.isFinite(input) || input <= 0) return 0;
|
return clampPercentage((output / input) * 100);
|
});
|
|
// 不良率:不良数量 / 产出数量(先按此口径,后续对接接口可调整)
|
const defectRateText = (p) => {
|
const bad = Number(p?.badQty ?? 0);
|
const output = Number(p?.outputQty ?? 0);
|
if (!Number.isFinite(bad) || bad <= 0) return "0%";
|
if (!Number.isFinite(output) || output <= 0) return "0%";
|
const rate = (bad / output) * 100;
|
return `${rate.toFixed(2)}%`;
|
};
|
</script>
|
|
<style scoped lang="scss">
|
.production-order-detail {
|
// 左侧步骤区的可视高度:随屏幕高度自适应
|
// 这里减去页面顶部(PageHeader + 基础信息卡片 + 边距等)的大致高度
|
--steps-left-height: calc(100vh - 320px);
|
|
.header {
|
display: flex;
|
flex-direction: column;
|
gap: 8px;
|
.title {
|
font-size: 16px;
|
font-weight: 600;
|
color: #303133;
|
}
|
.sub {
|
color: #606266;
|
display: flex;
|
flex-wrap: wrap;
|
row-gap: 6px;
|
}
|
}
|
|
.steps-head {
|
display: flex;
|
flex-direction: column;
|
gap: 4px;
|
margin-bottom: 12px;
|
.steps-title {
|
font-size: 14px;
|
font-weight: 600;
|
color: #303133;
|
}
|
.steps-desc {
|
font-size: 12px;
|
color: #909399;
|
}
|
}
|
|
.steps-wrap {
|
padding: 4px 0 0;
|
height: 100%;
|
overflow-y: auto;
|
}
|
|
.steps-body {
|
display: flex;
|
gap: 16px;
|
align-items: flex-start;
|
}
|
.steps-left {
|
flex: 0 0 50%;
|
max-width: 50%;
|
min-width: 0;
|
height: var(--steps-left-height);
|
overflow: hidden;
|
}
|
.steps-right {
|
flex: 1;
|
min-width: 0;
|
}
|
|
.step-title {
|
cursor: pointer;
|
display: inline-flex;
|
align-items: center;
|
padding: 2px 6px;
|
border-radius: 6px;
|
transition: background-color 0.15s ease;
|
&:hover {
|
background: #f5f7fa;
|
}
|
&.selected {
|
background: rgba(64, 158, 255, 0.12);
|
color: #409eff;
|
}
|
}
|
|
.process-steps {
|
width: 100%;
|
:deep(.el-step__title) {
|
font-weight: 600;
|
}
|
:deep(.el-step__description) {
|
font-size: 12px;
|
color: #606266;
|
line-height: 18px;
|
margin-top: 8px;
|
width: 100%;
|
}
|
:deep(.el-step__main) {
|
padding-bottom: 20px;
|
width: 100%;
|
}
|
:deep(.el-step__icon.is-text) {
|
border-color: #dcdfe6;
|
}
|
:deep(.el-step.is-vertical) {
|
align-items: flex-start;
|
}
|
:deep(.el-step__head) {
|
width: 28px;
|
flex: 0 0 28px;
|
}
|
:deep(.el-step__main) {
|
flex: 1;
|
min-width: 0;
|
padding-right: 6px;
|
}
|
}
|
|
.step-panel {
|
background: #f6f8fb;
|
border: 1px solid #ebeef5;
|
border-radius: 10px;
|
padding: 12px 12px 10px;
|
width: 100%;
|
max-width: none;
|
box-sizing: border-box;
|
}
|
|
.right-panel {
|
border: 1px solid #ebeef5;
|
border-radius: 10px;
|
background: #ffffff;
|
padding: 12px;
|
min-height: 520px;
|
box-sizing: border-box;
|
}
|
.right-panel-head {
|
display: flex;
|
flex-direction: column;
|
gap: 4px;
|
margin-bottom: 12px;
|
}
|
.right-title {
|
font-size: 14px;
|
font-weight: 700;
|
color: #303133;
|
}
|
.right-sub {
|
font-size: 12px;
|
color: #909399;
|
}
|
.right-empty {
|
height: 100%;
|
min-height: 460px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
color: #909399;
|
background: #fafafa;
|
border: 1px dashed #dcdfe6;
|
border-radius: 10px;
|
}
|
|
.current-progress {
|
background: #ffffff;
|
border: 1px dashed #dcdfe6;
|
border-radius: 10px;
|
padding: 10px 12px 8px;
|
margin-bottom: 10px;
|
.current-progress-head {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
margin-bottom: 6px;
|
}
|
.current-progress-title {
|
color: #303133;
|
font-weight: 600;
|
font-size: 12px;
|
}
|
.current-progress-value {
|
color: #606266;
|
font-size: 12px;
|
font-weight: 600;
|
}
|
}
|
|
.step-meta {
|
display: flex;
|
gap: 16px;
|
margin-bottom: 10px;
|
.meta-item {
|
display: inline-flex;
|
gap: 8px;
|
align-items: center;
|
}
|
.meta-label {
|
color: #909399;
|
}
|
.meta-value {
|
color: #303133;
|
font-weight: 600;
|
&.danger {
|
color: #f56c6c;
|
}
|
}
|
}
|
|
.step-grid {
|
display: grid;
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
gap: 10px;
|
.grid-item {
|
background: #ffffff;
|
border: 1px solid #ebeef5;
|
border-radius: 10px;
|
padding: 10px 10px 8px;
|
}
|
.grid-label {
|
font-size: 12px;
|
color: #909399;
|
margin-bottom: 6px;
|
}
|
.grid-value {
|
font-size: 16px;
|
font-weight: 700;
|
color: #303133;
|
&.success {
|
color: #67c23a;
|
}
|
&.danger {
|
color: #f56c6c;
|
}
|
}
|
}
|
|
.mb12 {
|
margin-bottom: 12px;
|
}
|
.mr12 {
|
margin-right: 12px;
|
}
|
}
|
</style>
|