Files
klp-oa/klp-ui/src/views/mes/eqp/check/day.vue
砂糖 40ebca2573 feat(mes/eqp/check): add inspector summary statistics table
新增按实际巡检人维度的巡检汇总统计表格,展示巡检人、巡检次数、通过/不通过数量及通过率
2026-06-03 16:59:03 +08:00

362 lines
15 KiB
Vue

<template>
<div class="app-container" v-loading="loading">
<el-row>
<el-form label-width="80px" inline>
<el-form-item label="时间段">
<el-date-picker v-model="dateRange" type="daterange" value-format="yyyy-MM-dd"
range-separator="" start-placeholder="开始日期" end-placeholder="结束日期"
@change="handleQuery" style="width: 260px;" />
</el-form-item>
<el-form-item label="产线">
<el-select v-model="productionLine" placeholder="请选择产线" clearable @change="handleQuery" style="width: 150px;">
<el-option v-for="item in lineList" :key="item.lineId" :label="item.lineName" :value="item.lineId" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
</el-form-item>
<el-form-item v-hasPermi="['mes:eqp:submit']">
<el-button type="success" icon="el-icon-s-promotion" @click="handleSubmitForApproval">送检</el-button>
</el-form-item>
</el-form>
</el-row>
<el-descriptions title="巡检日报" :column="4" border style="margin-bottom: 16px;">
<el-descriptions-item label="巡检部位数">{{ summary.partCount }}</el-descriptions-item>
<el-descriptions-item label="巡检项数">{{ summary.checklistCount }}</el-descriptions-item>
<el-descriptions-item label="总巡检次数">{{ summary.totalCount }}</el-descriptions-item>
<el-descriptions-item label="理论应检次数">{{ summary.expectedTotal }}</el-descriptions-item>
<el-descriptions-item label="通过">
<el-tag type="success" size="small">{{ summary.passCount }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="不通过">
<el-tag type="danger" size="small">{{ summary.failCount }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="通过率">{{ summary.passRate }}</el-descriptions-item>
<el-descriptions-item label="检验完成率">{{ summary.completionRate }}</el-descriptions-item>
</el-descriptions>
<!-- 负责人汇总 -->
<el-card class="section-card" v-if="inspectorSummary.length" style="margin-bottom: 16px;">
<div slot="header"><i class="el-icon-user"></i> 按实际巡检人汇总</div>
<el-table :data="inspectorSummary" size="small" stripe border>
<el-table-column prop="inspector" label="巡检人" />
<el-table-column prop="total" label="巡检次数" align="center" />
<el-table-column prop="pass" label="通过" align="center" />
<el-table-column prop="fail" label="不通过" align="center" />
<el-table-column prop="passRate" label="通过率" align="center" />
</el-table>
</el-card>
<el-card class="section-card" v-if="personSummary.length" style="margin-bottom: 16px;">
<div slot="header"><i class="el-icon-user"></i> 按负责人汇总</div>
<el-table :data="personSummary" size="small" stripe border>
<el-table-column prop="responsiblePerson" label="负责人" />
<el-table-column prop="partCount" label="负责部位数" align="center" />
<el-table-column prop="checkCount" label="巡检项数" align="center" />
<el-table-column prop="totalInspections" label="实检次数" align="center" />
<el-table-column prop="expectedTotal" label="应检次数" align="center" />
<el-table-column prop="passCount" label="通过" align="center" />
<el-table-column prop="failCount" label="不通过" align="center" />
<el-table-column label="完成率" align="center">
<template slot-scope="scope">
<el-progress :percentage="scope.row.completionRate" :stroke-width="6" :show-text="true" />
</template>
</el-table-column>
</el-table>
</el-card>
<el-table :data="tableData" border stripe style="width: 100%" :cell-style="cellStyle"
@cell-mouse-enter="handleCellEnter" @cell-mouse-leave="handleCellLeave">
<el-table-column label="巡检部位" align="center" prop="partName" width="140" />
<el-table-column label="巡检内容" align="center" prop="checkContent" min-width="200" show-overflow-tooltip />
<el-table-column label="白班" align="center" width="150">
<template slot-scope="scope">
<div v-if="scope.row.dayRecords && scope.row.dayRecords.length" style="display: flex; gap: 8px;">
<div v-for="(r, idx) in scope.row.dayRecords" :key="idx" style="margin:2px 0;">
<el-tooltip placement="top" popper-class="inspect-tooltip">
<div slot="content">
<div>{{ r.inspectTime }} | 白班 | {{ r.inspector }} | {{ r.runStatus == 1 ? '通过' : '不通过' }}{{ r.abnormalDesc ? '异常: ' + r.abnormalDesc : '' }}</div>
</div>
<span v-if="r.runStatus == 1" class="result-icon result-pass"></span>
<span v-else class="result-icon result-fail"></span>
</el-tooltip>
</div>
</div>
<span v-else class="result-none">-</span>
</template>
</el-table-column>
<el-table-column label="夜班" align="center" width="150">
<template slot-scope="scope">
<div v-if="scope.row.nightRecords && scope.row.nightRecords.length" style="display: flex; gap: 8px;">
<div v-for="(r, idx) in scope.row.nightRecords" :key="idx" style="margin:2px 0;">
<el-tooltip placement="top" popper-class="inspect-tooltip">
<div slot="content">
<div>{{ r.inspectTime }} | 夜班 | {{ r.inspector }} | {{ r.runStatus == 1 ? '通过' : '不通过' }} {{ r.abnormalDesc ? '异常: ' + r.abnormalDesc : '' }}</div>
</div>
<span v-if="r.runStatus == 1" class="result-icon result-pass"></span>
<span v-else class="result-icon result-fail"></span>
</el-tooltip>
</div>
</div>
<span v-else class="result-none">-</span>
</template>
</el-table-column>
<el-table-column prop="expectedCount" label="应检" width="55" align="center" />
<el-table-column prop="actualCount" label="实检" width="55" align="center" />
<el-table-column label="差异" width="55" align="center">
<template slot-scope="scope">
<span v-if="scope.row.diff > 0" class="diff-over">+{{ scope.row.diff }}</span>
<span v-else-if="scope.row.diff < 0" class="diff-under">{{ scope.row.diff }}</span>
<span v-else class="diff-ok">0</span>
</template>
</el-table-column>
<el-table-column prop="passCount" label="通过" width="55" align="center" />
<el-table-column prop="failCount" label="不通过" width="60" align="center" />
</el-table>
</div>
</template>
<script>
import { listEquipmentPart } from "@/api/mes/eqp/equipmentPart";
import { listEquipmentInspectionRecord } from "@/api/mes/eqp/equipmentInspectionRecord";
import { listProductionLine } from "@/api/wms/productionLine";
import { addEquipmentInspectionApproval } from "@/api/mes/eqp/equipmentInspectionApproval";
export default {
name: "DailyInspectionReport",
data() {
const d = new Date();
const today = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
const routeQuery = this.$route && this.$route.query || {};
const hasRouteQuery = !!(routeQuery.productionLine || (routeQuery.startDate && routeQuery.endDate));
return {
loading: false,
dateRange: (routeQuery.startDate && routeQuery.endDate)
? [routeQuery.startDate, routeQuery.endDate]
: [today, today],
productionLine: routeQuery.productionLine ? Number(routeQuery.productionLine) : 2,
lineList: [],
partList: [],
records: [],
hasRouteQuery,
};
},
computed: {
checklistList() {
return this.partList.flatMap(p => (p.checklistList || []).map(cl => ({
...cl,
partName: cl.partName || p.inspectPart,
})));
},
selectedLineName() {
const found = this.lineList.find(l => l.lineId === this.productionLine);
return found ? found.lineName : '';
},
tableData() {
const recordMap = {};
this.records.forEach(r => {
const key = `${r.checkId}_${r.shift}`;
if (!recordMap[key]) recordMap[key] = [];
recordMap[key].push(r);
});
return this.checklistList.map(cl => {
const dayRecords = recordMap[`${cl.checkId}_1`] || [];
const nightRecords = recordMap[`${cl.checkId}_2`] || [];
const expectedCount = this.daysInRange * 4;
const actualCount = dayRecords.length + nightRecords.length;
const passCount = [...dayRecords, ...nightRecords].filter(r => r.runStatus === 1).length;
const failCount = [...dayRecords, ...nightRecords].filter(r => r.runStatus === 2).length;
return {
checkId: cl.checkId,
partName: cl.partName,
checkContent: cl.checkContent,
dayRecords,
nightRecords,
expectedCount,
actualCount,
diff: actualCount - expectedCount,
passCount,
failCount,
};
});
},
summary() {
const days = this.daysInRange || 1;
const expectedTotal = this.checklistList.length * days * 4;
const totalCount = this.records.length;
const passCount = this.records.filter(r => r.runStatus === 1).length;
const failCount = this.records.filter(r => r.runStatus === 2).length;
return {
partCount: this.partList.length,
checklistCount: this.checklistList.length,
totalCount,
expectedTotal,
passCount,
failCount,
passRate: totalCount > 0 ? (passCount / totalCount * 100).toFixed(1) + "%" : "0%",
completionRate: expectedTotal > 0 ? (totalCount / expectedTotal * 100).toFixed(1) + "%" : "0%",
};
},
daysInRange() {
if (!this.dateRange || this.dateRange.length !== 2) return 1;
const start = new Date(this.dateRange[0]);
const end = new Date(this.dateRange[1]);
return Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
},
inspectorSummary() {
const map = {};
this.records.forEach(r => {
const name = r.inspector || '未指定';
if (!map[name]) {
map[name] = { inspector: name, total: 0, pass: 0, fail: 0 };
}
map[name].total++;
if (r.runStatus === 1) map[name].pass++;
else if (r.runStatus === 2) map[name].fail++;
});
return Object.values(map).map(p => ({
...p,
passRate: p.total > 0 ? (p.pass / p.total * 100).toFixed(1) + '%' : '0%',
}));
},
personSummary() {
const personMap = {};
this.partList.forEach(p => {
const person = p.responsiblePerson || '未指定';
if (!personMap[person]) {
personMap[person] = { responsiblePerson: person, partIds: new Set(), checkIds: new Set(), recordIds: new Set(), passIds: new Set(), failIds: new Set() };
}
personMap[person].partIds.add(p.partId);
(p.checklistList || []).forEach(cl => personMap[person].checkIds.add(cl.checkId));
});
this.records.forEach(r => {
for (const person of Object.values(personMap)) {
if (person.checkIds.has(r.checkId)) {
person.recordIds.add(r.recordId);
if (r.runStatus === 1) person.passIds.add(r.recordId);
if (r.runStatus === 2) person.failIds.add(r.recordId);
break;
}
}
});
return Object.values(personMap).map(p => ({
responsiblePerson: p.responsiblePerson,
partCount: p.partIds.size,
checkCount: p.checkIds.size,
totalInspections: p.recordIds.size,
expectedTotal: p.checkIds.size * this.daysInRange * 4,
passCount: p.passIds.size,
failCount: p.failIds.size,
completionRate: p.checkIds.size * this.daysInRange * 4 > 0
? Math.round(p.recordIds.size / (p.checkIds.size * this.daysInRange * 4) * 100)
: 0,
}));
},
},
methods: {
getToday() {
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
},
cellStyle({ columnIndex }) {
if (columnIndex === 2 || columnIndex === 3) return { fontSize: "20px", padding: "4px" };
},
handleCellEnter(row, column) {},
handleCellLeave(row, column) {},
async handleQuery() {
if (!this.dateRange || this.dateRange.length !== 2) return;
this.loading = true;
try {
const partParams = {};
const productionLine = this.productionLine;
if (productionLine) partParams.productionLine = productionLine;
const recordParams = {
startInspectTime: this.dateRange[0] + ' 00:00:00',
endInspectTime: this.dateRange[1] + ' 23:59:59',
pageSize: 9999,
};
if (productionLine) recordParams.productionLine = productionLine;
const [partRes, recordRes] = await Promise.all([
listEquipmentPart(partParams),
listEquipmentInspectionRecord(recordParams),
]);
if (partRes.code === 200) this.partList = partRes.rows || [];
if (recordRes.code === 200) this.records = recordRes.rows || [];
} catch (e) {
console.error("查询失败", e);
} finally {
this.loading = false;
}
},
handleSubmitForApproval() {
if (!this.dateRange || this.dateRange.length !== 2) {
this.$modal.msgWarning("请选择时间段");
return;
}
if (!this.productionLine) {
this.$modal.msgWarning("请选择产线");
return;
}
this.$modal.confirm('确认将该时间段"' + this.dateRange[0] + '至' + this.dateRange[1] + '"的巡检日报提交审批吗?').then(() => {
this.loading = true;
return addEquipmentInspectionApproval({
productionLine: this.productionLine,
insStartTime: this.dateRange[0] + ' 00:00:00',
insEndTime: this.dateRange[1] + ' 23:59:59',
});
}).then(() => {
this.$modal.msgSuccess("送检成功");
this.loading = false;
}).catch(() => {
this.loading = false;
});
},
async loadLineList() {
try {
const res = await listProductionLine({ pageSize: 999 });
if (res.rows) this.lineList = res.rows;
if (!this.hasRouteQuery && this.lineList.length > 0) {
const suanYa = this.lineList.find(l => l.lineName === '酸轧线');
this.productionLine = suanYa ? suanYa.lineId : this.lineList[0].lineId;
}
this.handleQuery();
} catch (e) { console.error('加载产线列表失败', e); }
},
},
mounted() {
this.loadLineList();
},
};
</script>
<style scoped>
.result-icon {
display: inline-block;
font-size: 22px;
font-weight: bold;
line-height: 1;
}
.result-pass { color: #67c23a; }
.result-fail { color: #f56c6c; }
.result-none { color: #c0c4cc; font-size: 16px; }
.diff-over { color: #e6a23c; font-weight: bold; }
.diff-under { color: #f56c6c; font-weight: bold; }
.diff-ok { color: #67c23a; }
.section-card {
margin-bottom: 16px;
}
</style>
<style>
.inspect-tooltip {
max-width: 400px;
line-height: 1.6;
font-size: 13px;
}
</style>