<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'
|
|
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: 3,
|
qualifiedCount: 0,
|
unqualifiedCount: 0,
|
qualifiedRate: 0,
|
unqualifiedRate: 0,
|
},
|
{
|
key: 'process',
|
title: '过程检测',
|
dateType: 3,
|
qualifiedCount: 0,
|
unqualifiedCount: 0,
|
qualifiedRate: 0,
|
unqualifiedRate: 0,
|
},
|
{
|
key: 'final',
|
title: '成品出厂检测',
|
dateType: 3,
|
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)
|
})
|
})
|
</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;
|
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;
|
animation: diamondPulse 2s ease-in-out infinite;
|
}
|
|
&::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;
|
animation: titleShimmer 3s ease-in-out infinite;
|
}
|
}
|
}
|
|
@keyframes diamondPulse {
|
0%, 100% { opacity: 0.8; }
|
50% { opacity: 1; box-shadow: 0 0 10px rgba(33, 133, 255, 0.5); }
|
}
|
|
@keyframes titleShimmer {
|
0%, 100% { opacity: 1; }
|
50% { opacity: 0.85; }
|
}
|
|
.panel-item-customers {
|
border: 1px solid #1a58b0;
|
padding: 14px 18px;
|
width: 100%;
|
height: 958px;
|
box-sizing: border-box;
|
position: relative;
|
}
|
|
/* 面板角落装饰 */
|
.panel-item-customers::before,
|
.panel-item-customers::after {
|
content: '';
|
position: absolute;
|
width: 20px;
|
height: 20px;
|
border-color: rgba(0, 212, 255, 0.5);
|
border-style: solid;
|
pointer-events: none;
|
}
|
|
.panel-item-customers::before {
|
top: -1px;
|
left: -1px;
|
border-width: 2px 0 0 2px;
|
}
|
|
.panel-item-customers::after {
|
bottom: -1px;
|
right: -1px;
|
border-width: 0 2px 2px 0;
|
}
|
|
.inspect-block {
|
flex: 1 1 0;
|
min-height: 0;
|
display: flex;
|
flex-direction: column;
|
padding: 8px 0;
|
gap: 6px;
|
position: relative;
|
animation: blockFadeIn 0.5s ease-out both;
|
animation-delay: calc(var(--index, 0) * 0.1s);
|
}
|
|
@keyframes blockFadeIn {
|
from {
|
opacity: 0;
|
transform: translateX(-20px);
|
}
|
to {
|
opacity: 1;
|
transform: translateX(0);
|
}
|
}
|
|
.inspect-block:nth-child(1) { --index: 0; }
|
.inspect-block:nth-child(2) { --index: 1; }
|
.inspect-block:nth-child(3) { --index: 2; }
|
|
.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;
|
animation: ringFloat 4s ease-in-out infinite;
|
}
|
|
@keyframes ringFloat {
|
0%, 100% { transform: translateY(0); }
|
50% { transform: translateY(-3px); }
|
}
|
|
/* 外圈刻度(点状环)- 旋转动画 */
|
.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;
|
animation: ringRotate 30s linear infinite;
|
}
|
|
@keyframes ringRotate {
|
from { transform: rotate(0deg); }
|
to { transform: rotate(360deg); }
|
}
|
|
/* 柔和发光背景 - 脉冲动画 */
|
.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;
|
animation: ringGlow 3s ease-in-out infinite;
|
}
|
|
@keyframes ringGlow {
|
0%, 100% { opacity: 0.8; transform: scale(1); }
|
50% { opacity: 1; transform: scale(1.02); }
|
}
|
|
.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);
|
transition: all 0.3s ease;
|
position: relative;
|
overflow: hidden;
|
}
|
|
/* 统计行悬停效果 */
|
.stat-row:hover {
|
border-color: rgba(0, 212, 255, 0.4);
|
box-shadow: inset 0 0 18px rgba(16, 45, 95, 0.25), 0 0 15px rgba(0, 212, 255, 0.15);
|
}
|
|
/* 统计行底部光线 */
|
.stat-row::after {
|
content: '';
|
position: absolute;
|
bottom: 0;
|
left: 0;
|
right: 0;
|
height: 1px;
|
background: linear-gradient(90deg, transparent, rgba(0, 212, 255, 0.4), transparent);
|
opacity: 0;
|
transition: opacity 0.3s;
|
}
|
|
.stat-row:hover::after {
|
opacity: 1;
|
}
|
|
.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);
|
animation: dotPulse 2s ease-in-out infinite;
|
}
|
|
@keyframes dotPulse {
|
0%, 100% { box-shadow: 0 0 10px rgba(78, 228, 255, 0.25); }
|
50% { box-shadow: 0 0 15px rgba(78, 228, 255, 0.5); }
|
}
|
|
.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);
|
transition: all 0.3s ease;
|
}
|
|
.stat-row:hover .stat-value {
|
text-shadow: 0 0 15px rgba(78, 228, 255, 0.4);
|
transform: scale(1.05);
|
}
|
|
.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>
|