Просмотр исходного кода

新增DeviceStatusPie组件;替换DeviceStatusTabs组件中的圆饼图;

画安 недель назад: 3
Родитель
Сommit
f7a72797b6
2 измененных файлов с 252 добавлено и 5 удалено
  1. 247 0
      src/components/ui/DeviceStatusPie.vue
  2. 5 5
      src/components/ui/DeviceStatusTabs.vue

+ 247 - 0
src/components/ui/DeviceStatusPie.vue

@@ -0,0 +1,247 @@
+<template>
+  <div class="screen-container">
+    <!-- 饼图区域 -->
+    <div class="tech-pie-chart">
+      <div class="chart-ring"></div>
+      <svg class="pie-svg" viewBox="0 0 500 500">
+        <circle class="pie-segment" v-for="(item, index) in chartData" :key="index"
+          ref="segments"
+          cx="250" cy="250" r="210"
+        />
+      </svg>
+      <div class="chart-center" v-if="showCenter">
+        <div class="center-total">{{ centerTitle || total }}</div>
+        <div class="center-label">{{ centerSubTitle || '设备总数' }}</div>
+      </div>
+    </div>
+
+    <!-- 右侧数据面板 -->
+    <div class="status-panel">
+      <div class="status-item" v-for="(item, index) in chartData" :key="index">
+        <div class="status-dot" :style="{ background: item.color }"></div>
+        <div class="status-text">{{ item.name }}</div>
+        <div class="status-num" :style="{ color: item.color }">{{ item.value }}</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'DeviceStatusPie',
+  props: {
+    chartData: {
+      type: Array,
+      required: true,
+      default: () => []
+    },
+    centerTitle: {
+      type: String,
+      default: ''
+    },
+    centerSubTitle: {
+      type: String,
+      default: ''
+    },
+    showCenter: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data() {
+    return {
+      radius: 210,
+      circumference: 0
+    };
+  },
+  computed: {
+    total() {
+      return this.chartData.reduce((sum, item) => sum + Number(item.value), 0);
+    }
+  },
+  watch: {
+    chartData: {
+      deep: true,
+      handler() {
+        this.$nextTick(() => this.drawChart());
+      }
+    }
+  },
+  mounted() {
+    this.circumference = 2 * Math.PI * this.radius;
+    this.$nextTick(() => this.drawChart());
+  },
+  methods: {
+    drawChart() {
+      const segments = this.$refs.segments;
+      if (!segments || !segments.length) return;
+
+      const total = this.total || 1;
+      let currentOffset = 0;
+
+      this.chartData.forEach((item, index) => {
+        const segment = segments[index];
+        if (!segment) return;
+        const percent = item.value / total;
+        const strokeLength = this.circumference * percent;
+
+        segment.style.stroke = item.color;
+        segment.style.strokeDasharray = `${strokeLength} ${this.circumference - strokeLength}`;
+        segment.style.strokeDashoffset = -currentOffset;
+        segment.style.animation = `draw 1.8s ease-out ${index * 0.25}s both`;
+        currentOffset += strokeLength;
+      });
+    }
+  }
+};
+</script>
+
+<style scoped>
+.screen-container {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  gap: clamp(10px, 2vw, 30px);
+  padding: 10px;
+}
+
+/* 饼图 */
+.tech-pie-chart {
+  position: relative;
+  width: auto;
+  height: 100%;
+  aspect-ratio: 1;
+  flex-shrink: 0;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.chart-ring {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  border-radius: 50%;
+  border: 1px solid rgba(0, 255, 120, 0.15);
+  box-shadow: 0 0 50px rgba(0, 255, 120, 0.15),
+    inset 0 0 40px rgba(255, 30, 80, 0.1);
+  animation: ringPulse 6s ease-in-out infinite;
+}
+.chart-ring::before {
+  content: "";
+  position: absolute;
+  inset: 15px;
+  border-radius: 50%;
+  border: 1px solid rgba(255, 50, 100, 0.15);
+  animation: ringPulse 6s ease-in-out infinite reverse;
+}
+
+.pie-svg {
+  width: 100%;
+  height: 100%;
+  transform: rotate(-90deg);
+  filter: drop-shadow(0 0 20px rgba(0, 255, 120, 0.2));
+}
+
+.pie-segment {
+  fill: none;
+  stroke-width: 80;
+  stroke-linecap: round;
+  transition: all 0.5s cubic-bezier(0.3, 0.9, 0.3, 1);
+  cursor: pointer;
+}
+.pie-segment:hover {
+  stroke-width: 100;
+  filter: brightness(1.4);
+  transform: scale(1.03);
+}
+
+/* 中心面板 */
+.chart-center {
+  position: absolute;
+  width: 50%;
+  height: 50%;
+  border-radius: 50%;
+  background: rgba(10, 15, 20, 0.92);
+  border: 1px solid rgba(0, 255, 120, 0.2);
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  color: #fff;
+  box-shadow: 0 0 60px rgba(0, 255, 120, 0.2),
+    inset 0 0 40px rgba(255, 30, 80, 0.1);
+  z-index: 10;
+  animation: centerGlow 4s infinite alternate;
+}
+
+.center-total {
+  font-size: clamp(16px, 3vh, 36px);
+  font-weight: 700;
+  background: linear-gradient(90deg, #00ff88, #ff2255);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+}
+.center-label {
+  font-size: clamp(10px, 1.2vh, 14px);
+  color: rgba(255, 255, 255, 0.8);
+  letter-spacing: 1px;
+  margin-top: 3px;
+}
+
+/* 右侧面板 */
+.status-panel {
+  display: flex;
+  flex-direction: column;
+  gap: clamp(6px, 1.5vh, 12px);
+  width: 45%;
+}
+
+.status-item {
+  display: flex;
+  align-items: center;
+  gap: clamp(6px, 1vw, 10px);
+  font-size: clamp(11px, 1.4vh, 15px);
+  font-weight: 500;
+  color: #fff;
+  padding: clamp(5px, 1vh, 10px) clamp(8px, 1.2vw, 14px);
+  transition: all 0.3s ease;
+  white-space: nowrap;
+}
+
+.status-item:hover {
+  transform: translateX(4px);
+}
+
+.status-dot {
+  width: clamp(8px, 1.2vh, 14px);
+  height: clamp(8px, 1.2vh, 14px);
+  border-radius: 50%;
+  flex-shrink: 0;
+}
+.status-text {
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.status-num {
+  font-weight: 700;
+  font-size: clamp(12px, 1.6vh, 18px);
+  flex-shrink: 0;
+}
+
+/* 动画 */
+@keyframes ringPulse {
+  0%, 100% { transform: scale(1); opacity: 0.7; }
+  50% { transform: scale(1.05); opacity: 1; }
+}
+@keyframes centerGlow {
+  0% { box-shadow: 0 0 40px rgba(0, 255, 120, 0.2); }
+  100% { box-shadow: 0 0 80px rgba(0, 255, 120, 0.35); }
+}
+@keyframes draw {
+  from { stroke-dashoffset: 1320; }
+}
+</style>

+ 5 - 5
src/components/ui/DeviceStatusTabs.vue

@@ -2,13 +2,13 @@
   <div class="device-status-tabs">
     <TechTabs v-model="activeTab" :interval="10000" type="segmented" autoPlay @tab-click="handleTabClick">
       <TechTabPane label="信号机" name="signalMachineStatus">
-        <DeviceStatusDonutChart v-if="displayData" :chartData="displayData.chartData" />
+        <DeviceStatusPie v-if="displayData" :chartData="displayData.chartData" :showCenter="false" />
       </TechTabPane>
       <TechTabPane label="检测器" name="detectorStatus">
-        <DeviceStatusDonutChart v-if="displayData" :chartData="displayData.chartData" />
+        <DeviceStatusPie v-if="displayData" :chartData="displayData.chartData" :showCenter="false" />
       </TechTabPane>
       <TechTabPane label="红绿灯" name="trafficLightStatus">
-        <DeviceStatusDonutChart v-if="displayData" :chartData="displayData.chartData" />
+        <DeviceStatusPie v-if="displayData" :chartData="displayData.chartData" :showCenter="false" />
       </TechTabPane>
     </TechTabs>
   </div>
@@ -17,7 +17,7 @@
 <script>
 import TechTabs from '@/components/ui/TechTabs.vue';
 import TechTabPane from '@/components/ui/TechTabPane.vue';
-import DeviceStatusDonutChart from '@/components/ui/DeviceStatusDonutChart.vue';
+import DeviceStatusPie from '@/components/ui/DeviceStatusPie.vue';
 
 // 提取 Mock 数据作为默认值
 const defaultMockStatusData = {
@@ -51,7 +51,7 @@ export default {
   components: {
     TechTabs,
     TechTabPane,
-    DeviceStatusDonutChart
+    DeviceStatusPie
   },
   props: {
     // 接收父组件传入的数据