<template>
|
<div>
|
<PanelHeader title="质量指标合格分析" />
|
<div class="main-panel panel-item-customers">
|
<div v-for="section in sections" :key="section.key" class="inspect-block">
|
<div class="filters-row">
|
<div class="filters-row-left">
|
<span></span>
|
<p>{{ section.title }}</p>
|
</div>
|
<DateTypeSwitch v-model="section.dateType" @change="(v) => handleDateTypeChange(section.key, v)" />
|
</div>
|
|
<div class="inspect-body">
|
<div class="ring">
|
<Echarts :chartStyle="ringChartStyle" :series="buildRingSeries(section)" :tooltip="ringTooltip"
|
:legend="{ show: false }" :options="ringOptions" />
|
</div>
|
|
<div class="stats">
|
<div class="stat-row">
|
<div class="stat-left">
|
<span class="dot dot-qualified"></span>
|
<span class="stat-label">合格数</span>
|
</div>
|
<div class="stat-right">
|
<span class="stat-value">{{ section.qualifiedCount }}</span>
|
<span class="stat-percent">{{ formatPercent(section.qualifiedRate) }}</span>
|
</div>
|
</div>
|
<div class="stat-row">
|
<div class="stat-left">
|
<span class="dot dot-unqualified"></span>
|
<span class="stat-label">不合格数</span>
|
</div>
|
<div class="stat-right">
|
<span class="stat-value">{{ section.unqualifiedCount }}</span>
|
<span class="stat-percent">{{ formatPercent(section.unqualifiedRate) }}</span>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script setup>
|
import { reactive, onMounted } from 'vue'
|
import Echarts from '@/components/Echarts/echarts.vue'
|
import PanelHeader from './PanelHeader.vue'
|
import DateTypeSwitch from './DateTypeSwitch.vue'
|
import { rawMaterialDetection, processDetection, factoryDetection } from '@/api/viewIndex.js'
|
import { usePolling } from '@/hooks/usePolling.js'
|
|
const QUALIFIED_COLOR = '#4EE4FF'
|
const UNQUALIFIED_COLOR = '#3378FF'
|
const TRACK_COLOR = 'rgba(78, 228, 255, 0.12)'
|
|
const apiMap = {
|
raw: rawMaterialDetection,
|
process: processDetection,
|
final: factoryDetection,
|
}
|
|
|
const fetchSectionData = async (section) => {
|
const api = apiMap[section.key]
|
if (!api) return
|
|
try {
|
const res = await api({
|
type: section.dateType,
|
})
|
|
if (res?.code === 200 && res?.data) {
|
const data = res.data
|
section.qualifiedCount = Number(data.qualifiedCount || 0)
|
section.unqualifiedCount = Number(data.unqualifiedCount || 0)
|
section.qualifiedRate = Number(data.qualifiedRate || 0)
|
section.unqualifiedRate = Number(data.unqualifiedRate || 0)
|
}
|
} catch (err) {
|
console.error(`${section.key} 接口请求失败`, err)
|
}
|
}
|
|
|
const sections = reactive([
|
{
|
key: 'raw',
|
title: '原材料检测',
|
dateType: 1,
|
qualifiedCount: 0,
|
unqualifiedCount: 0,
|
qualifiedRate: 0,
|
unqualifiedRate: 0,
|
},
|
{
|
key: 'process',
|
title: '过程检测',
|
dateType: 1,
|
qualifiedCount: 0,
|
unqualifiedCount: 0,
|
qualifiedRate: 0,
|
unqualifiedRate: 0,
|
},
|
{
|
key: 'final',
|
title: '成品出厂检测',
|
dateType: 1,
|
qualifiedCount: 0,
|
unqualifiedCount: 0,
|
qualifiedRate: 0,
|
unqualifiedRate: 0,
|
},
|
])
|
|
const ringChartStyle = {
|
width: '110px',
|
height: '110px',
|
}
|
|
const ringOptions = {
|
backgroundColor: 'transparent',
|
textStyle: { color: '#B8C8E0' },
|
}
|
|
const ringTooltip = {
|
show: false,
|
}
|
|
const calcRates = (qualifiedCount, unqualifiedCount) => {
|
const total = Number(qualifiedCount || 0) + Number(unqualifiedCount || 0)
|
if (total <= 0) return { qualifiedRate: 0, unqualifiedRate: 0 }
|
const qualifiedRate = Math.round((Number(qualifiedCount || 0) / total) * 100)
|
const unqualifiedRate = Math.max(0, 100 - qualifiedRate)
|
return { qualifiedRate, unqualifiedRate }
|
}
|
|
const formatPercent = (v) => `${Number(v || 0)}%`
|
|
const buildRingSeries = (section) => {
|
const qualified = Number(section.qualifiedCount || 0)
|
const unqualified = Number(section.unqualifiedCount || 0)
|
const total = qualified + unqualified
|
|
return [
|
{
|
type: 'pie',
|
radius: ['68%', '82%'],
|
center: ['50%', '50%'],
|
silent: true,
|
label: { show: false },
|
labelLine: { show: false },
|
itemStyle: { color: TRACK_COLOR },
|
data: [1],
|
},
|
{
|
name: section.title,
|
type: 'pie',
|
radius: ['68%', '82%'],
|
center: ['50%', '50%'],
|
silent: true,
|
label: { show: false },
|
labelLine: { show: false },
|
startAngle: 90,
|
clockwise: true,
|
minAngle: total > 0 ? 8 : 0,
|
itemStyle: {
|
borderColor: 'rgba(10, 28, 58, 0.95)',
|
borderWidth: 2,
|
},
|
data: [
|
{
|
value: qualified,
|
name: '合格数',
|
itemStyle: {
|
color: QUALIFIED_COLOR,
|
shadowBlur: 16,
|
shadowColor: 'rgba(78, 228, 255, 0.45)',
|
},
|
},
|
{
|
value: unqualified,
|
name: '不合格数',
|
itemStyle: {
|
color: UNQUALIFIED_COLOR,
|
shadowBlur: 10,
|
shadowColor: 'rgba(51, 120, 255, 0.35)',
|
},
|
},
|
],
|
},
|
{
|
type: 'pie',
|
radius: ['52%', '56%'],
|
center: ['50%', '50%'],
|
silent: true,
|
label: { show: false },
|
labelLine: { show: false },
|
itemStyle: { color: 'rgba(0, 127, 255, 0.22)' },
|
data: [1],
|
},
|
]
|
}
|
|
const handleDateTypeChange = (key, dateType) => {
|
const section = sections.find((s) => s.key === key)
|
if (!section) return
|
section.dateType = dateType
|
// 切换日期类型时重新获取数据
|
fetchSectionData(section)
|
}
|
|
// 组件挂载时获取所有section的数据
|
onMounted(() => {
|
sections.forEach((section) => {
|
fetchSectionData(section)
|
})
|
})
|
|
// 启动轮询,每分钟刷新一次数据
|
usePolling(() => {
|
sections.forEach((section) => {
|
fetchSectionData(section)
|
})
|
})
|
</script>
|
|
<style scoped lang="scss">
|
.main-panel {
|
display: flex;
|
flex-direction: column;
|
height: 100%;
|
gap: 0;
|
}
|
|
.filters-row {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
gap: 12px;
|
margin-bottom: 10px;
|
|
.filters-row-left {
|
width: 50%;
|
color: white;
|
/* 用flex替代float,让子元素对齐更稳定 */
|
display: flex;
|
align-items: center;
|
|
span {
|
/* 核心:父级相对定位,作为伪元素基准 */
|
position: relative;
|
display: inline-block;
|
/* 给伪元素和文字留空间 */
|
padding-left: 22px;
|
/* 文字垂直居中 */
|
line-height: 23px;
|
margin-right: 8px;
|
|
&::after {
|
content: '';
|
display: inline-block;
|
width: 16px;
|
height: 16px;
|
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
|
background: #217AFF;
|
position: absolute;
|
top: 50%;
|
left: 0;
|
transform: translateY(-50%);
|
/* 确保菱形在渐变块上方 */
|
z-index: 1;
|
}
|
|
&::before {
|
content: '';
|
display: inline-block;
|
width: 18px;
|
height: 7px;
|
border-radius: 8px;
|
background: linear-gradient(360deg, rgba(33, 133, 255, 0.4) 0%, rgba(33, 221, 255, 0) 100%);
|
position: absolute;
|
top: 50%;
|
left: -1px;
|
/* 精准贴在菱形正下方 */
|
transform: translateY(calc(0% + 8px));
|
z-index: 0;
|
}
|
}
|
|
p {
|
width: 100px;
|
height: 23px;
|
/* 渐变起始色和菱形统一,更协调 */
|
background: linear-gradient(90deg, #217AFF 0%, rgba(33, 221, 255, 0) 100%);
|
/* 精准垂直居中 */
|
line-height: 26px;
|
text-align: center;
|
color: white;
|
/* 用高度的一半做圆角,确保左边是完美半圆 */
|
border-radius: 12px 0 0 12px;
|
/* 可选:加一点左内边距,让文字不贴边 */
|
padding-left: 4px;
|
}
|
}
|
}
|
|
.panel-item-customers {
|
border: 1px solid #1a58b0;
|
padding: 14px 18px;
|
width: 100%;
|
height: 958px;
|
box-sizing: border-box;
|
}
|
|
.inspect-block {
|
flex: 1 1 0;
|
min-height: 0;
|
display: flex;
|
flex-direction: column;
|
padding: 8px 0;
|
gap: 6px;
|
position: relative;
|
}
|
|
.inspect-block:not(:last-child)::after {
|
content: '';
|
position: absolute;
|
left: 0;
|
right: 0;
|
bottom: 0;
|
height: 1px;
|
background: linear-gradient(90deg, rgba(33, 122, 255, 0) 0%, rgba(33, 122, 255, 0.55) 50%, rgba(33, 122, 255, 0) 100%);
|
pointer-events: none;
|
}
|
|
.inspect-body {
|
flex: 1 1 auto;
|
min-height: 0;
|
display: flex;
|
justify-content: space-around;
|
align-items: center;
|
gap: 18px;
|
}
|
|
.ring {
|
width: 120px;
|
height: 120px;
|
flex: 0 0 120px;
|
position: relative;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
/* 外圈刻度(点状环) */
|
.ring::before {
|
content: '';
|
position: absolute;
|
inset: -8px;
|
border-radius: 50%;
|
background: repeating-conic-gradient(from 0deg,
|
rgba(78, 228, 255, 0.75) 0 1deg,
|
rgba(78, 228, 255, 0) 1deg 9deg);
|
-webkit-mask: radial-gradient(circle, transparent 62%, #000 63%);
|
mask: radial-gradient(circle, transparent 62%, #000 63%);
|
opacity: 0.35;
|
pointer-events: none;
|
}
|
|
/* 柔和发光背景 */
|
.ring::after {
|
content: '';
|
position: absolute;
|
inset: -20px;
|
border-radius: 50%;
|
background: radial-gradient(circle, rgba(78, 228, 255, 0.18) 0%, rgba(78, 228, 255, 0.06) 40%, rgba(0, 0, 0, 0) 70%);
|
filter: blur(0.2px);
|
pointer-events: none;
|
}
|
|
.stats {
|
width: 240px;
|
flex: 0 0 240px;
|
display: grid;
|
grid-template-rows: 1fr 1fr;
|
gap: 10px;
|
}
|
|
.stat-row {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
height: 100%;
|
padding: 10px 14px;
|
border-radius: 4px;
|
border: 1px solid rgba(78, 228, 255, 0.22);
|
background: linear-gradient(90deg, rgba(33, 122, 255, 0.28) 0%, rgba(10, 28, 58, 0.35) 55%, rgba(10, 28, 58, 0.2) 100%);
|
box-shadow: inset 0 0 18px rgba(16, 45, 95, 0.25);
|
}
|
|
.stat-left {
|
display: inline-flex;
|
align-items: center;
|
gap: 10px;
|
color: #b8c8e0;
|
font-size: 12px;
|
}
|
|
.dot {
|
width: 10px;
|
height: 10px;
|
border-radius: 2px;
|
display: inline-block;
|
box-shadow: 0 0 10px rgba(78, 228, 255, 0.25);
|
}
|
|
.dot-qualified {
|
background: rgba(184, 200, 224, 0.85);
|
}
|
|
.dot-unqualified {
|
background: #4ee4ff;
|
}
|
|
.stat-right {
|
display: inline-flex;
|
align-items: baseline;
|
gap: 14px;
|
}
|
|
.stat-value {
|
color: #ffffff;
|
font-size: 14px;
|
font-weight: 600;
|
min-width: 40px;
|
text-align: right;
|
text-shadow: 0 0 10px rgba(78, 228, 255, 0.15);
|
}
|
|
.stat-percent {
|
color: rgba(184, 200, 224, 0.95);
|
font-size: 12px;
|
min-width: 40px;
|
text-align: right;
|
}
|
|
/* 让切换按钮更贴近截图(更紧凑) */
|
:deep(.date-type-switch .el-radio-button__inner) {
|
padding: 4px 16px;
|
font-size: 12px;
|
}
|
</style>
|