spring
2 天以前 b12b55a5ee1b34b5a3f9d21533fa9fc909207285
src/views/reportAnalysis/qualityAnalysis/components/left-top.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,448 @@
<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: 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)
  })
})
</script>
<style scoped>
.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: 960px;
  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>