Explorar o código

新增DynamicDonutChart组件;新增PanelContainer首页面板组件;修改TechTab的字体颜色;调整左右布局的宽度;调整home页面的内容;

画安 hai 1 semana
pai
achega
71a6bd421d

+ 179 - 0
src/components/ui/DynamicDonutChart.vue

@@ -0,0 +1,179 @@
+<template>
+    <div class="donut-chart-wrapper">
+        <div class="custom-legend">
+            <div class="legend-box" v-for="(item, index) in chartData" :key="index">
+                <span class="color-dot" :style="{ backgroundColor: item.color }"></span>
+                <span class="legend-name">{{ item.name }}</span>
+            </div>
+        </div>
+
+        <div class="echarts-container" ref="chartRef"></div>
+    </div>
+</template>
+
+<script>
+import * as echarts from 'echarts';
+// 1. 引入你的全局自适应 Mixin 和像素转换工具
+import echartsResize, { px2echarts } from '@/mixins/echartsResize.js';
+
+export default {
+    name: 'DynamicDonutChart',
+    // 2. 注册混入,自动接管自适应逻辑
+    mixins: [echartsResize],
+    props: {
+        // 传入的图表数据:[{ name: '正常', value: 425, color: '#32F6F8' }, ...]
+        chartData: {
+            type: Array,
+            required: true
+        },
+        // 圆环中间的大字(如 98%)
+        centerTitle: {
+            type: String,
+            default: ''
+        },
+        // 圆环中间的小字(如 980/1000)
+        centerSubTitle: {
+            type: String,
+            default: ''
+        }
+    },
+    watch: {
+        // 深度监听数组变化,触发重绘
+        chartData: {
+            deep: true,
+            handler() {
+                this.$nextTick(() => {
+                    if (this.$_chart) {
+                        this.$_chart.resize(); // 在更新数据前,先强制重算尺寸
+                    }
+                    this.updateChart();
+                });
+            }
+        },
+        // 监听标题变化
+        centerTitle() {
+            this.$nextTick(() => {
+                if (this.$_chart) this.$_chart.resize();
+                this.updateChart();
+            });
+        },
+        centerSubTitle() {
+            this.$nextTick(() => {
+                if (this.$_chart) this.$_chart.resize();
+                this.updateChart();
+            });
+        }
+    },
+    mounted() {
+        this.initChart();
+        // 清理:无需手动监听 resize
+    },
+    // 清理:彻底删除 beforeDestroy
+    methods: {
+        initChart() {
+            // 3. 按照 mixin 约定,将实例挂载到 this.$_chart
+            this.$_chart = echarts.init(this.$refs.chartRef);
+            this.updateChart();
+        },
+
+        // 4. Mixin 会在窗口变化时自动静默调用此方法
+        updateChart() {
+            if (!this.$_chart) return;
+
+            const colorPalette = this.chartData.map(item => item.color);
+
+            const option = {
+                color: colorPalette,
+                title: {
+                    // 【核心改造】:使用富文本结构,用 \n 换行
+                    text: `{main|${this.centerTitle}}\n{sub|${this.centerSubTitle}}`,
+                    left: 'center',
+                    top: 'center',
+                    textStyle: {
+                        rich: {
+                            // 全面使用 px2echarts 包裹尺寸数值
+                            main: {
+                                fontSize: px2echarts(26),
+                                color: '#ffffff',
+                                fontWeight: 'bold',
+                                fontFamily: 'Arial',
+                                // 用 padding 的 bottom 值代替原来的 itemGap,实现精准缩放
+                                padding: [0, 0, px2echarts(6), 0]
+                            },
+                            sub: {
+                                fontSize: px2echarts(14),
+                                lineHeight: px2echarts(18),
+                                color: '#cccccc'
+                            }
+                        }
+                    }
+                },
+                series: [
+                    {
+                        type: 'pie',
+                        // 半径原则上也可以写成具体像素如 [px2echarts(60), px2echarts(80)]
+                        // 但写成百分比原生就自带外层容器的自适应,通常保留百分比即可
+                        radius: ['60%', '80%'],
+                        center: ['50%', '50%'],
+                        avoidLabelOverlap: false,
+                        label: { show: false },
+                        labelLine: { show: false },
+                        data: this.chartData
+                    }
+                ]
+            };
+
+            this.$_chart.setOption(option);
+        }
+    }
+}
+</script>
+
+<style scoped>
+.donut-chart-wrapper {
+    display: flex;
+    align-items: center;
+    width: 100%;
+    height: 100%;
+}
+
+/* 左侧图例样式 */
+.custom-legend {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    gap: 12px;
+    width: 35%;
+    margin-right: 8px;
+}
+
+.legend-box {
+    display: flex;
+    align-items: center;
+    background: linear-gradient( 90deg, #2D426C 0%, rgba(45,66,108,0) 100%);
+    height: 40px;
+}
+
+.color-dot {
+    width: 8px;
+    height: 8px;
+    margin-right: 8px;
+    margin-left: 16px;
+    flex-shrink: 0;
+    border-radius: 2px;
+}
+
+.legend-name {
+    font-weight: 400;
+    font-size: 12px;
+    color: rgba(255, 255, 255, 0.60);
+    line-height: 18px;
+}
+
+/* 右侧图表样式 */
+.echarts-container {
+    width: 65%;
+    height: 100%;
+    min-height: 160px;
+}
+</style>

+ 87 - 0
src/components/ui/PanelContainer.vue

@@ -0,0 +1,87 @@
+<template>
+    <div class="panel-container">
+        <div class="panel-header">
+            <div class="title-wrap">
+                <span class="dot"></span>
+                <span class="title-text">{{ title }}</span>
+            </div>
+
+            <div class="header-action">
+                <slot name="action"></slot>
+            </div>
+        </div>
+
+        <div class="panel-content">
+            <slot></slot>
+        </div>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'PanelContainer',
+    props: {
+        // 面板标题
+        title: {
+            type: String,
+            required: true
+        }
+    }
+}
+</script>
+
+<style scoped>
+.panel-container {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    /* 如果面板需要底色或边框,可以在这里加 */
+    /* background: rgba(10, 35, 75, 0.4); */
+    /* border: 1px solid rgba(43, 220, 255, 0.2); */
+}
+
+/* 标题栏布局 */
+.panel-header {
+    display: flex;
+    justify-content: space-between;
+    /* 左右两端对齐 */
+    align-items: center;
+    height: 32px;
+    background: linear-gradient(90deg, #2D426C 0%, rgba(45, 66, 108, 0) 100%);
+}
+
+.title-wrap {
+    display: flex;
+    align-items: center;
+    padding-left: 13px;
+}
+
+.dot {
+    width: 7px;
+    height: 7px;
+    background-color: #fff;
+    border-radius: 50%;
+    margin-right: 5px;
+}
+
+.title-text {
+    letter-spacing: 1px;
+    font-family: var(--title-font-family);
+    font-size: 22px;
+    color: #FFFFFF;
+    line-height: 22px;
+    text-align: left;
+    font-style: normal;
+}
+
+/* 内容区占满剩余空间 */
+.panel-content {
+    flex: 1;
+    position: relative;
+    min-height: 0;
+    margin-top: 3px;
+    background: #112446;
+    padding: 11px 24px 0 24px;
+}
+</style>

+ 1 - 1
src/components/ui/TechTabs.vue

@@ -237,7 +237,7 @@ export default {
   justify-content: center;
   align-items: center;
   height: 100%;
-  color: #ffffff;
+  color: rgba(255, 255, 255, 0.65);
   font-size: 14px;
   line-height: 20px;
   cursor: pointer;

+ 2 - 2
src/layouts/DashboardLayout.vue

@@ -175,8 +175,8 @@ export default {
 /* --- 主体网格 --- */
 .main-layout {
     display: grid;
-    /* 默认网格:左320,中间自适应,右480 */
-    grid-template-columns: 320px 1fr 480px;
+    /* 默认网格:左400,中间自适应,右400 */
+    grid-template-columns: 400px 1fr 400px;
     gap: 20px;
     /* 【关键】给四周留出 padding,防止里面的图表被外围的装饰边框挡住! */
     padding: 20px 50px 60px 50px;

+ 74 - 9
src/views/Home.vue

@@ -14,36 +14,101 @@
     </template>
 
     <template #left>
-      <!-- <SidebarMenu @leaf-node-click="handleLeafNodeClick" /> -->
+      <div>
+        <PanelContainer title="在线状态">
+
+          <TechTabs v-model="onlineStatusActiveTab" type="segmented"  @tab-click="handleTabClick">
+            <TechTabPane label="信号机" name="signalMachine" style="height: 100%;">
+              <DynamicDonutChart v-if="onlineStatusDisplayData" :chartData="onlineStatusDisplayData.chartData"
+                :centerTitle="onlineStatusDisplayData.centerTitle" :centerSubTitle="onlineStatusDisplayData.centerSubTitle" />
+            </TechTabPane>
+            <TechTabPane label="检测器" name="detector">
+              <DynamicDonutChart v-if="onlineStatusDisplayData" :chartData="onlineStatusDisplayData.chartData"
+                :centerTitle="onlineStatusDisplayData.centerTitle" :centerSubTitle="onlineStatusDisplayData.centerSubTitle" />
+            </TechTabPane>
+            <TechTabPane label="红路灯" name="trafficLight">
+              <DynamicDonutChart v-if="onlineStatusDisplayData" :chartData="onlineStatusDisplayData.chartData"
+                :centerTitle="onlineStatusDisplayData.centerTitle" :centerSubTitle="onlineStatusDisplayData.centerSubTitle" />
+            </TechTabPane>
+          </TechTabs>
+
+
+        </PanelContainer>
+      </div>
     </template>
 
-    </DashboardLayout>
+  </DashboardLayout>
 </template>
 
 <script>
 import DashboardLayout from '@/layouts/DashboardLayout.vue';
 import WeatherWidget from '@/components/ui/WeatherWidget.vue';
 import DateTimeWidget from '@/components/ui/DateTimeWidget.vue';
-import SidebarMenu from '@/components/ui/SidebarMenu.vue';
+import PanelContainer from '@/components/ui/PanelContainer.vue';
+import DynamicDonutChart from '@/components/ui/DynamicDonutChart.vue';
+import TechTabs from '@/components/ui/TechTabs.vue';
+import TechTabPane from '@/components/ui/TechTabPane.vue';
+
+
+const mockDeviceData = {
+  'signalMachine': {
+    centerTitle: '98%',
+    centerSubTitle: '980/1000',
+    chartData: [
+      { name: '在线', value: 980, color: '#32F6F8' },
+      { name: '离线', value: 20, color: '#E4D552' }
+    ]
+  },
+  'detector': {
+    centerTitle: '85%',
+    centerSubTitle: '425/500',
+    chartData: [
+      { name: '正常', value: 425, color: '#32F6F8' },
+      { name: '故障', value: 50, color: '#faad14' },
+      { name: '掉线', value: 25, color: '#ff4d4f' }
+    ]
+  },
+  'trafficLight': {
+    centerTitle: '99%',
+    centerSubTitle: '1188/1200',
+    chartData: [
+      { name: '正常发光', value: 1188, color: '#32F6F8' },
+      { name: '灯组损坏', value: 12, color: '#ff4d4f' }
+    ]
+  }
+};
 
 export default {
   name: "HomePage",
-  components: { 
-    DashboardLayout, 
+  components: {
+    DashboardLayout,
     WeatherWidget,
     DateTimeWidget,
-    SidebarMenu 
+    PanelContainer,
+    DynamicDonutChart,
+    TechTabs,
+    TechTabPane
   },
   data() {
     return {
-      
+      // 在线状态面板
+      onlineStatusActiveTab: 'detector',
+      onlineStatusDisplayData: mockDeviceData['detector']
     };
   },
   mounted() {
-    
+
   },
   methods: {
-    
+    // 监听 Tab 切换事件
+    handleTabClick(selectedTabName) {
+      console.log('用户切换了设备类型:', selectedTabName);
+
+      // 从 mock 字典中取出对应的数据并赋值,图表会自动响应式更新!
+      if (mockDeviceData[selectedTabName]) {
+        this.onlineStatusDisplayData = mockDeviceData[selectedTabName];
+      }
+    }
   }
 }
 </script>