Преглед изворни кода

修改日志监控、业务监控问题

吴朋磊 пре 1 недеља
родитељ
комит
e2b7ee3d0b

+ 224 - 172
src/baseComponents/LineChart.vue

@@ -13,7 +13,8 @@ import _ from 'lodash'
 import * as echarts from 'echarts';
 
 export default {
-    name: "LineChart",//折线图组件
+    name: "LineChart",
+    emits: ['dataPointClick'],
     data() {
         return {
             echart: null,//图标实例
@@ -23,6 +24,7 @@ export default {
             legendSelected: {},//图例选中状态
             legenSelectList: [],//选择的图例
             legenAllList: this.title,//所有的图例数据
+            _tooltipDetailHandler: null,//tooltip详情按钮点击处理器
 
         }
     },
@@ -85,10 +87,6 @@ export default {
             type: Array,
             default: () => []
         },//图例标题
-        // isSetMarkNumber:{
-        //     type: Boolean,
-        //     default: false
-        // },//是否设置辅助线label值
         markNumber: {
             type: Array,
             default: () => []
@@ -99,11 +97,6 @@ export default {
             default: false
         },//是否是曲线图
 
-        // showMarkLine: {
-        //     type: Boolean,
-        //     default: false
-        // },//是否显示辅助线平均值
-
         showBackground: {
             type: Boolean,
             default: true,
@@ -155,6 +148,22 @@ export default {
         disableLabelRotate: {
             type: Boolean,
             default: false
+        },
+        tooltipDetailData: {
+            type: Array,
+            default: () => []
+        },
+        clickDataPoint: {
+            type: Boolean,
+            default: false
+        },
+        showTooltipDetail: {
+            type: Boolean,
+            default: true
+        },
+        showYAxisInteger: {
+            type: Boolean,
+            default: false
         }
 
     },
@@ -169,7 +178,12 @@ export default {
         },//重新加载图表
 
         datay: {
-            handler: 'LoadEchart',
+            handler() {
+                this.LoadEchart();
+                this.$nextTick(() => {
+                    this.bindChartEvents();
+                });
+            },
             deep: true,
         },//数值变化时重新加载图表
     },
@@ -179,12 +193,17 @@ export default {
     beforeDestroy() {
         // 组件销毁移除监听 防止内存泄漏
         window.removeEventListener('resize', this.HandleResize);
+        if (this._tooltipDetailHandler && this.echart) {
+            this.echart.getDom().removeEventListener('click', this._tooltipDetailHandler);
+        }
         this.echart.dispose(); // 销毁之前的实例
     },
     mounted() {
-
-        this.LoadEchart();//初始化图表
-        this.HandleResize()
+        this.LoadEchart();
+        this.HandleResize();
+        this.$nextTick(() => {
+            this.bindChartEvents();
+        });
     },
     methods: {
         markLine(index) {
@@ -214,6 +233,20 @@ export default {
         //加载echarts
         LoadEchart() {
 
+            const container = this.$refs.line_chart;
+            
+            // 空数据处理
+            if (!this.datax || this.datax.length === 0 || !this.datay || this.datay.length === 0) {
+                if (this.echart) {
+                    this.echart.dispose();
+                    this.echart = null;
+                }
+                if (container) {
+                    container.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:#909399;font-size:14px;">暂无数据</div>';
+                }
+                return;
+            }
+
             // 简化图例选中列表的初始化
             this.legenSelectList = this.legendList.length > 0
                 ? [...this.legendList]
@@ -221,11 +254,14 @@ export default {
 
             this.legenAllList = [...this.title];
 
+            // 清空可能存在的"暂无数据"文本
+            container.innerHTML = '';
+            
             if (this.echart) {
                 this.echart.dispose(); // 销毁之前的实例
             }
             //devicePixelRatio: 2   2表示每个逻辑像素对应 2 个物理像素。这会进一步提高图表的清晰度,适用于更高分辨率的屏幕  解决高分辨率显示器文字模糊的问题
-            this.echart = echarts.init(this.$refs.line_chart, { devicePixelRatio: 2 });//获取echarts容器  如果组件同一个页面多处使用 这里不能用id获取实例
+            this.echart = echarts.init(container, { devicePixelRatio: 2 });//获取echarts容器  如果组件同一个页面多处使用 这里不能用id获取实例
 
             // 图例选中状态初始化
             this.legendSelected = {};
@@ -248,81 +284,87 @@ export default {
                 .filter(index => index !== -1)
                 .map(index => this.datay[index]);
 
-            // const maxValue = visibleData.reduce((max, series) => {
-            //     const seriesMax = Math.max(...series);
-            //     return seriesMax > max ? seriesMax : max;
-            // }, -Infinity);
-
-            // const yAxisMax =  Math.ceil((maxValue+2) / 5) * 5;//向上取整到5的倍数
-            // console.log("打印最大值",maxValue);
             const extraText = this.extraText ? "占比" : ""
             const dataCount = this.datax.length;
-            // if(maxValue<10)
-            // {
-            //     maxValue=9
-            // }
-            //计算每个x轴的宽度 
             let totalWidth = this.$refs.line_chart.clientWidth;
-            let singleSeriesWidth = (totalWidth - 100) / dataCount;
-            // console.log("打印x轴宽度",singleSeriesWidth);
+            const gridWidth = totalWidth - 80;
+            let singleSeriesWidth = dataCount > 0 ? gridWidth / dataCount : gridWidth;
+            // Estimate label width: Chinese chars ~12px, "M月D日 HH时" ~11 chars
+            const avgLabelWidth = Math.max(...this.datax.map(d => d.length * 12));
+            const minGap = avgLabelWidth + 8;
+            let labelInterval = 0;
+            if (singleSeriesWidth < minGap) {
+                labelInterval = Math.ceil(minGap / singleSeriesWidth) - 1;
+            }
             let gridTop = this.showMarkPoint ? 45 : 20;
             // 无图例 默认20  有图例 70
             if (this.title.length > 0) {
                 gridTop = 70;
             }
+                const hasDetailData = this.tooltipDetailData.length > 0
             let option = {
 
                 tooltip: {
                     trigger: 'axis',
 
                     axisPointer: {
-                        type: 'line', // 指示器类型,cross 表示交叉指示线
-                        // lineStyle: {
-                        //     color: 'red',
-                        //     width: 2,
-                        //     type: 'dashed'
-                        // },
-                        // crossStyle: {
-                        //     color: '#999', // 交叉线颜色
-                        //     width: 1,      // 交叉线宽度
-                        //     type: 'dashed' // 交叉线样式
-                        // }
+                        type: 'line',
+                        lineStyle: {
+                            color: '#999',
+                            width: 1,
+                            type: 'dashed',
+                        },
                     },
 
                     renderModel: 'html',//使用html渲染模式
                     confine: true,//是否将 tooltip 框限制在图表的区域内。
                     extraCssText: 'border-radius: 4px;padding:5px 0px 5px 5px;white-space:normal;word-warp:break-word;max-width: 400px;', // 设置最大宽度和高度
                     enterable: true,
+                    hideDelay: 800,
                     formatter: (params) => {
-                        // console.log(params);
-
-                        // console.log("打印this.tooltipData",this.tooltipData);
-
                         let tooltip = `<div class='tooltip_content'>`;
                         let title = params[0].name;
-                        tooltip += `<div class='tooltip_title'>${title}</div>`;
-                        params.forEach((item, index) => {
-                            // console.log("打印item",item);
-                            let name = item.seriesName;
-                            let rate = item.value + this.unit;//占比
-                            if (this.tooltipData.length > 0) {
-                                let tooltipItem = this.tooltipData?.[item.seriesIndex]?.[item.dataIndex] || '';
-                                tooltip += `<div class='tooltip_student'>
-                                        <div class='tooltip_line_icon' style='background:${item.color}'></div>
-                                        <div class='tooltip_student_name'>
-                                             ${tooltipItem.name}:${tooltipItem.value}
-                                        </div></div>`;
+                        const dataIndex = params[0].dataIndex;
+                        // showTooltipDetail 控制是否显示详情按钮和学校列表
+                        if (this.tooltipDetailData.length > 0 && this.showTooltipDetail) {
+                            tooltip += `<div class='tooltip_header'>`;
+                            tooltip += `<div class='tooltip_title'>${title}</div>`;
+                            tooltip += `<div class='tooltip_detail_btn' data-index="${dataIndex}">详情</div>`;
+                            tooltip += `</div>`;
+                            const detail = this.tooltipDetailData[dataIndex] || {};
+                            const schoolList = detail.schoolList || [];
+                            const totalCount = schoolList.reduce((sum, s) => sum + (s.count || 0), 0);
+                            tooltip += `<div class='tooltip_total'>人数总计:${totalCount}</div>`;
+                            if (schoolList.length > 0) {
+                                tooltip += `<div class='tooltip_school_list'>`;
+                                schoolList.forEach((school) => {
+                                    tooltip += `<div class='tooltip_school_item'>
+                                        <span class='tooltip_school_name'>${school.xname || ''}:${school.count || 0}</span>
+                                    </div>`;
+                                });
+                                tooltip += `</div>`;
                             }
-                            else {
-                                tooltip += `<div class='tooltip_student'><div class='tooltip_line_icon' style='background:${item.color}'></div><div class='tooltip_student_name'>${name}${extraText}:${rate || '-'}</div></div>`;
-                            }
-
-                        })
+                        } else {
+                            tooltip += `<div class='tooltip_title'>${title}</div>`;
+                            params.forEach((item) => {
+                                let name = item.seriesName;
+                                let value = this.showYAxisInteger ? Math.round(item.value) : item.value;
+                                let rate = value + this.unit;
+                                if (this.tooltipData.length > 0) {
+                                    let tooltipItem = this.tooltipData?.[item.seriesIndex]?.[item.dataIndex] || '';
+                                    tooltip += `<div class='tooltip_student'>
+                                            <div class='tooltip_student_name'>
+                                                 ${tooltipItem.name}:${tooltipItem.value}
+                                            </div></div>`;
+                                } else {
+                                    const extraText = '';
+                                    tooltip += `<div class='tooltip_student'>
+                                        <div class='tooltip_student_name'>${rate }</div></div>`;
+                                }
+                            });
+                        }
                         tooltip += `</div>`;
                         return tooltip;
-
-
-
                     },
                 },
                 legend: {
@@ -349,7 +391,6 @@ export default {
                     pageButtonPosition: 'end', // 分页按钮显示在右侧
                     orient: 'horizontal', // 图例横向排列(也可以设为 'vertical')
                     alignTo: 'none',//alignTo 可以用于图例 (legend) 的配置项,决定图例如何对齐到其他元素。常见的值包括 'left', 'right', 'center' 等。
-                    // icon:'emptyCircle',//标记类型 'circle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', 'none'
                 },
                 grid: {
                     left: this.gridLeft,
@@ -366,70 +407,38 @@ export default {
 
                     axisTick: {
                         alignWithLabel: true,//不然刻度线跟随标签对齐
-                        // show: !this.yInverse,            // 显示刻度线
                         show: !hideTick,
                         inside: false,         // 刻度线朝外(默认是朝内)
                         length: 8,             // 刻度线长度
-                        // lineStyle: {
-                        //     color: "#E4E7ED",       // 刻度线颜色
-                        //     width: 1             // 线宽
-                        // },
-                        // color:'#E4E7ED'
                     },
                     axisLine: {
                         show: !this.yInverse,
                     },
                     axisLabel: {
-                        interval: 0,//0 显示所有刻度
-                        rotate: this.disableLabelRotate ? 0 : (singleSeriesWidth < 60 ? 45 : 0),
-                        fontSize: this.fontSize ? this.fontSize : 14,
+                        interval: labelInterval,
+                        rotate: 0,
+                        fontSize: this.fontSize ? this.fontSize : 12,
                         color: this.fontColor || "#666",
                         fontWeight: 400,
-                        formatter: function (value) {
-
-                            //一个文字的长度大概约16.6  按16算
-
-                            const valueWidth = value.length * 14;
-                            // console.log("lineChart打印字的大概宽度",valueWidth);
-                            // console.log("打印singleSeriesWidth",singleSeriesWidth);
-                            if (valueWidth > singleSeriesWidth) {
-
-                                //单系列宽度小于50 倾斜显示
-                                if (singleSeriesWidth < 80) {
-                                    return value;
-                                }
-                                else {
-                                    let maxLength = Math.floor(singleSeriesWidth / 14);
-
-                                    return value.slice(0, maxLength) + '...';
-                                }
-
-
-
-                            }
-                            else {
-                                return value;
-                            }
-                        },
                     },
                 },
                 yAxis: {
                     type: 'value',
+                    triggerEvent: true,
                     axisLabel: {
                         margin: 20,
-                        formatter: '{value}' + this.unit,
+                        formatter: this.showYAxisInteger ? (v) => Math.round(v) + this.unit : '{value}' + this.unit,
                         interval: 0, // 显示所有 label
                         fontSize: this.fontSize ? this.fontSize : 14,
                         color: this.fontColor || "#666",
                         fontWeight: 400
                     },
+                    minInterval: this.showYAxisInteger ? 1 : 0,
                     splitNumber: 5,//固定显示5个刻度  非强制 不一定生效
 
                     axisTick: {
                         interval: 0 // 显示所有刻度线
                     },
-                    // splitNumber:4,//固定将y轴划分为5段
-
                     splitLine: {
                         show: true, //显示横向网格线
                         lineStyle:
@@ -446,30 +455,6 @@ export default {
                         },// 网格线设置隔行背景色
                     },
                     inverse: this.yInverse,
-                    // min:this.showBackground?0: (val) => {
-
-
-                    //     if(val.min > 0 || val.min < -10) {
-                    //         // return Math.floor(val.min / 10) * 10
-                    //         return Math.floor(val.min)
-                    //     }else {
-                    //         return val.min + (val.min % 10)
-                    //     }
-                    // },
-                    // max:yAxisMax,//固定x轴最高值位100
-                    // max: this.unit != '%' ? (val) => {
-                    //     // console.log(val.max)
-                    //     // let num = Math.ceil(val.max)
-                    //     // let num1 = Math.abs(Math.ceil(val.max))
-                    //     // let count = 0
-                    //     // do {
-                    //     //     num1 = parseInt(num1 / 10)
-                    //     //     count++
-                    //     // } while (num1 > 0);
-                    //     // let num2 = (parseInt(num / (10 ** (count - 1))) + 1 ) * (10 ** (count - 1))
-                    //     // return num2
-                    //     return Math.ceil(val.max / 10) * 10
-                    // } : null,
                 },
                 graphic: {
                     elements: [{
@@ -481,92 +466,109 @@ export default {
                 series: this.datay.map((data, index) => ({
                     type: 'line',
                     name: this.title[index],
-                    // symbol: 'none',//设置无折线点
-                    symbol: "emptyCircle",//设置折线点为圆形
-                    showSymbol: true,//显示小圆点
-                    symbolSize: 8,//折线点大小
+                    symbol: "emptyCircle",
+                    showSymbol: !hasDetailData,
+                    symbolSize: 8,
                     label: {
-                        show: this.isShowLabel, //是否显示数值
+                        show: this.isShowLabel,
                         color: this.labelColor || this.colors[index],
                         formatter: "{c}" + this.unit
                     },
-                    smooth: this.isSmooth,//设置折线平滑
-                    markLine: this.markLine(index), //平均线
+                    smooth: this.isSmooth,
+                    markLine: this.markLine(index),
 
                     markPoint: this.showMarkPoint ? {
-                        // symbolSize:25,//气泡大小
-                        // label: {
-                        //     show: true,
-                        //     position: 'inside',
-                        //     color: '#fff',
-                        //     z: 0, // 降低层级
-                        //     emphasis: { z: 0 }
-                        // },
                         data: [
                             {
                                 type: "max",
                                 name: "最大值",
                                 label: { color: "#fff", },
-                                // z: index*5+1,
-                                // symbolOffset: [0, index * 8 - 10], // 每一个点依次向下偏移
                             },
                             {
                                 type: "min",
                                 name: "最小值",
                                 label: { color: "#fff", },
-                                // z:index+1,
-                                // symbolOffset: [0, index * 8 - 10], // 每一个点依次向下偏移
                             },
                         ],
-                    } : undefined,//是否显示气泡      
+                    } : undefined,
                     itemStyle: {
                         color: this.colors[index],
                         borderColor: this.colors[index],
-                        borderWidth: 1,//空心圆的边框粗细
+                        borderWidth: 2,
                         shadowColor: 'rgba(0, 0, 0, .1)',
                         shadowBlur: 0,
                     },
+                    symbol: hasDetailData ? 'emptyCircle' : 'circle',
+                    symbolSize: hasDetailData ? 0 : 6,
                     lineStyle: {
-                        width: 3,//设置折线的粗细 
+                        width: 2,
                     },
                     areaStyle: this.showBackground ? {
                         color: {
                             type: 'linear',
-                            x: 0,//渐变起点x
-                            y: 0,//渐变起点y
-                            x2: 0,//渐变终点x
-                            y2: 1,//渐变终点y
+                            x: 0,
+                            y: 0,
+                            x2: 0,
+                            y2: 1,
                             colorStops: [
                                 {
-                                    offset: 0, color: this.colors[index] + '30' // 30% 透明度
+                                    offset: 0, color: hasDetailData ? 'rgba(46, 100, 250, 0.40)' : (this.colors[index] + '30')
                                 },
                                 {
-                                    offset: 1, color: this.colors[index] + '00'   // 0% 透明度
+                                    offset: 1, color: hasDetailData ? 'rgba(46, 100, 250, 0.04)' : (this.colors[index] + '00')
                                 }
                             ],
                         },
-                    } : undefined,//折线背景阴影设置
+                    } : undefined,
                     emphasis: {
                         focus: 'series',
-                        blurScope: 'coordinateSystem'
+                        blurScope: 'coordinateSystem',
+                        scale: true,
+                        itemStyle: {
+                            color: '#fff',
+                            borderColor: '#2E64FA',
+                            borderWidth: 8,
+                            shadowColor: 'rgba(46, 100, 250, 0.3)',
+                            shadowBlur: 8,
+                        }
                     },
                     data: data,
                 })),
             };
 
-            this.LabelMouseOver();//X轴文字过程显示省略号,鼠标hover上去时,弹出一个tooltip显示全称
+            this.LabelMouseOver();
             this.echart.setOption(option);
             this.echart.on("legendselectchanged", this.HandleLegendSelectChanged);
+        },
 
+        bindChartEvents() {
+            if (!this.echart) return;
+            if (!this.datax || this.datax.length === 0) return;
+
+            // 通过 tooltip 的 enterable: true,鼠标可进入 tooltip 区域
+            // 使用事件委托监听详情按钮点击
+            const dom = this.echart.getDom();
+            dom.removeEventListener('click', this._tooltipDetailHandler);
+            this._tooltipDetailHandler = (e) => {
+                if (e.target && e.target.classList && e.target.classList.contains('tooltip_detail_btn')) {
+                    const dataIndex = parseInt(e.target.getAttribute('data-index'), 10);
+                    if (!isNaN(dataIndex) && dataIndex >= 0 && dataIndex < this.datax.length) {
+                        const detail = this.tooltipDetailData[dataIndex] || {};
+                        const schoolList = detail.schoolList || [];
+                        this.$emit('dataPointClick', {
+                            dataIndex,
+                            schoolList,
+                            rawData: detail
+                        });
+                    }
+                }
+            };
+            dom.addEventListener('click', this._tooltipDetailHandler);
         },
 
         // X轴文字过程显示省略号,鼠标hover上去时,弹出一个tooltip显示全称
         LabelMouseOver() {
-            // 注意这里,是以X轴显示内容过长为例,如果是y轴的话,需要把params.componentType == 'xAxis'改为yAxis
-            // 判断是否创建过div框,如果创建过就不再创建了
-            // 该div用来盛放文本显示内容的,方便对其悬浮位置进行处理
             let elementDiv = document.getElementById("tooltipContent");
-            // console.log("elementDiv",elementDiv)
 
             if (!elementDiv) {
                 let div = document.createElement("div");
@@ -593,7 +595,6 @@ export default {
                 }
             });
             this.echart.on("mouseout", function (params) {
-                //注意这里,我是以X轴显示内容过长为例,如果是y轴的话,需要改为yAxis
                 if (params.componentType == "xAxis") {
                     let elementDiv = document.querySelector("#tooltipContent");
                     elementDiv.style.cssText = "display:none";
@@ -614,7 +615,6 @@ export default {
         },
         //全选
         ChangeCheckAll(value) {
-            // console.log("打印value",this.checkAll);
             if (value) {
                 //选中全选
                 this.legenSelectList = this.legenAllList;
@@ -633,7 +633,6 @@ export default {
         HandleLegendSelectChanged(params) {
             const legendData = this.echart.getOption().legend[0].data;
             const selected = params.selected;
-            // 检查是否有任何图例项被隐藏
             this.legendSelected = selected;
             const allSelected = legendData.every(
                 (seriesName) => selected[seriesName]
@@ -642,7 +641,6 @@ export default {
                 (seriesName) => !selected[seriesName]
             );
             const someSelected = !allSelected && !noneSelected;
-            // 更新“显示全部”按钮的状态
             if (noneSelected) {
                 this.checkAll = false;
             }
@@ -735,9 +733,6 @@ export default {
 
         // X轴文字过程显示省略号,鼠标hover上去时,弹出一个tooltip显示全称
         extension() {
-            // 注意这里,是以X轴显示内容过长为例,如果是y轴的话,需要把params.componentType == 'xAxis'改为yAxis
-            // 判断是否创建过div框,如果创建过就不再创建了
-            // 该div用来盛放文本显示内容的,方便对其悬浮位置进行处理
             let elementDiv = document.getElementById("extension");
             if (!elementDiv) {
                 let div = document.createElement("div");
@@ -748,7 +743,6 @@ export default {
             this.myChart.on("mouseover", (params) => {
                 if (params.componentType == "xAxis") {
                     let elementDiv = document.querySelector("#extension");
-                    //设置悬浮文本的位置以及样式
                     let elementStyle =
                         "position: absolute;z-index: 99999;color: #fff;font-size: 12px;padding: 5px;display: inline;border-radius: 4px;background-color: #303133;box-shadow: rgba(0, 0, 0, 0.3) 2px 2px 8px";
                     elementDiv.style.cssText = elementStyle;
@@ -763,7 +757,6 @@ export default {
                 }
             });
             this.myChart.on("mouseout", function (params) {
-                //注意这里,我是以X轴显示内容过长为例,如果是y轴的话,需要改为yAxis
                 if (params.componentType == "xAxis") {
                     let elementDiv = document.querySelector("#extension");
                     elementDiv.style.cssText = "display:none";
@@ -785,4 +778,63 @@ export default {
 .is_show_all {
     top: 10px;
 }
-</style>
+
+:deep(.tooltip_content) {
+    font-size: 12px;
+    min-width: 180px;
+}
+:deep(.tooltip_header) {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 8px;
+}
+:deep(.tooltip_title) {
+    font-size: 14px;
+    font-weight: 500;
+    color: #333333;
+}
+:deep(.tooltip_detail_btn) {
+    font-size: 12px;
+    color: #2E64FA;
+    cursor: pointer;
+    padding: 2px 8px;
+    white-space: nowrap;
+}
+:deep(.tooltip_total) {
+    font-size: 12px;
+    font-weight: 400;
+    color: #666666;
+    margin-bottom: 6px;
+}
+:deep(.tooltip_school_list) {
+    display: flex;
+    flex-direction: column;
+    gap: 4px;
+    margin-bottom: 8px;
+}
+:deep(.tooltip_school_item) {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    color: #606266;
+}
+:deep(.tooltip_line_icon) {
+    display: inline-block;
+    width: 8px;
+    height: 8px;
+    border-radius: 50%;
+    flex-shrink: 0;
+}
+:deep(.tooltip_school_name) {
+    flex: 1;
+}
+:deep(.tooltip_student) {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+}
+:deep(.tooltip_student_name) {
+    color: #606266;
+}
+</style>

+ 32 - 7
src/baseComponents/MultipleBarCharts.vue

@@ -43,7 +43,7 @@ import * as echarts from 'echarts';
       },
       unit:{
         type:String,
-        default:'%'
+        default:''
       },//单位  默认%
 
       showNuitY:{
@@ -171,10 +171,26 @@ import * as echarts from 'echarts';
         //加载echarts
         LoadEchart() 
         {
+            const container = this.$refs.bar_echart;
+            
+            // 空数据处理
+            if (!this.datax || this.datax.length === 0 || !this.datay || this.datay.length === 0) {
+                if (this.echart) {
+                    this.echart.dispose();
+                    this.echart = null;
+                }
+                if (container) {
+                    container.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:#909399;font-size:14px;">暂无数据</div>';
+                }
+                return;
+            }
             
             if(this.echart)
             {
                 this.echart.dispose(); // 销毁之前的实例
+                if (container) {
+                    container.innerHTML = ''; // 清空容器
+                }
             }
             // 定义每个柱子的颜色
 
@@ -297,17 +313,25 @@ import * as echarts from 'echarts';
                     //     }
                     // },
                     show: this.showTooltip,
-                    trigger: this.showTooltip?'item':'axis',
-                    triggerOn: "mousemove",
+                    trigger: this.showTooltip ? 'axis' : 'item',
+                    axisPointer: {
+                        type: 'shadow',
+                        shadowStyle: {
+                            color: 'rgba(46, 100, 250, 0.05)'
+                        }
+                    },
                     renderModel:'html',//使用html渲染模式
                     confine:true,//是否将 tooltip 框限制在图表的区域内。
                     // extraCssText: 'border-radius: 4px;padding:5px 0px 5px 5px;white-space:normal;word-warp:break-word;max-width: 400px;', // 设置最大宽度和高度
                     borderColor: "#fff",
 
                     formatter:this.tooltipFormater ? this.tooltipFormater : function (params) {
-                        const {name,seriesName,value,marker} = params
-                        return name + '<br/>' + marker + seriesName + `<span style=\"margin-left:10px;\">${value}</span>` ;
-                    },
+                        const p = Array.isArray(params) ? params[0] : params;
+                        const name = p?.name ?? '';
+                        const value = p?.value ?? 0;
+                        const unit = this.unit || '';
+                        return `${name}<br/><span ">${value}${unit}</span>`;
+                    }.bind(this),
 
                     /*
                     formatter: (params)=>{
@@ -507,8 +531,9 @@ import * as echarts from 'echarts';
                         //         color: '#2E64FA' // 设置选中时的颜色
                         //     }
                         // },
-                        data: this.datay.map(value => ({
+                        data: this.datay.map((value, index) => ({
                             value: value,
+                            name: this.datax[index] ?? '',
                             label: {
                                 // show: this.showMarkPoint ? (value === 0 || value === maxValue || value === minValue) : singleSeriesWidth > 26,//数值为0不显示
                                 show: true,//数值为0不显示

+ 8 - 2
src/views/logMonitor/index.vue

@@ -67,7 +67,7 @@ const searchParams = ref({
     accountTypeCode: 0,
     moduleCode: 0,
     operationTypeCode: 0,
-    userAccount: 0,
+    userAccount: '',
 })
 
 // 详情弹窗
@@ -92,7 +92,13 @@ const handleSearchChange = (data: Record<string, any>, key: string) => {
     
     if (fieldMap[key]) {
         const val = data[key]
-        searchParams.value[fieldMap[key] as keyof typeof searchParams.value] = (val !== undefined && val !== null && val !== '') ? val : 0
+        const field = fieldMap[key] as keyof typeof searchParams.value
+        // userAccount 是字符串类型,其他是数字类型
+        if (field === 'userAccount') {
+            searchParams.value[field] = (val !== undefined && val !== null) ? String(val) : ''
+        } else {
+            searchParams.value[field] = (val !== undefined && val !== null && val !== '') ? Number(val) : 0
+        }
     }
     
     currentPage.value = 1

+ 237 - 139
src/views/performanceMonitor/index.vue

@@ -41,7 +41,7 @@
             </div>
 
             <!-- 主折线图:某时间段在线人数统计 -->
-            <div class="page_jg_20"></div>
+            <div class="page_jg_16"></div>
             <div class="chart_panel">
                 <div class="panel_title">
                     {{ currentOnlineLabel }}人数统计
@@ -50,84 +50,87 @@
                     reportHeight="280px"
                     :datax="mainLineChartData.datax"
                     :datay="mainLineChartData.datay"
-                    :title="[currentOnlineLabel + '在线']"
                     :showCheckBox="false"
                     :showBackground="true"
                     :disableLabelRotate="true"
-                    isSmooth
+                    :tooltipDetailData="chartDetailDataList"
+                    :clickDataPoint="true"
                     unit="人"
+                    @dataPointClick="handleDataPointClick"
                 />
             </div>
 
-            <!-- 底部:柱状图 + 折线图/表格 -->
-            <div class="page_jg_20"></div>
-            <div class="bottom_section">
-                <!-- 左侧:某时刻各学校在线人数 -->
-                <div class="chart_panel left_panel">
-                    <div class="panel_title">
-                        {{ currentHourLabel }}时 {{ currentOnlineLabel }}在线人数统计
-                    </div>
-                    <MultipleBarCharts
-                        :datax="barChartData.datax"
-                        :datay="barChartData.datay"
-                        :height="260"
-                        :isClick="true"
-                        color="#2E64FA"
-                        :showDataZoom="false"
-                        @HandleChartClick="handleBarClick"
-                    />
+            <!-- 某时刻各学校在线人数统计(独立模块) -->
+            <div class="page_jg_16"></div>
+            <div class="chart_panel">
+                <div class="panel_title">
+                    {{ currentHourLabel }}时 {{ currentOnlineLabel }}在线人数统计
                 </div>
+                <MultipleBarCharts
+                    :datax="barChartData.datax"
+                    :datay="barChartData.datay"
+                    :height="260"
+                    :isClick="true"
+                    color="#5470C6"
+                    :barMaxWidth="50"
+                    :barMinWidth="50"
+                    :showDataZoom="false"
+                    @HandleChartClick="handleBarClick"
+                />
+            </div>
 
-                <!-- 右侧:某学校当天在线人数 -->
-                <div class="chart_panel right_panel">
-                    <div class="panel_header">
-                        <div class="panel_title">{{ selectedSchoolName }} 当天在线人数统计</div>
-                        <div class="panel_actions">
-                            <el-button class="btn_export" @click="handleExport">
-                                <el-icon class="btn_icon"><Download /></el-icon>
-                                导出Excel
-                            </el-button>
-                            <div class="view_toggle">
-                                <span
-                                    :class="{ active: defaultVisualizationTypeTab === 'chart' }"
-                                    @click="defaultVisualizationTypeTab = 'chart'"
-                                >分析图</span>
-                                <span
-                                    :class="{ active: defaultVisualizationTypeTab === 'table' }"
-                                    @click="defaultVisualizationTypeTab = 'table'"
-                                >分析表</span>
-                            </div>
+            <!-- 某学校当天在线人数统计(独立模块) -->
+            <div class="page_jg_16"></div>
+            <div class="chart_panel">
+                <div class="panel_header">
+                    <div class="panel_title">{{ selectedSchoolName }} 当天在线人数统计</div>
+                    <div class="panel_actions">
+                        <el-button class="btn_export" @click="handleExport">
+                            <el-icon class="btn_icon"><Download /></el-icon>
+                            导出Excel
+                        </el-button>
+                        <div class="view_toggle">
+                            <span
+                                :class="{ active: defaultVisualizationTypeTab === 'chart' }"
+                                @click="defaultVisualizationTypeTab = 'chart'"
+                            >分析图</span>
+                            <span
+                                :class="{ active: defaultVisualizationTypeTab === 'table' }"
+                                @click="defaultVisualizationTypeTab = 'table'"
+                            >分析表</span>
                         </div>
                     </div>
+                </div>
 
-                    <template v-if="defaultVisualizationTypeTab === 'chart'">
-                        <LineChart
-                            reportHeight="230px"
-                            :datax="schoolLineChartData.datax"
-                            :datay="schoolLineChartData.datay"
-                            :title="[selectedSchoolName + '在线']"
-                            :showCheckBox="false"
-                            :showBackground="true"
-                            :disableLabelRotate="true"
-                            isSmooth
-                            unit="人"
-                        />
-                    </template>
+                <template v-if="defaultVisualizationTypeTab === 'chart'">
+                    <LineChart
+                        reportHeight="280px"
+                        :datax="schoolLineChartData.datax"
+                        :datay="schoolLineChartData.datay"
+                        :showCheckBox="false"
+                        :showBackground="true"
+                        :disableLabelRotate="true"
+                        :tooltipDetailData="schoolChartDetailDataList"
+                        :showTooltipDetail="false"
+                        :showYAxisInteger="true"
+                        :clickDataPoint="false"
+                        unit="人"
+                    />
+                </template>
 
-                    <template v-else>
-                        <div class="table_info">
-                            共 {{ schoolTableData.length }} 条数据
-                        </div>
-                        <Table 
-                            :tableColumns="tableColumns" 
-                            :tableData="pagedTableData"
-                            :currentPage="currentPage"
-                            :pageSize="pageSize"
-                            :totalNum="schoolTableData.length"
-                            @paginationChange="handleTablePageChange"
-                        />
-                    </template>
-                </div>
+                <template v-else>
+                    <div class="table_info">
+                        共 {{ schoolTableData.length }} 条数据
+                    </div>
+                    <Table 
+                        :tableColumns="tableColumns" 
+                        :tableData="pagedTableData"
+                        :currentPage="currentPage"
+                        :pageSize="pageSize"
+                        :totalNum="schoolTableData.length"
+                        @paginationChange="handleTablePageChange"
+                    />
+                </template>
             </div>
         </div>
     </div>
@@ -196,34 +199,59 @@ const handleCustomDateChange = (val: [string, string] | null) => {
 const timeHours = ['0时','1时','2时','3时','4时','5时','6时','7时','8时','9时','10时','11时','12时','13时','14时','15时','16时','17时','18时','19时','20时','21时','22时','23时']
 
 const mainLineChartData = reactive({
-    datax: [...timeHours],
-    datay: [[0,2,5,3,4,3,5,8,15,25,40,60,80,100,120,150,180,200,220,210,190,160,120,80]],
+    datax: [] as string[],
+    datay: [[]] as number[][],
 })
 
+const chartDetailDataList = ref<any[]>([])
+const schoolChartDetailDataList = ref<any[]>([])
+
+const handleDataPointClick = async (payload: { dataIndex: number; schoolList: any[]; rawData: any }) => {
+    // 更新柱状图数据
+    if (payload.schoolList && payload.schoolList.length > 0) {
+        barChartData.datax = payload.schoolList.map((item: any) => item.xname ?? '')
+        barChartData.datay = payload.schoolList.map((item: any) => item.count ?? 0)
+        barChartSchoolList.value = payload.schoolList
+
+        // 柱状图更新后,默认选中第一个柱子,获取其 id 调用接口更新学校折线图
+        const firstSchoolId = payload.schoolList[0]?.id
+        if (firstSchoolId != null && firstSchoolId !== undefined) {
+            selectedSchoolName.value = payload.schoolList[0].xname ?? ''
+            await fetchSchoolLineChartData(firstSchoolId)
+        }
+    } else {
+        console.warn('schoolList 为空,无法更新柱状图')
+    }
+
+    // 更新时间标签
+    if (payload.dataIndex >= 0 && mainLineChartData.datax[payload.dataIndex]) {
+        const xname = mainLineChartData.datax[payload.dataIndex]
+        const hourMatch = xname.match(/(\d{1,2})时/)
+        if (hourMatch) {
+            onLineTime.value = parseInt(hourMatch[1], 10)
+        }
+    }
+}
+
+// 保存柱状图的完整学校列表数据
+const barChartSchoolList = ref<any[]>([])
+
 const barChartData = reactive({
-    datax: ['北京师大', '师达', '五十七', '五十七', '五十七', '一分校', '北师大', '北师大', '北师大'],
-    datay: [96, 91, 96, 96, 97, 96, 96, 96, 96],
+    datax: [] as string[],
+    datay: [] as number[],
 })
 
 const onLineTime = ref(0)
 const currentHourLabel = computed(() => String(onLineTime.value))
 
-const selectedSchoolName = ref('北京师大')
+const selectedSchoolName = ref('')
 
 const schoolLineChartData = reactive({
-    datax: [...timeHours],
-    datay: [[0,2,5,3,4,3,5,8,15,25,40,60,80,100,120,150,180,200,220,210,190,160,120,80]],
+    datax: [] as string[],
+    datay: [] as number[][],
 })
 
-const schoolTableData = ref([
-    { num: 1, school: '北京师大', account: '18738372732', name: '黎明村', operationDetail: '系统登录', operationTime: '26-01-03 10:05:11' },
-    { num: 2, school: '北京师大', account: '18738372732', name: '严谨', operationDetail: '系统登录', operationTime: '26-01-03 10:05:11' },
-    { num: 3, school: '北京师大', account: '18738372732', name: '庄重', operationDetail: '系统登录', operationTime: '26-01-03 10:05:11' },
-    { num: 4, school: '北京师大', account: '18738372732', name: '杨晓', operationDetail: '系统登录', operationTime: '26-01-03 10:05:11' },
-    { num: 5, school: '北京师大', account: '18738372732', name: '孙赏语', operationDetail: '系统登录', operationTime: '26-01-03 10:05:11' },
-    { num: 6, school: '北京师大', account: '18738372732', name: '赵梓萱', operationDetail: '系统登录', operationTime: '26-01-03 10:05:11' },
-    { num: 7, school: '北京师大', account: '18738372732', name: '朱玉哲', operationDetail: '系统登录', operationTime: '26-01-03 10:05:11' },
-])
+const schoolTableData = ref<any[]>([])
 
 const currentPage = ref(1)
 const pageSize = ref(20)
@@ -263,49 +291,47 @@ const HandleTabChange = (type: string, val: string) => {
     }
 }
 
-const handleBarClick = (index: number, name: string) => {
+const handleBarClick = async (index: number, name: string) => {
     selectedSchoolName.value = name
     currentPage.value = 1
-    const base = barChartData.datay[index] || 50
-    schoolLineChartData.datay = [[
-        Math.floor(Math.random() * 5),
-        Math.floor(Math.random() * 10),
-        Math.floor(base * 0.3),
-        Math.floor(base * 0.2),
-        Math.floor(base * 0.4),
-        Math.floor(base * 0.5),
-        Math.floor(base * 0.6),
-        Math.floor(base * 0.8),
-        Math.floor(base * 1.0),
-        Math.floor(base * 1.2),
-        Math.floor(base * 1.5),
-        Math.floor(base * 1.8),
-        Math.floor(base * 2.0),
-        Math.floor(base * 2.2),
-        Math.floor(base * 2.5),
-        Math.floor(base * 2.2),
-        Math.floor(base * 2.0),
-        Math.floor(base * 1.8),
-        Math.floor(base * 1.5),
-        Math.floor(base * 1.2),
-        Math.floor(base * 1.0),
-        Math.floor(base * 0.8),
-        Math.floor(base * 0.5),
-        Math.floor(base * 0.3),
-    ]]
-    const newRows = Array.from({ length: 50 }, (_, i) => ({
-        num: i + 1,
-        school: name,
-        account: `1873837273${String(i % 10).padStart(2, '0')}`,
-        name: ['黎明村', '严谨', '庄重', '杨晓', '孙赏语', '赵梓萱', '朱玉哲', '王小明', '李华', '张伟'][i % 10],
-        operationDetail: '系统登录',
-        operationTime: `26-01-03 10:05:${String(10 + (i % 50)).padStart(2, '0')}`,
-    }))
-    schoolTableData.value = newRows
+
+    // 获取当前柱子的 schoolId (schoolList 数组中 id 字段即为 schoolId)
+    const schoolItem = barChartSchoolList.value[index]
+    const schoolId = schoolItem?.id ?? 0
+
+    // 调用接口获取学校折线图数据
+    const timeType = dateRangeToTimeType[defaultDateRangeTab.value] ?? 1
+    const isToday = defaultDateRangeTab.value === 'today'
+    try {
+        const res = await getChartData({
+            schoolId,
+            timeType,
+            moduleCode: Number(defaultOnlineTypeTab.value) || 0,
+            startTime: customDateRange.value?.[0] ?? '',
+            endTime: customDateRange.value?.[1] ?? '',
+        })
+        console.log('点击柱子 getChartData 响应:', res)
+
+        const data = res.data || res
+        if (Array.isArray(data)) {
+            if (isToday) {
+                schoolLineChartData.datax = timeHours
+            } else {
+                schoolLineChartData.datax = data.map((item: any) => formatXname(item.xname ?? '', false))
+            }
+            schoolLineChartData.datay = [data.map((item: any) => item.count ?? 0)]
+            schoolChartDetailDataList.value = data.map((item: any) => ({
+                schoolList: item.schoolList || [],
+                count: item.count ?? 0,
+            }))
+        }
+    } catch (err) {
+        console.error('点击柱子 getChartData 请求失败:', err)
+    }
 }
 
 const handleExport = () => {
-    console.log('导出Excel', selectedSchoolName.value)
+    // console.log('导出Excel', selectedSchoolName.value)
 }
 
 // ==================== 数据请求 ====================
@@ -324,12 +350,12 @@ const fetchHeadData = async () => {
 }
 
 const fetchModuleList = async () => {
-    console.log('[fetchModuleList] 开始请求模块列表...')
+
     try {
         const res = await getModuleList()
-        console.log('[fetchModuleList] 接口原始响应:', JSON.stringify(res))
+
         const data = res.data || res
-        console.log('[fetchModuleList] 解析后的 data:', JSON.stringify(data))
+
         if (!Array.isArray(data)) {
             console.warn('[fetchModuleList] data 不是数组,实际类型:', typeof data, data)
             return
@@ -343,11 +369,11 @@ const fetchModuleList = async () => {
                 label: String(item.name ?? ''),
             }
         })
-        console.log('[fetchModuleList] 映射后的 onlineList:', JSON.stringify(onlineList))
+
         if (onlineList.length) {
             onlineTypeTablist.value = onlineList
             defaultOnlineTypeTab.value = onlineList[0].key
-            console.log('[fetchModuleList] 已设置 onlineTypeTablist,defaultOnlineTypeTab =', onlineList[0].key)
+
         } else {
             console.warn('[fetchModuleList] onlineList 为空,未更新标签列表')
         }
@@ -357,9 +383,55 @@ const fetchModuleList = async () => {
     }
 }
 
+const formatXname = (xname: string, isToday: boolean): string => {
+    if (isToday) return xname
+    if (!xname) return ''
+    // Try ISO format: 2026-04-08 12:00:00 or 2026-04-08T12:00:00
+    let match = xname.match(/(\d{4})-(\d{1,2})-(\d{1,2})[ T](\d{1,2})/)
+    if (match) {
+        return `${parseInt(match[2], 10)}月${parseInt(match[3], 10)}日 ${parseInt(match[4], 10)}时`
+    }
+    // Try short format: 4-8 12:00 or 4-8T12:00
+    match = xname.match(/^(\d{1,2})-(\d{1,2})[ T](\d{1,2})/)
+    if (match) {
+        return `${parseInt(match[1], 10)}月${parseInt(match[2], 10)}日 ${parseInt(match[3], 10)}时`
+    }
+    return xname
+}
+
+const fetchSchoolLineChartData = async (schoolId: number) => {
+    const timeType = dateRangeToTimeType[defaultDateRangeTab.value] ?? 1
+    const isToday = defaultDateRangeTab.value === 'today'
+    try {
+        const res = await getChartData({
+            schoolId,
+            timeType,
+            moduleCode: Number(defaultOnlineTypeTab.value) || 0,
+            startTime: customDateRange.value?.[0] ?? '',
+            endTime: customDateRange.value?.[1] ?? '',
+        })
+        const data = res.data || res
+        if (Array.isArray(data)) {
+            if (isToday) {
+                schoolLineChartData.datax = timeHours
+            } else {
+                schoolLineChartData.datax = data.map((item: any) => formatXname(item.xname ?? '', false))
+            }
+            schoolLineChartData.datay = [data.map((item: any) => item.count ?? 0)]
+            schoolChartDetailDataList.value = data.map((item: any) => ({
+                schoolList: item.schoolList || [],
+                count: item.count ?? 0,
+            }))
+        }
+    } catch (err) {
+        console.error('fetchSchoolLineChartData 请求失败:', err)
+    }
+}
+
 const fetchChartData = async () => {
     try {
         const timeType = dateRangeToTimeType[defaultDateRangeTab.value] ?? 1
+        const isToday = defaultDateRangeTab.value === 'today'
         const res = await getChartData({
             schoolId: 0,
             timeType,
@@ -367,10 +439,37 @@ const fetchChartData = async () => {
             startTime: customDateRange.value?.[0] ?? '',
             endTime: customDateRange.value?.[1] ?? '',
         })
-        console.log('getChartData 响应数据:', res)
+
         const data = res.data || res
-        if (data) {
-            // TODO: 根据后端返回数据结构更新图表
+        if (Array.isArray(data)) {
+            if (isToday) {
+                mainLineChartData.datax = timeHours
+            } else {
+                mainLineChartData.datax = data.map((item: any) => formatXname(item.xname ?? '', false))
+            }
+            mainLineChartData.datay = [data.map((item: any) => item.count ?? 0)]
+            chartDetailDataList.value = data.map((item: any) => ({
+                schoolList: item.schoolList || [],
+                count: item.count ?? 0,
+            }))
+
+            // 初始化柱状图数据:取第一个数据点的 schoolList
+            if (data.length > 0) {
+                const firstItem = data[0]
+                const schoolList = firstItem.schoolList || []
+                if (schoolList.length > 0) {
+                    barChartData.datax = schoolList.map((item: any) => item.xname ?? '')
+                    barChartData.datay = schoolList.map((item: any) => item.count ?? 0)
+                    barChartSchoolList.value = schoolList
+                }
+                // 初始化时间标签
+                if (firstItem.xname) {
+                    const hourMatch = String(firstItem.xname).match(/(\d{1,2})时/)
+                    if (hourMatch) {
+                        onLineTime.value = parseInt(hourMatch[1], 10)
+                    }
+                }
+            }
         }
     } catch (err) {
         console.error('getChartData 请求失败:', err)
@@ -451,6 +550,16 @@ onMounted(() => {
     border: 1px solid #edf0f5;
     border-radius: 8px;
     padding: 20px;
+    width: 100%;
+    box-sizing: border-box;
+
+    :deep(.echart_content) {
+        width: 100%;
+
+        .chart_box {
+            width: 100%;
+        }
+    }
 
     .panel_title {
         font-size: 16px;
@@ -512,17 +621,6 @@ onMounted(() => {
     }
 }
 
-.bottom_section {
-    display: grid;
-    grid-template-columns: 1fr 1fr;
-    gap: 16px;
-
-    .left_panel,
-    .right_panel {
-        min-height: 360px;
-    }
-}
-
 .table_info {
     font-size: 13px;
     color: #909399;