|
|
@@ -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>
|