<template>
|
<div class="app-container">
|
<el-row>
|
<el-col :span="4">
|
<el-radio-group v-model="dateType" @change="changeDateType">
|
<el-radio-button label="1">本周</el-radio-button>
|
<el-radio-button label="2">本月</el-radio-button>
|
<el-radio-button label="3">今年</el-radio-button>
|
</el-radio-group>
|
</el-col>
|
<el-col :span="20">
|
<el-form ref="entity" size="small" :inline="true">
|
<el-form-item style="width: 20%;">
|
<el-date-picker v-model="datePicker" end-placeholder="结束日期" format="yyyy-MM-dd" placeholder="选择日期"
|
range-separator="至" size="small" start-placeholder="开始日期" type="daterange" style="width: 100%;"
|
value-format="yyyy-MM-dd" @change="changeDatePicker">
|
</el-date-picker>
|
</el-form-item>
|
<el-form-item label="样品名称" prop="sampleName">
|
<el-input v-model="sampleName" clearable placeholder="请输入样品名称" size="small" @change="changeDate"></el-input>
|
</el-form-item>
|
<el-form-item label="型号" prop="modelName">
|
<el-input v-model="modelName" clearable placeholder="请输入型号" size="small" @change="changeDate"></el-input>
|
</el-form-item>
|
<el-form-item label="供应商名称" prop="supplierName">
|
<el-input v-model="supplierName" clearable placeholder="请输入供应商名称" size="small"
|
@change="changeDate"></el-input>
|
</el-form-item>
|
</el-form>
|
</el-col>
|
</el-row>
|
<el-row :gutter="20">
|
<el-col :span="8">
|
<div class="pie-card" :class="{ active: currentMaterialProp === '00Raw' }" @click="updateTitle('原材料')">
|
<div class="title"><span style="color: #005BAC;font-weight: bold;">原材料</span>合格率</div>
|
<Echarts ref="chart" :legend="pieLegend" :series="rawPieSeries" :tooltip="pieTooltip" style="height: 36vh;">
|
</Echarts>
|
</div>
|
</el-col>
|
<el-col :span="8">
|
<div class="pie-card" :class="{ active: currentMaterialProp === '01Cu,02Al' }" @click="updateTitle('导体')">
|
<div class="title"><span style="color: #005BAC;font-weight: bold;">导体</span>合格率</div>
|
<Echarts ref="chart" :legend="pieLegend" :series="conductorPieSeries" :tooltip="pieTooltip"
|
style="height: 36vh;">
|
</Echarts>
|
</div>
|
</el-col>
|
<el-col :span="8">
|
<div class="pie-card" :class="{ active: currentMaterialProp === '04Dlan' }" @click="updateTitle('电缆')">
|
<div class="title"><span style="color: #005BAC;font-weight: bold;">电缆</span>合格率</div>
|
<Echarts ref="chart" :legend="pieLegend" :series="dlanPieSeries" :tooltip="pieTooltip" style="height: 36vh;">
|
</Echarts>
|
</div>
|
</el-col>
|
</el-row>
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<div class="inspection-card">
|
<div class="title" style="display:flex;align-items:center;justify-content:space-between;">
|
<div>
|
<span style="color: #005BAC;font-weight: bold;">{{ inspectionTitle }}</span>合格率趋势
|
</div>
|
<el-button type="text" icon="el-icon-download" @click="downloadBarChart">
|
下载
|
</el-button>
|
</div>
|
<Echarts ref="barChart" :barColors="barColors" :grid="grid" :legend="legend" :lineColors="lineColors"
|
:options="echartsOptions" :series="echartsSeries" :tooltip="tooltip" :xAxis="xAxis" :yAxis="yAxis"
|
style="height: 40vh;">
|
</Echarts>
|
|
</div>
|
</el-col>
|
<el-col :span="12">
|
<div class="inspection-card">
|
<div class="title" style="display:flex;align-items:center;justify-content:space-between;">
|
<div>
|
<span style="color: #005BAC;font-weight: bold;">{{ inspectionTitle }}</span>明细数据
|
</div>
|
<el-button type="text" icon="el-icon-download" @click="downloadTable">
|
下载
|
</el-button>
|
</div>
|
<lims-table :tableData="tableData" :column="tableColumn" :tableLoading="tableLoading"
|
:height="'calc(40vh - 40px)'" :show-summary="true" :summary-method="getSummaries"></lims-table>
|
</div>
|
</el-col>
|
</el-row>
|
</div>
|
</template>
|
|
<script>
|
import Echarts from "@/components/echarts/echarts.vue";
|
import limsTable from "@/components/Table/lims-table.vue";
|
import {
|
getOrderTypeCookie,
|
getRawPassRateByBarChart,
|
getRawPassRateByCake,
|
getRawUpMonth,
|
getMaterialPropTable,
|
exportSupplierExcel
|
} from "@/api/statisticalCharts/dataAnalysis";
|
|
export default {
|
name: "QualificationRateStatistics",
|
// import 引入的组件需要注入到对象中才能使用
|
components: { Echarts, limsTable },
|
data() {
|
// 这里存放数据
|
return {
|
dateType: '1',
|
datePicker: [],
|
beginDate: '',
|
endDate: '',
|
sampleName: '',
|
modelName: '',
|
supplierName: '',
|
inspectionTitle: '原材料',
|
currentMaterialProp: '00Raw',
|
echartsOptions: {
|
title: {
|
text: '',
|
left: 'center',
|
top: 10,
|
textStyle: {
|
fontSize: 14,
|
fontWeight: 'bold'
|
}
|
}
|
},
|
xAxis: [
|
{
|
type: 'category',
|
data: [],
|
axisPointer: {
|
type: 'shadow'
|
}
|
}
|
],
|
yAxis: [
|
{
|
type: 'value',
|
name: '总数',
|
min: 0,
|
},
|
{
|
type: 'value',
|
name: '合格率',
|
min: 0,
|
max: 100,
|
axisLabel: {
|
formatter: '{value} %'
|
}
|
}
|
],
|
echartsSeries: [
|
{
|
name: '合格',
|
type: 'bar',
|
data: [],
|
label: {
|
show: true,
|
position: 'inside'
|
}
|
},
|
{
|
name: '不合格',
|
type: 'bar',
|
data: [],
|
label: {
|
show: true,
|
position: 'inside'
|
}
|
},
|
{
|
name: '合格率',
|
type: 'line',
|
smooth: true,
|
yAxisIndex: 1,
|
data: [],
|
label: {
|
show: true,
|
formatter: (v) => v.value + '%'
|
}
|
}
|
],
|
grid: {
|
left: '3%',
|
right: '4%',
|
bottom: '15%',
|
containLabel: true
|
},
|
legend: {
|
data: ['合格', '不合格', '合格率'],
|
orient: 'horizontal',
|
bottom: 0,
|
itemGap: 20
|
},
|
tooltip: {
|
trigger: 'axis',
|
axisPointer: {
|
type: 'shadow'
|
}
|
},
|
lineColors: ['#91A0FC'],
|
barColors: ['#13ce66', '#F56C6C'], // 合格绿 / 不合格红
|
barColors2: ['#A4EEDA'],
|
pieTooltip: {
|
trigger: 'item'
|
},
|
pieLegend: {
|
orient: 'horizontal',
|
bottom: 0,
|
},
|
rawPieSeries: [
|
{
|
name: 'Access From',
|
type: 'pie',
|
radius: '70%',
|
center: ['50%', '50%'],
|
avoidLabelOverlap: false,
|
itemStyle: {
|
borderColor: '#fff',
|
borderWidth: 2
|
},
|
label: {
|
alignTo: 'edge',
|
formatter: '{name|{b}}\n{time|{c}}',
|
edgeDistance: 10,
|
lineHeight: 15,
|
rich: {
|
time: {
|
fontSize: 10,
|
color: '#999'
|
}
|
}
|
},
|
labelLine: {
|
length: 20,
|
length2: 40
|
},
|
data: [
|
{ value: 0, name: '不合格数量', itemStyle: { color: '#F56C6C' } },
|
{ value: 0, name: '合格数量', itemStyle: { color: '#67C23A' } }
|
]
|
}
|
],
|
conductorPieSeries: [
|
{
|
name: 'Access From',
|
type: 'pie',
|
radius: '70%',
|
center: ['50%', '50%'],
|
avoidLabelOverlap: false,
|
itemStyle: {
|
borderColor: '#fff',
|
borderWidth: 2
|
},
|
label: {
|
alignTo: 'edge',
|
formatter: '{name|{b}}\n{time|{c}}',
|
edgeDistance: 10,
|
lineHeight: 15,
|
rich: {
|
time: {
|
fontSize: 10,
|
color: '#999'
|
}
|
},
|
},
|
labelLine: {
|
length: 20,
|
length2: 40,
|
},
|
data: [
|
{ value: 0, name: '不合格数量', itemStyle: { color: '#F56C6C' } },
|
{ value: 0, name: '合格数量', itemStyle: { color: '#67C23A' } },
|
]
|
}
|
],
|
dlanPieSeries: [
|
{
|
name: 'Access From',
|
type: 'pie',
|
radius: '70%',
|
center: ['50%', '50%'],
|
avoidLabelOverlap: false,
|
itemStyle: {
|
borderColor: '#fff',
|
borderWidth: 2
|
},
|
label: {
|
alignTo: 'edge',
|
formatter: '{name|{b}}\n{time|{c}}',
|
edgeDistance: 10,
|
lineHeight: 15,
|
rich: {
|
time: {
|
fontSize: 10,
|
color: '#999'
|
}
|
},
|
},
|
labelLine: {
|
length: 20,
|
length2: 40,
|
},
|
data: [
|
{ value: 0, name: '不合格数量', itemStyle: { color: '#F56C6C' } },
|
{ value: 0, name: '合格数量', itemStyle: { color: '#67C23A' } },
|
]
|
}
|
],
|
materialPieSeries1: [
|
{
|
name: 'Access From',
|
type: 'pie',
|
radius: ['40%', '70%'],
|
right: '22%',
|
avoidLabelOverlap: false,
|
itemStyle: {
|
borderColor: '#fff',
|
borderWidth: 2
|
},
|
label: {
|
alignTo: 'edge',
|
formatter: '{name|{b}}\n{time|{c}}',
|
edgeDistance: 10,
|
lineHeight: 15,
|
rich: {
|
time: {
|
fontSize: 10,
|
color: '#999'
|
}
|
},
|
},
|
labelLine: {
|
length: 20,
|
length2: 50,
|
},
|
data: [
|
{ value: 0, name: '委托检验' },
|
{ value: 0, name: '进厂检验' },
|
{ value: 0, name: '季度检验' },
|
{ value: 0, name: '抽样' },
|
]
|
}
|
],
|
barLegend: {},
|
chartStyle: {
|
width: '90%',
|
height: '100%',
|
},
|
chartStyle2: {
|
width: '90%',
|
height: '80%',
|
},
|
xAxis1: [
|
{
|
type: 'value',
|
min: 0,
|
max: 100,
|
axisLabel: {
|
formatter: '{value} %'
|
}
|
}
|
],
|
yAxis1: [
|
{
|
type: 'category',
|
data: []
|
}
|
],
|
barSeries: [
|
{
|
type: 'bar',
|
data: [],
|
tooltip: {
|
valueFormatter: function (value) {
|
return value + ' %';
|
}
|
},
|
label: {
|
show: true,
|
formatter: (params) => Math.round(params.value * 100) / 100 + '%'
|
}
|
},
|
],
|
tableData: [],
|
tableLoading: false,
|
tableColumn: [
|
{ label: '供应商名称', prop: 'supplierName', minWidth: '200px' },
|
{ label: '到货批次', prop: 'totalBatch', minWidth: '100px' },
|
{ label: '不合格批次', prop: 'unqualifiedBatch', minWidth: '100px' },
|
{
|
label: '合格率(%)',
|
prop: 'passRate',
|
minWidth: '100px',
|
formatData: (val) => (val != null ? val + '%' : '0%')
|
}
|
],
|
rawPassRate: '',
|
conductorPassRate: '',
|
dlanPassRate: '',
|
sum: '',
|
}
|
},
|
|
mounted() {
|
this.setBarChartTitle();
|
this.getBar();
|
this.getRawPass();
|
this.getOrderType();
|
this.getPassRateCom();
|
this.getTableData();
|
},
|
|
// 方法集合
|
methods: {
|
// 获取合格率图表数据
|
|
setBarChartTitle() {
|
this.echartsOptions.title.text = `${this.inspectionTitle}合格率趋势`;
|
},
|
|
getBar() {
|
const types = this.currentMaterialProp.split(',');
|
const requests = types.map(t => {
|
const params = {
|
dateType: this.dateType,
|
beginDate: this.beginDate,
|
endDate: this.endDate,
|
sampleName: this.sampleName,
|
modelName: this.modelName,
|
supplierName: this.supplierName,
|
materialProp: t,
|
};
|
return getRawPassRateByBarChart(params);
|
});
|
|
Promise.all(requests).then((responses) => {
|
let dateMap = {}; // { date: { qualified, unQualified } }
|
|
responses.forEach(res => {
|
if (res.data && Array.isArray(res.data)) {
|
res.data.forEach(item => {
|
const date = item.searchTime;
|
if (!dateMap[date]) {
|
dateMap[date] = { qualified: 0, unQualified: 0 };
|
}
|
dateMap[date].qualified += (item.qualified || 0);
|
dateMap[date].unQualified += (item.unQualified || 0);
|
});
|
}
|
});
|
|
const sortedDates = Object.keys(dateMap).sort();
|
let qualifiedData = [];
|
let unQualifiedData = [];
|
let lineData = [];
|
let xAxis = [];
|
|
sortedDates.forEach(date => {
|
const { qualified, unQualified } = dateMap[date];
|
const total = qualified + unQualified;
|
const passRate = total > 0 ? (qualified / total * 100).toFixed(2) : 0;
|
|
xAxis.push(date);
|
qualifiedData.push(qualified);
|
unQualifiedData.push(unQualified);
|
lineData.push(parseFloat(passRate));
|
});
|
|
this.echartsSeries[0].data = qualifiedData;
|
this.echartsSeries[1].data = unQualifiedData;
|
this.echartsSeries[2].data = lineData;
|
this.xAxis[0].data = xAxis;
|
});
|
},
|
|
// 获取物料属性合格率图表数据
|
getRawPass() {
|
const materials = [
|
{ type: '00Raw', series: 'rawPieSeries', rate: 'rawPassRate' },
|
{ type: '01Cu,02Al', series: 'conductorPieSeries', rate: 'conductorPassRate' },
|
{ type: '04Dlan', series: 'dlanPieSeries', rate: 'dlanPassRate' }
|
];
|
|
materials.forEach(item => {
|
const types = item.type.split(',');
|
const requests = types.map(t => {
|
const params = {
|
dateType: this.dateType,
|
beginDate: this.beginDate,
|
endDate: this.endDate,
|
sampleName: this.sampleName,
|
modelName: this.modelName,
|
supplierName: this.supplierName,
|
materialProp: t
|
};
|
return getRawPassRateByCake(params);
|
});
|
|
Promise.all(requests).then((responses) => {
|
let unQualified = 0;
|
let qualified = 0;
|
responses.forEach(res => {
|
if (res.data) {
|
unQualified += (res.data.unQualified || 0);
|
qualified += (res.data.qualified || 0);
|
}
|
});
|
|
if (qualified + unQualified > 0) {
|
this[item.series][0].data[0].value = unQualified;
|
this[item.series][0].data[1].value = qualified;
|
const passRate = (qualified / (qualified + unQualified) * 100).toFixed(2);
|
this[item.rate] = passRate + '%';
|
} else {
|
this[item.series][0].data[0].value = 0;
|
this[item.series][0].data[1].value = 0;
|
this[item.rate] = '';
|
}
|
});
|
});
|
},
|
// 获取本月检验类型数量
|
getOrderType() {
|
getOrderTypeCookie().then((res) => {
|
this.materialPieSeries1[0].data[0].value = res.data.customer // 委托
|
this.materialPieSeries1[0].data[1].value = res.data.enter // 进厂
|
this.materialPieSeries1[0].data[2].value = res.data.quarterly // 季度
|
this.materialPieSeries1[0].data[3].value = res.data.spotCheck // 抽样
|
})
|
},
|
// 本月与上月合格率对比
|
getPassRateCom() {
|
getRawUpMonth().then((res) => {
|
let month = []
|
let barData = []
|
res.data.forEach(item => {
|
month.push(item.month)
|
barData.push(item.passRate)
|
})
|
this.yAxis1[0].data = month
|
this.barSeries[0].data = barData
|
})
|
},
|
changeDate() {
|
this.getBar()
|
this.getRawPass()
|
this.getTableData()
|
// this.getOrderType()
|
// this.getPassRateCom()
|
},
|
|
changeDateType() {
|
this.datePicker = []
|
this.beginDate = ''
|
this.endDate = ''
|
this.getBar()
|
this.getRawPass()
|
this.getTableData()
|
},
|
|
changeDatePicker(val) {
|
if (val) {
|
this.beginDate = val[0] + ' 00:00:00'
|
this.endDate = val[1] + ' 23:59:59'
|
} else {
|
this.beginDate = ''
|
this.endDate = ''
|
}
|
this.getBar()
|
this.getRawPass()
|
this.getTableData()
|
},
|
|
updateTitle(title) {
|
this.inspectionTitle = title;
|
|
const typeMap = {
|
'原材料': '00Raw',
|
'导体': '01Cu,02Al',
|
'电缆': '04Dlan',
|
};
|
|
this.currentMaterialProp = typeMap[title] || '00Raw';
|
|
this.setBarChartTitle();
|
this.getBar();
|
this.getTableData();
|
},
|
|
|
getTableData() {
|
this.tableLoading = true;
|
const types = this.currentMaterialProp.split(',');
|
const requests = types.map(t => {
|
const params = {
|
dateType: this.dateType,
|
beginDate: this.beginDate,
|
endDate: this.endDate,
|
sampleName: this.sampleName,
|
modelName: this.modelName,
|
supplierName: this.supplierName,
|
materialProp: t,
|
};
|
return getMaterialPropTable(params);
|
});
|
|
Promise.all(requests).then((responses) => {
|
let supplierMap = {}; // { supplierName: { totalBatch, unqualifiedBatch } }
|
|
responses.forEach(res => {
|
const data = Array.isArray(res.data) ? res.data : (res.data && res.data.records ? res.data.records : []);
|
data.forEach(item => {
|
const name = item.supplierName || '未知供应商';
|
if (!supplierMap[name]) {
|
supplierMap[name] = { totalBatch: 0, unqualifiedBatch: 0 };
|
}
|
supplierMap[name].totalBatch += (item.totalBatch || 0);
|
supplierMap[name].unqualifiedBatch += (item.unqualifiedBatch || 0);
|
});
|
});
|
|
const tableData = Object.keys(supplierMap).map(name => {
|
const { totalBatch, unqualifiedBatch } = supplierMap[name];
|
const passRate = totalBatch > 0 ? ((totalBatch - unqualifiedBatch) / totalBatch * 100).toFixed(2) : 0;
|
return {
|
supplierName: name,
|
totalBatch,
|
unqualifiedBatch,
|
passRate
|
};
|
});
|
|
// Sort by totalBatch descending
|
tableData.sort((a, b) => b.totalBatch - a.totalBatch);
|
this.tableData = tableData;
|
this.tableLoading = false;
|
}).catch(() => {
|
this.tableLoading = false;
|
});
|
},
|
|
// 下载柱状图图片
|
downloadBarChart() {
|
if (!this.$refs.barChart) {
|
this.$message.warning('图表尚未加载完成');
|
return;
|
}
|
|
const title = `${this.inspectionTitle}_合格率趋势`;
|
this.$refs.barChart.downloadImage(title);
|
},
|
|
// 下载表格数据
|
downloadTable() {
|
if (!this.tableData || this.tableData.length === 0) {
|
this.$message.warning("没有可导出的数据");
|
return;
|
}
|
|
exportSupplierExcel(this.tableData)
|
.then(res => {
|
if (res.type && res.type.includes("application/json")) {
|
const fileReader = new FileReader();
|
fileReader.onload = () => {
|
const msg = JSON.parse(fileReader.result);
|
this.$message.error(msg.msg || "导出失败");
|
};
|
fileReader.readAsText(res);
|
return;
|
}
|
|
const blob = new Blob([res], {
|
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
});
|
|
const fileName = `${this.inspectionTitle}合格率统计.xlsx`;
|
|
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
|
window.navigator.msSaveOrOpenBlob(blob, fileName);
|
} else {
|
const link = document.createElement("a");
|
link.href = window.URL.createObjectURL(blob);
|
link.download = fileName;
|
document.body.appendChild(link);
|
link.click();
|
document.body.removeChild(link);
|
window.URL.revokeObjectURL(link.href);
|
}
|
})
|
.catch(() => {
|
this.$message.error("导出失败");
|
});
|
},
|
|
getSummaries(param) {
|
const { columns, data } = param;
|
const sums = [];
|
columns.forEach((column, index) => {
|
if (index === 0) {
|
sums[index] = '合计';
|
return;
|
}
|
|
const property = column.property;
|
if (property === 'supplierName') {
|
sums[index] = '';
|
return;
|
}
|
|
if (property === 'totalBatch' || property === 'unqualifiedBatch') {
|
const values = data.map(item => Number(item[property]));
|
sums[index] = values.reduce((prev, curr) => {
|
const value = Number(curr);
|
if (!isNaN(value)) {
|
return prev + curr;
|
} else {
|
return prev;
|
}
|
}, 0);
|
} else if (property === 'passRate') {
|
const totalBatch = data.reduce((prev, curr) => prev + (Number(curr.totalBatch) || 0), 0);
|
const unqualifiedBatch = data.reduce((prev, curr) => prev + (Number(curr.unqualifiedBatch) || 0), 0);
|
sums[index] = totalBatch > 0 ? ((totalBatch - unqualifiedBatch) / totalBatch * 100).toFixed(2) + '%' : '0%';
|
} else {
|
sums[index] = '';
|
}
|
});
|
|
return sums;
|
},
|
},
|
}
|
</script>
|
|
<style scoped>
|
.title {
|
padding: 10px 0 0 20px;
|
}
|
|
.table {
|
padding: 0 10px 10px;
|
}
|
|
.pie-card {
|
width: 100%;
|
background: #FFFFFF;
|
margin-top: 10px;
|
position: relative;
|
cursor: pointer;
|
border-radius: 8px;
|
transition:
|
transform 0.25s ease,
|
box-shadow 0.25s ease;
|
}
|
|
.pie-card.active {
|
transform: translateY(-3px) scale(1.03);
|
box-shadow: 0 5px 10px rgba(0, 91, 172, 0.25);
|
}
|
|
.data {
|
position: absolute;
|
font-size: 20px;
|
transform: translate(-50%, -50%);
|
left: 50%;
|
top: 50%;
|
z-index: 1;
|
}
|
|
.inspection-card {
|
width: 100%;
|
background: #FFFFFF;
|
margin-top: 10px;
|
}
|
</style>
|