فهرست منبع

添加用户查询,日志监控和业务监控的静态页面

lm 2 هفته پیش
والد
کامیت
9e19c4964a

+ 49 - 0
package-lock.json

@@ -10,7 +10,9 @@
       "dependencies": {
         "@element-plus/icons-vue": "^2.3.1",
         "axios": "^1.9.0",
+        "echarts": "^6.0.0",
         "element-plus": "^2.9.11",
+        "lodash": "^4.17.21",
         "particles.js": "^2.0.0",
         "vue": "^3.5.13",
         "vue-router": "^4.5.1",
@@ -1587,6 +1589,16 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/echarts": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmmirror.com/echarts/-/echarts-6.1.0.tgz",
+      "integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "tslib": "2.3.0",
+        "zrender": "6.1.0"
+      }
+    },
     "node_modules/element-plus": {
       "version": "2.9.11",
       "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.9.11.tgz",
@@ -2315,6 +2327,12 @@
         "node": ">=8.0"
       }
     },
+    "node_modules/tslib": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
+      "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
+      "license": "0BSD"
+    },
     "node_modules/typescript": {
       "version": "5.8.3",
       "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.8.3.tgz",
@@ -2474,6 +2492,15 @@
       "peerDependencies": {
         "vue": "^3.2.0"
       }
+    },
+    "node_modules/zrender": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmmirror.com/zrender/-/zrender-6.1.0.tgz",
+      "integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "tslib": "2.3.0"
+      }
     }
   },
   "dependencies": {
@@ -3380,6 +3407,15 @@
         "gopd": "^1.2.0"
       }
     },
+    "echarts": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmmirror.com/echarts/-/echarts-6.1.0.tgz",
+      "integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==",
+      "requires": {
+        "tslib": "2.3.0",
+        "zrender": "6.1.0"
+      }
+    },
     "element-plus": {
       "version": "2.9.11",
       "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.9.11.tgz",
@@ -3864,6 +3900,11 @@
         "is-number": "^7.0.0"
       }
     },
+    "tslib": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
+      "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
+    },
     "typescript": {
       "version": "5.8.3",
       "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.8.3.tgz",
@@ -3934,6 +3975,14 @@
       "requires": {
         "@vue/devtools-api": "^6.0.0-beta.11"
       }
+    },
+    "zrender": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmmirror.com/zrender/-/zrender-6.1.0.tgz",
+      "integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==",
+      "requires": {
+        "tslib": "2.3.0"
+      }
     }
   }
 }

+ 3 - 1
package.json

@@ -16,7 +16,9 @@
     "particles.js": "^2.0.0",
     "vue": "^3.5.13",
     "vue-router": "^4.5.1",
-    "vuex": "^4.1.0"
+    "vuex": "^4.1.0",
+    "lodash": "^4.17.21",
+    "echarts": "^6.0.0"
   },
   "devDependencies": {
     "@types/node": "^22.15.26",

+ 171 - 0
src/baseComponents/FormSearchGroup.vue

@@ -0,0 +1,171 @@
+
+<!-- 需要配合 page_search   search_content    content_left  一起使用 -->
+ 
+<template>
+    <div class="simple_filter">
+        <template v-for="item in props.searchList" :key="item.key">
+            <el-select 
+                v-if="item.type == 'select' && !item.hidden" 
+                v-model="searchData[item.key]" 
+                @change="(val)=>HandleSearchItemChange(item.key)" 
+                :placeholder="item.placeholder"  
+                :disabled="formDisabled || item.disabled"
+            >
+                <el-option 
+                    v-for="optItm in item.options" 
+                    :key="optItm.value" :value="optItm.value" 
+                    :label="optItm.label" 
+                    :disabled="optItm.disabled ? true : false">
+                </el-option>
+            </el-select>
+
+
+
+            <!-- :size="size" -->
+            <el-date-picker
+                @change="(val)=>HandleSearchItemChange(item.key)" 
+                v-else-if="item.type == 'datePicker'"  
+                v-model="searchData[item.key]"
+                :disabeld="formDisabled || item.disabled"
+                type="daterange"
+                range-separator="-"
+                start-placeholder="开始时间"
+                end-placeholder="结束时间"
+            />
+
+
+
+            <!-- 这个输入框的搜索要动态查询 ————  -->
+            <el-input 
+                v-else-if="item.type == 'input'"  
+                v-model="searchData[item.key]" 
+
+                @input="(val,e)=>HandleInputChange(val,e,item.key)"
+
+                @keyup.enter="(event)=>HandleSearchItemChange(item.key)" 
+                :placeholder="item.placeholder"  
+                :disabled="formDisabled || item.disabled"
+            >
+                <template #append>
+                    <el-button @click="HandleSearchItemChange(item.key)" :icon="Search" />
+                </template> 
+            </el-input>
+
+
+            <el-radio-group
+                v-else-if="item.type == 'radio'" 
+                v-model="searchData[item.key]" 
+                @change="()=>HandleSearchItemChange(item.key)"
+                :disabled="formDisabled || item.disabled"
+            >
+                <el-radio v-for="radioItem in item.options" :key="radioItem.value" :value="radioItem.value">{{ radioItem.label }}</el-radio>
+            </el-radio-group>
+
+        </template>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { Search } from '@element-plus/icons-vue'
+
+import { ref, watch } from 'vue'
+
+interface SearchItem {
+    key:String,
+    type:'input'|'select'|'datePicker'|'radio',
+    placeholder?:String,
+    hidden:boolean,
+    defaultValue?:any,
+    disabled?:Boolean
+    options?:{
+        label:String,
+        value:String,
+        disabled?:Boolean
+    }[] 
+}
+
+const props = defineProps({
+    searchList:{
+        type:Array<SearchItem>,
+        required:true
+    },
+    formDisabled:{
+        type:Boolean,
+        required:false
+    }
+})
+
+const emit = defineEmits(['searchChange'])
+
+const searchData = ref({})
+
+
+// 筛选条件变更触发新的查询 
+const HandleSearchItemChange = async (key)=>{
+    //   触发父级页面的查询
+    emit('searchChange',searchData.value,key)
+}
+
+const inputFlag = ref() 
+
+const HandleInputChange = (val,e,key)=>{
+    if(inputFlag.value){
+        // 还在定时器内,延迟触发
+        clearTimeout(inputFlag.value)
+        inputFlag.value = ''
+    }
+    // 要防抖
+    inputFlag.value = setTimeout(()=>{
+        console.log('input__输入的值',val)
+        //   触发父级页面的查询
+        emit('searchChange',searchData.value,key)
+    },400)
+}
+
+
+// 
+watch(()=>props.searchList, (newVal)=>{
+    searchData.value = {}
+    // console.log('查询项变更了===表单',newVal)
+    newVal.forEach(item=>{
+        if(item.defaultValue){
+            searchData.value[item.key] = item.defaultValue
+        }
+    })
+    // emit('searchChange',searchData.value)
+},{immediate:true,deep:true})
+
+</script>
+
+
+<style lang="scss" scoped>
+.simple_filter {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+
+  :deep(.el-date-editor){
+    max-width: 240px;
+  }
+  
+  :deep(.el-input){
+    width: 260px;
+  }
+
+  :deep(.el-radio-group){
+    .el-radio{
+        margin-right: 20px;
+    }
+  }
+
+}
+
+
+.page_search .search_content .content_right{
+    .el-button {
+        margin-left: -20px !important;
+        min-width: auto !important;
+    }
+}
+    
+</style>

+ 213 - 0
src/baseComponents/HonorTimeLine.vue

@@ -0,0 +1,213 @@
+<template>
+<div class="timeline_component">
+    <el-timeline v-if="honorList?.length > 0" class="timeline" v-for="(honorItm) in honorList" :key="honorItm.id">
+        <el-timeline-item
+            placement="top"
+            center="true"
+            :timestamp="honorItm.awardDatetime"
+            :hide-timestamp="true"
+        >
+            <div class="timeline_item">
+                <div class="base_summarize">
+                    <!-- <span class="timestamp">{{ honorItm.awardDatetime }}</span> -->
+                    <!-- <span class="honor_type one_line_ellipsis"  :title="honorItm.honorTypeName">{{ honorItm.honorTypeName }}</span> -->
+                </div>
+
+                <div class="honoor_introduce">
+                    <!-- <img :src="honorItm.certPicUrl" alt=""> -->
+                    <div class="honor_msg">
+                        <div class="base_msg">
+                            <div class="title one_line_ellipsis" :title="honorItm.honorName">{{ honorItm.honorName }}</div>
+                            <div class="honor_people">访问ip:{{ honorItm.teacherName }}</div>
+                            <!-- <div v-if="honorItm.inputData?.length > 0" class="introduce_list one_line_ellipsis">
+                                {{ 
+                                    honorItm.inputData?.map(item=>{
+                                        return item
+                                    }).join('&nbsp; &nbsp;')
+                                }}
+                            </div> -->
+                            <!-- <ul v-if="honorItm.radioData?.length > 0" class="tag_list one_line_ellipsis" >
+                                <li v-for="tag in honorItm.radioData">{{ tag}}</li>
+                            </ul> -->
+                        </div>
+                        <div class="operation">
+                            <div class="honor_status">
+                                <span class="upload_time">{{ honorItm.awardDatetime }}</span>
+                                <slot name="status"  :honorItm="honorItm">
+                                    <!-- <span class="status">{{ honorItm.reviewStatus }}</span> -->
+                                </slot>
+                            </div>
+                            <div class="table_row_option">
+                                <slot name="operation" :honorItm="honorItm"></slot>
+                             </div>
+                        </div>
+                    </div>
+                </div>
+                
+            </div>
+        </el-timeline-item>
+        <el-timeline-item  placement="top" />
+    </el-timeline>
+    <div class="no_data_bg" v-else>
+        <span>暂无数据</span>
+    </div>
+</div>
+</template>
+
+<script setup lang="ts">
+interface HonorItem {
+    id:String,
+    awardDatetime: String,
+    certPicUrl:String,
+    honorTypeName: String,
+    honorName:String,
+    teacherName:String,
+    radioData:Array<String>,
+    inputData:Array<Object>    
+    createdAt:String,
+    reviewStatus:String,
+}
+const props = defineProps({
+    honorList:{
+        type:Array<HonorItem>,
+        default:()=>[]
+    }
+})
+</script>
+
+
+<style lang="scss" scoped>
+.timeline{
+    padding:0;
+}
+
+:deep(.el-timeline){
+    .el-timeline-item:last-child{
+        padding-bottom: 10px;
+    }
+    
+    .el-timeline-item{
+        justify-content: flex-start;
+        .el-timeline-item__wrapper{
+            padding:0px;
+        }
+
+        .el-timeline-item__node{
+            width: 5px;
+            height: 5px;
+
+        }
+
+        .el-timeline-item__tail{
+            left:1px
+        }
+    }
+
+}
+
+
+.timeline_item{
+    position: relative;
+    width: 100%;
+    display: flex;
+    align-items: flex-start;
+
+    .base_summarize{
+        flex-grow: 0;
+        flex-shrink: 0;
+        display: flex;
+        flex-direction: column;
+        // width: 94px;
+        width: 24px;
+        .honor_type{
+            font-size: 14px;
+            color:#999;
+            cursor: pointer;
+        }
+        .timestamp{
+            font-weight: 600;
+            font-size: 16px;
+            color:#333;
+        }
+    }
+    .honoor_introduce{
+        flex:1 1;
+        display: flex;
+        gap:16px;
+        background-color: #F8FAFE;
+        border:solid #EBEEF5 1px;
+        border-radius: 8px;
+        position: relative;
+        overflow: auto;
+       
+        padding:10px 20px 10px 10px;
+
+        &:hover{
+            background-color: rgba(46, 100, 250,0.1);
+            border:solid #2E64FA 1px;
+        }
+        img{
+            width: 200px;
+            height: 117px;
+        }
+        .honor_msg{
+            flex:1 1;
+            position: relative;
+            overflow: auto;
+            display: flex;
+            justify-content: space-between;
+            gap:20px;
+            .base_msg{
+                overflow: auto;
+                flex:1 1;
+                display: flex;
+                flex-direction: column;
+                gap:8px;
+
+                .title{
+                    font-weight: 600;
+                    font-size: 16px;
+                    color:#333;
+                    line-height: 22px;
+                }
+                .honor_people{
+                    font-size: 14px;
+                }
+                .introduce_list{
+                    font-size: 14px;
+                    color:#999;
+                }
+                .tag_list{
+                    display: flex;
+                    gap:10px;
+                    font-size: 12px;
+                    color:#2E64FA;
+                    li{
+                        padding:3px 8px;
+                        background-color: rgba(46, 100, 249,0.1);
+                    }
+                }
+            }
+
+            .operation{
+                display: flex;
+                flex-direction: column;
+                justify-content: space-between;
+
+                .honor_status{
+                    justify-content: flex-end;
+                    display: flex;
+                    gap:20px;
+                    .upload_time{
+                        color:#999;
+                    }
+                }
+                .table_row_option{
+                    justify-content: flex-end;
+                    gap:20px;
+                }
+            }
+        }
+    }
+}
+</style>

+ 815 - 0
src/baseComponents/LineChart.vue

@@ -0,0 +1,815 @@
+<template>
+    <div class="echart_content">
+        <div class="is_show_all" v-if="showCheckBox">
+            <el-checkbox v-model="checkAll" :indeterminate="isIndeterminate"  @change="ToggleChangeAll" >
+                显示全部
+            </el-checkbox>
+        </div>
+        <div ref="line_chart" class="chart_box" :style="{height:reportHeight || '400px'}"></div>
+    </div>
+</template>
+<script>
+import _ from 'lodash'
+import * as echarts from 'echarts';
+
+export default {
+    name: "LineChart",//折线图组件
+    data() {
+        return {
+            echart: null,//图标实例
+
+            checkAll: true,//是否显示全部
+            isIndeterminate: false,
+            legendSelected: {},//图例选中状态
+            legenSelectList: [],//选择的图例
+            legenAllList:this.title,//所有的图例数据
+
+        }
+    },
+
+    props: {
+        gridRight: {
+            type: Number,
+            default: 0,
+        },//grid右间距
+        gridTop: {
+            type: [Number,String],
+            default: '',
+        },
+        legendWidth: {
+            type: [Number,String],
+            default: 'auto',
+        },
+        showCheckBox: {
+            type: Boolean,
+            default: true,
+        },//是否显示全部按钮
+        extraText: {
+            type: Boolean,
+            default: true,
+        },//是否占比
+
+        datax: {
+            type: Array,
+            default: function () {
+                return [
+                    'G10', 'G9', 'G8', 'G7', 'G6', 'G5', 'G4', 'G3', 'G2', 'G1'
+                ];
+            }
+        },//x轴数据
+        datay: {
+            type: Array,
+            default: function () {
+                return [
+                   
+
+                ];
+            }
+        },//y轴数据
+
+        tooltipData:{
+            type: Array,
+            default: function () {
+                return [];
+            }
+        },//悬浮弹窗的数据
+        colors: {
+            type: Array,
+            default: function () {
+                return window.$global.getScorePerformanceAnalysis();
+            }
+        },//柱状图颜色
+        unit: {
+            type: String,
+            default: ''
+        },//单位
+        title: {
+            type: Array,
+            default: ()=>[]
+        },//图例标题
+        // isSetMarkNumber:{
+        //     type: Boolean,
+        //     default: false
+        // },//是否设置辅助线label值
+        markNumber: {
+            type: Array,
+            default: ()=>[]
+        },//辅助线位置值 
+
+        isSmooth: {
+            type: Boolean,
+            default: false
+        },//是否是曲线图
+
+        // showMarkLine: {
+        //     type: Boolean,
+        //     default: false
+        // },//是否显示辅助线平均值
+
+        showBackground:{
+            type:Boolean,
+            default:true,
+        },//是否显示背景阴影  默认显示
+
+        isShowLabel:{
+            type:Boolean,
+            default:false,
+        },//是否显示柱状图label  默认显示
+        labelColor:{
+            type: String,
+            default:'',
+        },//柱状图label 颜色
+        showMarkPoint:{
+            type: Boolean,
+            default:false,
+        },//是否显示气泡图  默认不显示
+
+        legendList:{
+            type: Array,
+            default: () => [],
+        },//默认显示的图例数据  
+
+        yInverse:{
+            type:Boolean,
+            default:false,
+        },//y轴是否反序
+
+        updateKey:{
+            type:Number,
+            default:0
+        },
+        gridLeft:{
+            type:Number,
+            default:20
+        },
+        fontSize:{//字体大小
+            type:[Number,String],
+            default:''
+        },
+        fontColor:{
+            type:String,
+            default:''
+        },
+        reportHeight:{//报告高度
+            type:String,
+            default:''
+        }
+
+    },
+    watch: {
+
+
+        legendList()
+        {
+            this.LoadEchart();
+        },//重新加载图表
+        updateKey()
+        {
+            this.LoadEchart();
+        },//重新加载图表
+
+        datay: {
+            handler: 'LoadEchart',
+            deep: true,
+        },//数值变化时重新加载图表
+    },
+    created() {
+        window.addEventListener('resize', this.HandleResize);//创建时增加监听窗口变化事件
+    },
+    beforeDestroy() {
+        // 组件销毁移除监听 防止内存泄漏
+        window.removeEventListener('resize', this.HandleResize);
+        this.echart.dispose(); // 销毁之前的实例
+    },
+    mounted() 
+    {
+     
+        this.LoadEchart();//初始化图表
+        this.HandleResize()
+    },
+    methods: {
+        markLine(index){
+            return this.markNumber.length ? {
+                data: [{
+                    type: 'average',
+                    name: '平均值',
+                    yAxis: this.markNumber.length?this.markNumber[index]:null//Y 值为给定值的标记线
+                }],
+                label: {
+                    position:"end",
+                    formatter: (params)=>{
+                        if(this.markNumber.length){
+                            return this.markNumber[index]
+                        }else{
+  
+                            return params.value//' {c}'
+                        }
+                    }// 显示平均值数值
+                },
+                lineStyle: {
+                    type: 'dashed', // 虚线样式
+                    color: this.colors[index]// 线条颜色
+                }
+            } : {}
+        },
+        //加载echarts
+        LoadEchart() 
+        {
+            
+            // 简化图例选中列表的初始化
+            this.legenSelectList = this.legendList.length > 0 
+                ? [...this.legendList] 
+                : []; //
+            
+            this.legenAllList = [...this.title];
+           
+            if (this.echart) {
+                this.echart.dispose(); // 销毁之前的实例
+            }
+            //devicePixelRatio: 2   2表示每个逻辑像素对应 2 个物理像素。这会进一步提高图表的清晰度,适用于更高分辨率的屏幕  解决高分辨率显示器文字模糊的问题
+            this.echart = echarts.init(this.$refs.line_chart, { devicePixelRatio: 2 });//获取echarts容器  如果组件同一个页面多处使用 这里不能用id获取实例
+            
+            // 图例选中状态初始化
+            this.legendSelected = {};
+            this.title.forEach((name, index) => {
+                this.legendSelected[name] = this.legenSelectList.includes(name);
+            });
+            
+            // 更新全选状态
+            this.checkAll = this.legenSelectList.length === this.legenAllList.length;
+            this.isIndeterminate = this.legenSelectList.length > 0 && 
+                                this.legenSelectList.length < this.legenAllList.length;
+           
+
+            console.log("打印this.isIndeterminate",this.isIndeterminate);
+            const maxValue = Math.max(...this.datay.flat());
+            const minValue=Math.min(...this.datay.flat());
+            const hideTick = minValue < 0 || this.yInverse;//是否隐藏刻度线 最小值小于0  或者反序y轴的时候隐藏刻度线
+            // console.log("打印最小值",minValue);
+            // 获取当前显示图例对应数据中的最大值
+            const visibleSeriesIndices = this.legenSelectList.map(name => this.title.indexOf(name));
+            const visibleData = visibleSeriesIndices
+                .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);
+            let gridTop= this.showMarkPoint ? 45 : 20;
+            // 无图例 默认20  有图例 70
+            if(this.title.length>0)
+            {
+                gridTop=70;
+            }
+            let option = {
+
+                tooltip: {
+                    trigger: 'axis',
+
+                    axisPointer: {
+                        type: 'line', // 指示器类型,cross 表示交叉指示线
+                        // lineStyle: {
+                        //     color: 'red',
+                        //     width: 2,
+                        //     type: 'dashed'
+                        // },
+                        // crossStyle: {
+                        //     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,
+                    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>`;
+                                }
+                                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>`;
+                                }
+                                
+                            })
+                            tooltip += `</div>`;
+                            return tooltip;
+                        
+                        
+                        
+                    },
+                },
+                legend: {
+                    show: this.title.length ? true : false,//是否显示图例
+                    left: this.showCheckBox ? "110px" : "0px",//图例距左边距离  有显示全部的 默认110px
+                    width:this.legendWidth,
+                    itemGap: 20,//图例间距
+                    itemHeight: 10,//图例的高度
+                    itemWidth: 25,//图例的宽度
+
+                    
+                    data: this.title,
+                    textStyle: {
+                        fontSize: 12, 
+                        color: "#333",
+                        lineHeight: 30,
+                    },
+                    selectedMode: true,//允许图例切换显示
+                    selected: this.legendSelected, // 绑定图例选中状态
+                    type:'scroll',//启用图例滚动  翻页功能
+                    pageIconColor: '#999', // 翻页按钮默认颜色
+                    pageIconInactiveColor: '#ccc', // 翻页按钮禁用状态颜色
+                    pageButtonItemGap: 5, // 分页按钮与图例的间距
+                    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,
+                    right: this.gridRight,
+                    top: this.gridTop || gridTop,
+                    bottom:0,
+                    containLabel: true, //用于控制网格区域是否包含坐标轴的标签  true 可以确保所有标签都能完整显示,不会被裁剪。
+                },
+                xAxis: {
+                    type: 'category',
+                    data: this.datax,
+                    boundaryGap: true,
+                    triggerEvent: true,
+
+                    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: singleSeriesWidth < 60 ? 45 : 0,
+                        fontSize: this.fontSize?this.fontSize:14,
+                        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',
+                    axisLabel: {
+                        margin: 20,
+                        formatter: '{value}' + this.unit,
+                        interval: 0, // 显示所有 label
+                        fontSize: this.fontSize?this.fontSize:14,
+                        color: this.fontColor || "#666",
+                        fontWeight: 400
+                    },
+                    splitNumber: 5,//固定显示5个刻度  非强制 不一定生效
+                 
+                    axisTick: {
+                        interval: 0 // 显示所有刻度线
+                    },
+                    // splitNumber:4,//固定将y轴划分为5段
+                    
+                    splitLine: {
+                        show: true, //显示横向网格线
+                        lineStyle:
+                        {
+                            color: "#E4E7ED",//横向网格线的颜色值 
+                            with: 1,
+                            type: "solid"//线的类型  solid  实线  dashed  虚线  dotted  点状线
+                        }
+                    },
+                    splitArea: {
+                        show: true,
+                        areaStyle: {
+                            color: ["#fafafa", "#ffffff"]
+                        },// 网格线设置隔行背景色
+                    },
+                    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: [{
+                        type: 'group',
+                        key: 'mark-point-group',
+                        z: 10, // 整体 markPoint 图层层级
+                    }]
+                },
+                series: this.datay.map((data, index) => ({
+                    type: 'line',
+                    name: this.title[index],
+                    // symbol: 'none',//设置无折线点
+                    symbol: "emptyCircle",//设置折线点为圆形
+                    showSymbol: true,//显示小圆点
+                    symbolSize:8,//折线点大小
+                    label: { 
+                        show: this.isShowLabel, //是否显示数值
+                        color: this.labelColor || this.colors[index], 
+                        formatter: "{c}" + this.unit 
+                    },
+                    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,//是否显示气泡      
+                    itemStyle: {
+                        color: this.colors[index],
+                        borderColor: this.colors[index],
+                        borderWidth: 1,//空心圆的边框粗细
+                        shadowColor: 'rgba(0, 0, 0, .1)',
+                        shadowBlur: 0,
+                    },
+                    lineStyle:{
+                        width:3,//设置折线的粗细 
+                    },
+                    areaStyle:this.showBackground? {
+                        color: {
+                            type: 'linear',
+                            x: 0,//渐变起点x
+                            y: 0,//渐变起点y
+                            x2: 0,//渐变终点x
+                            y2: 1,//渐变终点y
+                            colorStops: [
+                                {
+                                    offset: 0, color: this.colors[index] + '30' // 30% 透明度
+                                },
+                                {
+                                    offset: 1, color: this.colors[index] + '00'   // 0% 透明度
+                                }
+                            ],
+                        },
+                    }:undefined,//折线背景阴影设置
+                    emphasis: {
+                        focus: 'series',
+                        blurScope: 'coordinateSystem'
+                    },
+                    data:data,
+                })),
+            };
+
+            this.LabelMouseOver();//X轴文字过程显示省略号,鼠标hover上去时,弹出一个tooltip显示全称
+            this.echart.setOption(option);
+            this.echart.on("legendselectchanged", this.HandleLegendSelectChanged);
+
+        },
+
+        // 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");
+                div.setAttribute("id", "tooltipContent");
+                div.style.display = "block";
+                document.querySelector("html").appendChild(div);//放入body下
+            }
+            this.echart.on("mouseover", (params) => 
+            {
+                
+                if (params.componentType == "xAxis") {
+                let elementDiv = document.querySelector("#tooltipContent");
+                //设置悬浮文本的位置以及样式
+                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;
+                elementDiv.innerHTML = params.value;
+                document.querySelector("html").onmousemove = function (event) {
+                    let elementDiv = document.querySelector("#tooltipContent");
+                    let xx = event.pageX - 10;
+                    let yy = event.pageY - 25;
+                    elementDiv.style.top = yy + "px";
+                    elementDiv.style.left = xx + "px";
+                };
+                }
+            });
+            this.echart.on("mouseout", function (params) {
+                //注意这里,我是以X轴显示内容过长为例,如果是y轴的话,需要改为yAxis
+                if (params.componentType == "xAxis") {
+                    let elementDiv = document.querySelector("#tooltipContent");
+                    elementDiv.style.cssText = "display:none";
+                }
+            });
+        },
+        // 图例与显示全部按钮联动 事件
+        HandleLegendSelectChangeds(params) 
+        {
+           
+            const selected = params.selected;
+            this.legenSelectList=[];
+            for(var i=0;i<this.legenAllList.length;i++)
+            {
+                if(selected[this.legenAllList[i]]==true)
+                {
+                    this.legenSelectList.push(this.legenAllList[i]);
+                }                                                         
+            }
+            this.LoadEchart();
+        },
+        //全选
+        ChangeCheckAll(value)
+        {
+            // console.log("打印value",this.checkAll);
+            if(value)
+            {
+                //选中全选
+                this.legenSelectList=this.legenAllList;
+                
+                this.isIndeterminate = false;
+            }
+            else
+            {
+                this.legenSelectList=[];
+                this.isIndeterminate = false;
+            }
+            this.LoadEchart();
+        },
+
+
+        //图例与显示全部按钮联动 事件
+        HandleLegendSelectChanged(params) {
+            const legendData = this.echart.getOption().legend[0].data;
+            const selected = params.selected;
+            // 检查是否有任何图例项被隐藏
+            this.legendSelected=selected;
+            const allSelected = legendData.every(
+                (seriesName) => selected[seriesName]
+            );
+            const noneSelected = legendData.every(
+                (seriesName) => !selected[seriesName]
+            );
+            const someSelected = !allSelected && !noneSelected;
+            // 更新“显示全部”按钮的状态
+            if (noneSelected) {
+                this.checkAll = false;
+            }
+            if (allSelected) {
+                this.checkAll = true;
+            }
+            this.isIndeterminate = someSelected;
+            let legendSelectedList = [];
+            for(let key in this.legendSelected) {
+                if(this.legendSelected[key]) {
+                    legendSelectedList.push(key)
+                }
+            }
+
+            this.$emit('SelectLegend', legendSelectedList)
+
+        },
+
+        // 显示全部按钮
+        ToggleChangeAll() 
+        {
+            if (!this.showCheckBox) return false
+            console.log(this.checkAll)
+           
+                
+            const legendData = this.echart.getOption().legend[0].data;
+            legendData.forEach((seriesName) => {
+                    this.echart.dispatchAction({
+                        type: this.checkAll ? "legendSelect" : "legendUnSelect",
+                        name: seriesName,
+                    });
+            });
+           
+           
+            this.isIndeterminate = false;
+
+            let legendSelectedList = [];
+            if(this.checkAll)
+            {
+                legendSelectedList = this.title;
+            }
+            else
+            {
+                legendSelectedList = [];
+            }
+
+            this.$emit('SelectLegend',legendSelectedList)
+        },
+
+        //显示X轴标签悬浮框
+        ShowXAxisTooltip()
+        {
+            const chart = this.echart; // 获取 echarts 实例
+            const container = chart.getDom(); // 获取容器 DOM
+
+            let tooltipDiv = document.getElementById('x-axis-tooltip');
+            if (!tooltipDiv) {
+                tooltipDiv = document.createElement('div');
+                tooltipDiv.id = 'x-axis-tooltip';
+                tooltipDiv.style.cssText = `
+                    position: absolute;
+                    z-index: 9999;
+                    background: #303133;
+                    color: #fff;
+                    padding: 6px 10px;
+                    border-radius: 4px;
+                    font-size: 12px;
+                    pointer-events: none;
+                    display: none;
+                    white-space: nowrap;
+                    `;
+                document.body.appendChild(tooltipDiv);
+            }
+
+            chart.on('mouseover', (params) => {
+                if (params.componentType === 'xAxis') {
+                const value = params.value;
+                if (value && value.length > 5) { // 设置长度判断是否需要显示 tooltip
+                    tooltipDiv.style.display = 'block';
+                    tooltipDiv.innerText = value;
+                }
+                }
+            });
+
+            chart.on('mouseout', (params) => {
+                if (params.componentType === 'xAxis') {
+                tooltipDiv.style.display = 'none';
+                }
+            });
+
+            // 动态定位 tooltip
+            container.addEventListener('mousemove', (e) => {
+                tooltipDiv.style.left = `${e.pageX + 10}px`;
+                tooltipDiv.style.top = `${e.pageY + 15}px`;
+            });
+        },
+
+        // X轴文字过程显示省略号,鼠标hover上去时,弹出一个tooltip显示全称
+        extension() {
+        // 注意这里,是以X轴显示内容过长为例,如果是y轴的话,需要把params.componentType == 'xAxis'改为yAxis
+        // 判断是否创建过div框,如果创建过就不再创建了
+        // 该div用来盛放文本显示内容的,方便对其悬浮位置进行处理
+        let elementDiv = document.getElementById("extension");
+        if (!elementDiv) {
+            let div = document.createElement("div");
+            div.setAttribute("id", "extension");
+            div.style.display = "block";
+            document.querySelector("html").appendChild(div);
+        }
+        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;
+            elementDiv.innerHTML = params.value;
+            document.querySelector("html").onmousemove = function (event) {
+                let elementDiv = document.querySelector("#extension");
+                let xx = event.pageX - 10;
+                let yy = event.pageY + 15;
+                elementDiv.style.top = yy + "px";
+                elementDiv.style.left = xx + "px";
+            };
+            }
+        });
+        this.myChart.on("mouseout", function (params) {
+            //注意这里,我是以X轴显示内容过长为例,如果是y轴的话,需要改为yAxis
+            if (params.componentType == "xAxis") {
+            let elementDiv = document.querySelector("#extension");
+            elementDiv.style.cssText = "display:none";
+            }
+        });
+        },
+
+        // 监听窗口大小变化,重新计算表格高度 使用节流防止频繁改变窗口大小导致计算量过大而页面卡顿
+        HandleResize: _.throttle(function () {
+            this.$nextTick(() => {
+                this.LoadEchart();
+            });
+
+        }, 500), // 节流 500 毫秒内最多执行一次
+    }
+}
+</script>
+<style lang="scss" scoped>
+.is_show_all
+{
+    top:10px;
+}
+</style>

+ 901 - 0
src/baseComponents/MultipleBarCharts.vue

@@ -0,0 +1,901 @@
+<template>
+    <div ref="bar_echart"  class="chart_width" :style="{height: `${height}px`}">
+
+    </div>
+</template>
+<script>
+import _ from 'lodash'
+import * as echarts from 'echarts'; 
+
+  export default{
+
+    name: "MultipleBarCharts",//单个柱状图组件
+    data(){
+      return {
+        echart:null,//图标实例
+        colorList:[],//       
+        // datax:[1,2,3,4,5,6,7],//x轴数据
+        // datay:[50, 60, 70, 80, 70, 70, 100],//y轴数据
+        // color:'#5470C6'
+      }
+    },
+
+    props:{
+      datax:{
+        type:Array,
+        default:function(){
+          return [];
+        }
+      },//x轴数据
+      datay:{
+        type:Array,
+        default:()=>[]
+      },//y轴数据
+      color:{
+        type:String,
+        default:function(){
+          return '#5470C6';
+        }
+      },//柱状图颜色
+      height:{
+        type:Number,
+        default:380
+      },
+      unit:{
+        type:String,
+        default:'%'
+      },//单位  默认%
+
+      showNuitY:{
+        type:Boolean,
+        default:true
+      },//y轴单位是否显示
+
+      showSplitArea:{
+        type:Boolean,
+        default:false
+      },//y轴单位是否显示
+
+      showTooltip:{
+        type:Boolean,
+        default:true
+      },//是否显示浮窗
+
+      tooltipData:{
+        type:Array,
+        default:()=>[],
+      },//悬浮弹窗的数据
+
+      tooltipFormater:{
+        type:Function,
+      },
+      
+      typeName:{
+        type:String,
+        default:''
+      },//标题
+
+      average:{
+        type:[String, Number,Array],
+        default:'0'
+      },//平均分辅助线
+      markLineData:{
+        type:Array,
+        default:()=>[]
+      },//平均分辅助线
+      isShowMarkLine:{
+        type:Boolean,
+        default:true
+      },//是否显示辅助线
+
+      showMarkPoint:{
+        type:Boolean,
+        default:false
+      },//是否显示气泡  默认不显示气泡
+
+      isClick:{
+        type:Boolean,
+        default:false
+      },//是否需要可以点击柱子事件
+      answerValue:{
+        type:String,
+        default:''
+      },//正确答案  与其他柱子显示颜色不同
+      unitX:{
+        type:String,
+        default:''
+      },//x轴单位
+      fullMark:{
+        type:[String,Number],
+        default:''
+      },//满分
+      gridLeft:{
+        type:Number,
+        default:40
+      },
+      gridRight:{
+        type:Number,
+        default:60
+      },
+      gridTop:{
+        type:Number,
+        default:20
+      },
+      fontSize:{//字体大小
+        type:[Number,String],
+        default:''
+      },
+      fontColor:{
+        type:String,
+        default:''
+      },
+      showDataZoom:{//是否显示滚动条
+        type:Boolean,
+        default:true
+      },
+      barMaxWidth:{
+        type:Number,
+        default:50
+      },
+      barMinWidth:{
+        type:Number,
+        default:20
+      }
+    },
+    watch:{
+        datay:{
+            handler:'LoadEchart',
+            deep:true,
+        },//数值变化时重新加载图表
+        markLineData:{
+            handler:'LoadEchart',
+            deep:true,
+        },//数值变化时重新加载图表
+    },
+    computed:{
+        getCompareAnalysis(){
+            return window.$global.getCompareAnalysis()
+        },
+    },
+    created(){
+        window.addEventListener('resize', this.handleResize);//创建时增加监听窗口变化事件
+    },
+    beforeDestroy() {
+        // 组件销毁移除监听 防止内存泄漏
+        window.removeEventListener('resize', this.handleResize);
+    },
+    mounted(){
+      this.LoadEchart();//初始化图表
+    },
+    methods:{
+        //加载echarts
+        LoadEchart() 
+        {
+            
+            if(this.echart)
+            {
+                this.echart.dispose(); // 销毁之前的实例
+            }
+            // 定义每个柱子的颜色
+
+            const colors = [];
+            const xColors=[];
+            // console.log("打印x轴数据",this.datax);
+            const rightAnswerValue = this.answerValue || '';//正确答案
+            for (let i = 0; i < this.datax.length; i++) {
+                if(this.color){
+                    colors.push(this.color);
+                }else{
+                    const colorArr= window.$global.getScorePerformanceAnalysis()
+                    colors.push(colorArr[i]);
+                }
+                xColors.push('#666666');
+            }
+
+            // 初始化 xAxisLabels 数组
+            // this.xAxisLabels = this.datax.map((label, index) => ({
+            //     value: label,
+            //     textStyle: {
+            //         color: xColors[index],
+            //         fontSize: 14
+            //     }
+            // }));
+            const markPointColor=this.color;
+            //devicePixelRatio: 2  2表示每个逻辑像素对应 2个物理像素。这会进一步提高图表的清晰度,适用于更高分辨率的屏幕  解决高分辨率显示器文字模糊的问题
+            this.echart=echarts.init(this.$refs.bar_echart,{devicePixelRatio: 2});//获取echarts容器  如果组件同一个页面多处使用 这里不能用id获取实例
+            // 图表宽度
+            let totalWidth = this.$refs.bar_echart.clientWidth;
+            // console.log(totalWidth, '>>>>>>>>>>>totalWidth')
+            //计算每个柱的宽度 (图表宽度-左右边距)/ x轴数据长度
+            let singleSeriesWidth = Math.ceil((totalWidth - 140) / this.datax.length);
+            // console.log(singleSeriesWidth, '>>>>>>>>>>>singleSeriesWidth')
+
+            const unit = this.unit;
+            // console.log("打印datax", this.datax);
+            // console.log("打印datay", this.datay);
+
+            //计算最大值和最小值
+            const datay = this.datay.filter(num => !isNaN(num))
+            const maxValue=Math.max(...datay);
+            const minValue=Math.min(...datay);
+            let average = 0, markLineData = [],maxAverage = [];
+            if(Object.prototype.toString.call(this.average) === '[object Array]'){
+                average = this.average;
+                this.average.forEach(item=>{
+                    const itemAverage = parseFloat(item) || 0
+                    if(item > 0) {
+                        markLineData.push({symbol:'circle',type: 'value', name: '', yAxis: itemAverage})
+                    }
+                })
+                maxAverage = average.length ? Math.max(...average) : 0;
+            }else if(this.markLineData && this.markLineData.length){
+                average = [];
+                this.markLineData.forEach((item,index)=>{
+                    const itemAverage = parseFloat(item.value) || 0
+                    average.push(itemAverage)
+                    if(item.isShow){
+                        markLineData.push({symbol:'circle',type: 'value', name: '', yAxis: itemAverage,lineStyle: {color: this.getCompareAnalysis[index]},label:{color:this.getCompareAnalysis[index]}})
+                    }
+                })
+                maxAverage = average.length ? Math.max(...average) : 0;
+            }else{
+                average=parseFloat(this.average) || 0;
+                maxAverage = parseFloat(this.average) || 0;
+                markLineData = average!=0?[{symbol:'circle',type: 'value', name: '', yAxis: average}]:[];
+            }
+            
+            // console.log("打印最大值", maxValue);
+            // console.log("打印最小值", minValue);
+            // console.log("打印平均分", average);
+            const nearestValue = this.GetMaxValue(maxValue);//向上取整为最接近10的数字
+            // console.log("打印最大值的",nearestValue)
+            let yAxisUnit=this.showNuitY && this.unit == '%' ? this.unit : '';
+            let splitArea=this.showSplitArea?{ "show": true, areaStyle: {"color": ["#fafafa", "#ffffff"]}}:{}
+            // 交替颜色
+            const typeName=this.typeName || "";
+            const fullMark = this.fullMark;
+            // console.log("打印正确答案的",rightAnswerValue);
+            // console.log("打印满分",fullMark);
+
+            const barMinWidth = 30; // 最小柱子宽度 加上两边间距的宽度
+            const dataZoomNum = Math.floor((totalWidth - 140) / (barMinWidth * 1));// 当前区域内可显示的柱子的数量
+            const dataZoomEnd = Math.floor((100 / this.datax.length) * dataZoomNum); // 当前区域内可显示柱子的占比
+            let dataZoom = {
+                start: 0, //默认为0
+                end: dataZoomEnd, //  默认为100(重点)
+                type: 'slider',
+                show: true,
+                // borderColor: 'red', // 滑动条边框颜色
+                borderColor: 'transparent',// 滑动条背景颜色
+                borderCap: 'round',
+                xAxisIndex: [0],
+                height: 8, //组件高度
+                left: 20, //左边的距离
+                right: 20, //右边的距离
+                bottom: 0, //右边的距离
+                fillerColor: 'transparent',// 滑动条选中区域颜色
+                zoomLock: true,//是否锁定选择区域(或叫做数据窗口)的大小。
+                handleSize: '0',
+                handleStyle: {
+                    color: '#b8b8b8',// 滑动条手柄颜色
+                    borderWidth: 2
+                },
+                backgroundColor: 'transparent', //两边未选中的滑动条区域的颜色
+                showDataShadow: false, //是否显示数据阴影 默认auto
+                showDetail: false, //即拖拽时候是否显示详细数值信息 默认true
+                filterMode: 'filter'
+
+            };//设置滚动条的样式
+          //网格线设置隔行背景色
+            let option= {
+                tooltip: {
+                    // trigger: 'axis',
+                    // axisPointer: {
+                    //     type: 'cross',
+                    //     crossStyle: {
+                    //         color: '#999'
+                    //     }
+                    // },
+                    show: this.showTooltip,
+                    trigger: this.showTooltip?'item':'axis',
+                    triggerOn: "mousemove",
+                    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>` ;
+                    },
+
+                    /*
+                    formatter: (params)=>{
+                        let tooltip=`<div class='tooltip_content'>`;
+                        let title=params?.name || params[0]?.name;
+                        let value=params?.value || params[0]?.value;//得分率
+                        let unit=this.unit;
+                        tooltip += `<div class='tooltip_title'>${title}</div>`;
+                        tooltip += `<div class='tooltip_student'>${typeName}:${value}${unit}</div>`;
+                        // console.log("打印this.tooltipData",this.tooltipData);
+                        if(this.tooltipData.length>0)
+                        {
+                            let list=this.tooltipData[params[0].dataIndex].list;
+                      
+                            for(let i=0;i<list.length;i++)
+                            {
+                                let item = list[i];
+                                tooltip += `<div class='tooltip_student'>${item.name}:${item.value}</div>`;
+                            }
+                        }
+                        tooltip+=`</div>`;
+                        return tooltip;
+                    }
+                    */
+                },
+                // graphic:{
+                //     id: 'highlight-box',
+                //     type: 'rect',
+                //     shape: {
+                //         x: 2, // 根据需要调整X坐标
+                //         y: 60,
+                //         width: 100, // 根据需要调整宽度
+                //         height: containerHeight,  // 高度为图表的高度
+                //     },
+                //     style: {
+                //         fill:'red' // 设置背景色
+                //     },
+                // },
+               
+                grid: {
+                    left: this.gridLeft,
+                    right: this.gridRight,
+                    top: this.gridTop,
+                    bottom: 0,
+                    containLabel: true, //用于控制网格区域是否包含坐标轴的标签  true 可以确保所有标签都能完整显示,不会被裁剪。
+                },
+                dataZoom: this.showDataZoom ?  (singleSeriesWidth < barMinWidth ?  dataZoom : null) :  null,
+                xAxis: [
+                    {
+                        type: 'category',
+                        data: this.datax,
+
+                        axisPointer: {
+                            show:true,
+                            label:{
+                                show:false
+                            },
+                            type: 'shadow',
+                            shadowStyle:{
+                                color:'rgba(46, 100, 250,0.05)'
+                            },
+                        },
+                        axisLabel: {
+                            interval: 0, // 确保所有刻度标签都显示
+                            fontWeight: 400,
+                            rotate: singleSeriesWidth < 80 ? 45 : 0,
+                            formatter:function(value) {
+                                // console.log(value)
+                                
+                                // 文字大小设置为14px  按14算
+                                const valueWidth = value.length * 14;
+                                // console.log("文字宽度",valueWidth);
+                                // console.log("单系列宽度",singleSeriesWidth);
+                                if(valueWidth > singleSeriesWidth) {
+                                    if (singleSeriesWidth < 100) {
+                                        if(valueWidth > 80) {
+                                            // let maxLength = Math.floor(80 / 14) - 1;
+                                            return value.slice(0, 2) + '...' + value.slice(-3)
+                                        } else {
+                                            return value;
+                                        }
+                                    } else {
+                                        let maxLength = Math.floor(singleSeriesWidth / 14) - 1;
+                                        return value.slice(0, maxLength) + '...';
+                                    }
+                                } else {
+                                    return value;
+                                }
+                            },
+                            color: this.fontColor || '#666666',//y轴文字颜色
+                            // width: 80, // 控制每个标签最大宽度,超出则截断
+                            // overflow: 'truncate', // 超出宽度显示省略号
+                            // ellipsis: '...', // 自定义省略符号
+                            // margin: 20,
+                            fontSize: this.fontSize?this.fontSize:14,
+                            // rich: {
+                            //     // 定义省略样式(可选)
+                            //     text: {
+                            //         whiteSpace: 'nowrap',
+                            //         overflow: 'hidden',
+                            //         textOverflow: 'ellipsis',
+                            //         display: 'inline-block',
+                            //         width: 80, // 与上面 width 一致
+                            //         fontSize:14,
+                            //     },
+                            // },
+                            // formatter: '{text|{value}}' + this.unitX, // 使用 rich 样式渲染
+                        }
+                       
+                    }
+                ],
+                yAxis:{
+                    type: 'value',
+                    // name: '进度(%)',
+                    axisLabel: {
+                        color: this.fontColor || '#666666',//y轴文字颜色
+                        fontSize: this.fontSize?this.fontSize:14,
+                        formatter: '{value}'+yAxisUnit, // 添加 % 符号
+                    },
+                    max: maxAverage > nearestValue ? maxAverage : nearestValue,//固定y轴最高值为最大值的向上取整
+                    // splitNumber: 10, // 设置刻度线分段数
+                    // interval: 0, // 确保所有刻度标签都显示
+                    // splitLine: {
+                    //     show: true,
+                    //         lineStyle: {
+                    //         color: "#E4E7ED",
+                    //         type: "solid",
+                    //     },
+                    //     interval: (index, value) => {
+                    //         // 隐藏最大的刻度线对应的网格线
+                    //         return value !== nearestValue;
+                    //     }
+                    // }, // 横向刻度线样式设置
+                    splitArea: {
+                        show: true,
+                        areaStyle: {
+                            color: ["#fafafa", "#ffffff"], // 交替颜色
+                        },
+                    }, //网格线设置隔行背景色
+                },
+                series: [
+                    {
+                        name: '阅卷进度',
+                        type: 'bar',
+                        // showBackground: true,
+                        
+                        backgroundStyle: {
+
+                            color: 'red'
+                        },//柱子的背景色
+                        // barMinHeight:this.isClick?30:0,//柱子最小高度
+                        barMaxWidth: this.barMaxWidth, // 设置柱状图的最大宽度
+                        barMinWidth: this.barMinWidth,
+                        // barWidth: '44px', // 设置柱状图的宽度,可以是百分比或具体数值
+                        itemStyle: {
+                            color: function(params) {
+                                // console.log(params)
+                                if(params.name == rightAnswerValue || params.name == fullMark){//正确答案 或主观题满分
+                                    return '#3BA272';
+                                }if(params.name == '0'){//主观题0分
+                                    return '#EE6666';//0分显示红色
+                                }else{
+                                    // 使用颜色数组中的颜色
+                                    return colors[params.dataIndex];
+                                }
+                            },
+                            // color: (params) => {
+                            //     const value = params.value;
+                            //     if (value === maxValue) {
+                            //         return 'red'; // 最大值颜色
+                            //     } else if (value === minValue) {
+                            //         return 'green'; // 最小值颜色
+                            //     } else {
+                            //         return 'red'; // 默认颜色
+                            //     }
+                            // },
+                            
+
+                        },
+                        // tooltip: {
+                        //     valueFormatter: function (value) {
+                        //         // console.log(value);
+                        //         return value +  unit;
+                        //     }
+                        // },
+                        // label: { // 显示柱子内部的数值
+                        //     show: true,
+                        //     position: 'top', // 柱子内部显示
+                            
+                        //     formatter: '{c}'+this.unit, // 显示的格式
+                        //     color:'#666666',
+                            
+                        //     fontSize: 14
+                        // },
+                        // emphasis: {
+                        //     itemStyle: {
+                        //         color: '#2E64FA' // 设置选中时的颜色
+                        //     }
+                        // },
+                        data: this.datay.map(value => ({
+                            value: value,
+                            label: {
+                                // show: this.showMarkPoint ? (value === 0 || value === maxValue || value === minValue) : singleSeriesWidth > 26,//数值为0不显示
+                                show: true,//数值为0不显示
+                                position: "top",
+                                fontSize: this.fontSize?this.fontSize:14,
+                                formatter: '{c}'+this.unit, // 显示的格式
+                                color:"#666"
+                                // color: (value == maxValue)? "#3BA272": (value == minValue)? "#EE6666": "#666"
+                            },
+                        })),
+                        markLine: {
+                            // symbol:average==0?['none', 'none']:['circle','arrow'],//设置标记线样式  圆形'circle', 矩形'rect', 圆角矩形'roundRect', 三角形'triangle', 菱形'diamond', 图钉行'pin', 箭头行'arrow', 'none'
+                            symbolSize:[8,8],//设置标记线样式大小 宽高
+                            symbolOffset: [
+                                [0, 0],    // 起始标记偏移
+                                [0, 0]  // 结束标记偏移
+                            ],//偏移量设置
+                            label:{
+                                // formatter: '{b}: {c}',
+                                color:'#F56C6C',
+                                fontSize: this.fontSize?this.fontSize:15,
+                                formatter: '{c}'+this.unit,
+                                position: 'end',//'start', 'middle', 'end', 'insideStartTop', 'insideStartBottom', 'insideMiddleTop', 'insideMiddleBottom', 'insideEndTop', 'insideEndBottom'
+                                // distance:[-10,-40],//标签与线之间的间距。如果是数组,第一项为横向间距,第二项为纵向间距。如果是数字,则表示横向纵向使用相同的间距
+                            },
+                            tooltip: {
+                               formatter:{},
+                            },
+                            data: markLineData,
+                            lineStyle: this?.markLineData?.length?null:{
+                                color: '#F56C6C'
+                            },//辅助线的颜色
+                        
+                        },//辅助线
+                        // markPoint: this.showMarkPoint?{
+                        //     symbolSize:55,//气泡大小
+                        //     data: [
+                        //         {
+                        //             type: "max",
+                        //             name: "最大值",
+                        //             label: { color: "#fff" },
+                        //         },
+                        //         {
+                        //             type: "min",
+                        //             name: "最小值",
+                        //             label: { color: "#fff" },
+                        //         },
+                        //     ],
+                        //     label: {
+                        //         show: true,
+                        //         fontSize:8,
+                        //         formatter: '{c}'+this.unit, // 显示的格式
+                        //     },
+                        //     itemStyle: {
+                        //         color: markPointColor,
+                        //     }
+                        // }:undefined,//是否显示气泡  
+                        markPoint: this.showMarkPoint ? {
+                            data: this.datay.map((value, index) => {
+                                // console.log("index",index);
+                                if (value == maxValue || value == minValue) {
+                                    return {
+                                        name: value == maxValue ? '最大值' : '最小值',
+                                        coord: [this.datax[index], value],
+                                        symbolSize: 65,
+                                        label: {
+                                            show: true,
+                                            position: 'inside',
+                                            color: '#fff',
+                                            formatter: value + this.unit
+                                        },
+                                        itemStyle: {
+                                            color: value === maxValue ?markPointColor : markPointColor
+                                        }
+                                    };
+                                }
+                                return null;
+                            }).filter(item => item !== null), // 过滤掉非 max/min 的 null 值
+                            label: {
+                                show: true,
+                                fontSize: 10,
+                                fontWeight: 'bold',
+                                // formatter: '{c}' + this.unit
+                            }
+                        } : undefined    
+                    },
+                ]
+            };
+            // 设置 rich 样式
+            // this.xAxisLabels.forEach((label, index) => {
+            //     option.xAxis[0].axisLabel.rich[`a${index}`] = label.textStyle;
+            // });
+            this.echart.setOption(option);
+
+            //是否可以点击
+            if(this.isClick)
+            {
+               
+                const xAxisDataLength = this.datax.length;//柱子的数量
+                // 获取grid区域偏移量
+                const gridRect = this.echart.getModel().getComponent('grid').coordinateSystem.getRect();
+                let singleLabelWidth = gridRect.width / xAxisDataLength;//单个x轴系列的宽度
+                if(singleSeriesWidth < barMinWidth)
+                {
+                    singleLabelWidth=barMinWidth;
+                }
+                this.echart.on('click',(params)=>{
+                    // console.log("打印params",params);
+                    if (params.seriesType === 'bar') 
+                    {
+                        
+                        // 更新 xAxisLabels 数组
+                        // this.xAxisLabels = this.datax.map((label, index) => ({
+                        //     value: label,
+                        //     textStyle: {
+                        //         color: index === params.dataIndex ? '#2E64FA' : xColors[index],
+                        //         fontSize: 14
+                        //     }
+                        // }));
+                        // 设置 rich 样式
+                        // this.xAxisLabels.forEach((label, index) => {
+                        //     this.echart.setOption({
+                        //         xAxis: [
+                        //             {
+                        //                 axisLabel: {
+                        //                     rich: {
+                        //                         [`a${index}`]: label.textStyle
+                        //                     }
+                        //                 }
+                        //             }
+                        //         ]
+                        //     });
+                        // });
+                        this.echart.setOption({
+                            series: [
+                                {
+                                    itemStyle: {
+                                        color: function(params) {
+                                            if(params.name == rightAnswerValue || params.name == fullMark){//正确答案
+                                                return '#3BA272';
+                                            }
+                                            else if(params.name == '0'){//主观题0分
+                                                return '#EE6666';//0分显示红色
+                                            }
+                                            else{
+                                                return colors[params.dataIndex];
+                                            }
+                                        }
+                                    }
+                                }
+                            ]
+                        });
+                        
+                        let pixelPosition= this.echart.convertToPixel({ xAxisIndex: 0 }, params.name);
+                        // console.log("pixelPosition",pixelPosition);
+                        this.echart.setOption({
+                            graphic:{
+                                id: 'highlight-box',
+                                type: 'rect',
+                                shape: {
+                                    x: pixelPosition - singleLabelWidth/2, // 根据需要调整X坐标
+                                    y: gridRect.y,//
+                                    width: singleLabelWidth, // 根据需要调整宽度
+                                    height: gridRect.height // 
+                                },
+                                style: {
+                                    fill:params.color+'30' // 设置背景色 柱子颜色的30%
+                                },
+                            },
+                        });
+                        // console.log(params);
+                        let index=params.dataIndex;
+                        let xName=params.name;
+                        this.$emit('HandleChartClick',index,xName);
+                        // 处理点击事件的逻辑
+                        // this.HandleBarClick(paintingId);
+                    }
+                        
+                })
+                let defaultPixelPosition= this.echart.convertToPixel({ xAxisIndex: 0 }, this.datax[0]);
+                this.echart.setOption({
+                    graphic: {
+                        id: "highlight-box",
+                        type: "rect",
+                        shape: {
+                            x: defaultPixelPosition - singleLabelWidth/2, // 根据需要调整X坐标 40 是 left 的偏移量
+                            y: gridRect.y, //20是top的偏移量
+                            width:singleLabelWidth, // 根据需要调整宽度 80是x上面减去的2倍
+                            height: gridRect.height, // 40 是 top 和 bottom 的偏移量
+                        },
+                        style: {
+                            fill: "rgba(84,112,198,0.1)", // 设置背景色
+                        },
+                    },
+                });
+            }
+
+            this.echart.on('dataZoom',(event)=>{
+                console.log('触发渲染了---')
+                 this.echart.setOption(
+                    {
+                        ...option,
+                        dataZoom: this.showDataZoom ? 
+                            (singleSeriesWidth < barMinWidth ? 
+                                {
+                                    ...dataZoom,
+                                    start: event?.batch ? event?.batch[0].start : event?.start,
+                                    end:  event?.batch ? event?.batch[0].end : event?.end,
+
+                                } : 
+                                null) : 
+                            null,
+                    }
+                );
+            })
+        },
+
+        //获取最大值  计算规则  
+        GetMaxValue(value)
+        {
+            const thresholdMap =[
+                {
+                    maxThreshold:0.3,//最大阈值0-0.6
+                    bound:0.3,//边界值
+                },
+                {
+                    maxThreshold:0.6,//最大阈值0-0.6
+                    bound:0.6,//边界值
+                },
+                {
+                    maxThreshold:1,//最大阈值 0.6 - 1
+                    bound:1,//边界值
+                },
+                {
+                    maxThreshold:1.5,//最大阈值 0.6 - 1
+                    bound:1.5,//边界值
+                },
+                {
+                    maxThreshold:2,//最大阈值 
+                    bound:2,//边界值
+                },
+                {
+                    maxThreshold:4,//最大阈值
+                    bound:4,//边界值
+                },
+                {
+                    maxThreshold:8,//最大阈值
+                    bound:8,//边界值
+                },
+                {
+                    maxThreshold:10,//最大阈值
+                    bound:10,//边界值
+                },
+                {
+                    maxThreshold:40,//最大阈值
+                    bound:40,//边界值
+                },
+                {
+                    maxThreshold:50,//最大阈值
+                    bound:50,//边界值
+                },
+                {
+                    maxThreshold:60,//最大阈值
+                    bound:60,//边界值
+                },
+                {
+                    maxThreshold:70,//最大阈值
+                    bound:70,//边界值
+                },
+                {
+                    maxThreshold:80,//最大阈值
+                    bound:80,//边界值
+                },
+                {
+                    maxThreshold:90,//最大阈值
+                    bound:90,//边界值
+                },
+                {
+                    maxThreshold:100,//最大阈值
+                    bound:100,//边界值
+                },
+                {
+                    maxThreshold:120,//最大阈值
+                    bound:120,//边界值
+                },
+                {
+                    maxThreshold:150,//最大阈值
+                    bound:150,//边界值
+                },
+                {
+                    maxThreshold:180,//最大阈值
+                    bound:180,//边界值
+                },
+                {
+                    maxThreshold:200,//最大阈值
+                    bound:200,//边界值
+                },
+                {
+                    maxThreshold:250,//最大阈值
+                    bound:250,//边界值
+                },
+                {
+                    maxThreshold:200,//最大阈值
+                    bound:200,//边界值
+                },
+                {
+                    maxThreshold:300,//最大阈值
+                    bound:300,//边界值
+                },
+                {
+                    maxThreshold:350,//最大阈值
+                    bound:350,//边界值
+                },
+                {
+                    maxThreshold:400,//最大阈值
+                    bound:400,//边界值
+                },
+                {
+                    maxThreshold:450,//最大阈值
+                    bound:450,//边界值
+                },
+                {
+                    maxThreshold:500,//最大阈值
+                    bound:500,//边界值
+                },
+                {
+                    maxThreshold:600,//最大阈值
+                    bound:600,//边界值
+                },
+                {
+                    maxThreshold:700,//最大阈值
+                    bound:700,//边界值
+                },
+            ];//配置区间规则
+            //查找匹配的边界值
+            const matchedRule = thresholdMap.find(({ maxThreshold }) => value == maxThreshold || value < maxThreshold);
+
+            // 处理超过阈值的情况(≥5)
+            const bound = matchedRule 
+            ? matchedRule.bound 
+            : Math.ceil((value) / 10) * 10;
+            return  bound;
+
+        },
+        // 处理点击事件
+        HandleBarClick(paintingId)
+        {
+            // console.log("点击了柱状图",paintingId);
+            this.$emit('HandleBarClick',paintingId);
+        },
+
+
+
+        //更新颜色
+        UpdateColors()
+        {
+            const newColors = ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40', '#FF6384'];
+             // 更新 series 中的 itemStyle.color
+            this.echart.setOption({
+                series: [
+                    {
+                        itemStyle: {
+                            color: function(params) {
+                                return newColors[params.dataIndex];
+                            }
+                        }
+                    }
+                ]
+            });
+        },
+
+        // 监听窗口大小变化,重新计算表格高度 使用节流防止频繁改变窗口大小导致计算量过大而页面卡顿
+        handleResize: _.throttle(function() {
+            
+            this.$nextTick(() => {
+               
+                this.LoadEchart();
+                
+            });
+            
+        }, 500), // 节流 500 毫秒内最多执行一次
+    }
+  }
+  </script>
+<style lang="scss" scoped>
+.chart_width {
+  width: 100%;
+  height: 380px; // 设置图表高度
+}
+</style>

+ 148 - 0
src/baseComponents/TabSearch.vue

@@ -0,0 +1,148 @@
+<template>
+    <div :class="{'box_tab':props.tabType == 'box','line_tab':props.tabType == 'line'}">
+        <span 
+            v-for="item in props.tabList" 
+            :key="item.key"  
+            :class="{ 
+                      'selected': activedId == item.key, 
+                      'un_selected': activedId !== item.key,
+                      'prefix':item.prefix,
+
+                    }"
+             @click="HandleTabChange(item.key)"
+        >
+            <img v-if="item.prefix" :src="activedId==item.key ? item.prefixLight : item.prefix" alt="" class="prefix" />
+            {{ item.label }}
+            <span  v-if="item.suffix || item.suffix === 0" class="suffix">{{item.suffix}}</span>
+        </span>
+      </div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch} from 'vue';
+
+interface TabItem {
+    label:String,
+    key:String,
+    prefix?:String,
+    prefixLight?:String,
+    suffix?:String,
+}
+
+type TabType = 'box'|'line'
+const props = defineProps({
+    tabList:{
+        required:true,
+        type:Array<TabItem>
+    },
+    tabType:{
+        required:true,
+        type:String
+    },
+    defaultTabKey:{
+        required:true,
+        type:String
+    }
+})
+const emit = defineEmits(['tabChange'])
+const activedId = ref(props.defaultTabKey)
+
+watch([()=>props.defaultTabKey,()=>props.tabList],([newVal,newValList])=>{
+    activedId.value = newVal
+},{immediate:true,deep:true})
+
+const HandleTabChange = async (params) =>{
+    activedId.value = params
+    emit('tabChange',activedId.value)
+}
+</script>
+
+
+<style lang="scss" scoped>
+ 
+.box_tab  {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    font-size: 14px;
+
+    > span{
+      height: 36px;
+      display: flex;
+      gap:4px;
+      justify-content: center;
+      align-items: center;
+
+      line-height: 1.5 !important;
+      padding:7px 12px;
+      &:hover{
+        cursor: pointer;
+      }
+
+      .prefix{
+        width: 16px;
+        height: 16px;
+      }
+    }
+
+    .un_selected{
+      border: 1px solid  #dcdfe6;
+      color: #666;
+    }
+    
+    .selected{
+      background:  #2E64FA;
+      color: #fff;
+      + span{
+        border-left: transparent;
+      }
+
+      &.prefix{
+        background-color: rgba(46, 100, 250,0.1);
+        color:rgba(46, 100, 250);
+        border:solid #2E64FA 1px;
+      }
+    }
+
+    > span:nth-child(1){
+      border-radius: 4px 0 0 4px;
+    }
+    > span:last-child{
+      border-radius:0 4px 4px 0;
+    }
+    >span:nth-child(n):not(:last-child):not(.selected){
+        border-right: transparent;
+    }
+}
+
+
+.line_tab{
+    display: flex;
+    gap:24px;
+    padding-bottom: 10px;
+    color:#666666;
+    font-size:14px;
+
+    >span{
+        &:hover{
+            cursor: pointer;
+        }
+    }
+
+    .selected{
+        position: relative;
+        color: #2E64FA;
+        font-weight: bold;
+        &::after{
+            position: absolute;
+            left: 0;
+            bottom: -10px;
+            content:'';
+            width: 100%;
+            height: 2px;
+            background-color: #2E64FA;
+        }
+    }
+}
+
+</style>

+ 175 - 0
src/baseComponents/Table.vue

@@ -0,0 +1,175 @@
+<template>
+    <div class="page_table">
+        <!-- :height="tableHeight" -->
+        <el-table 
+          :show-header="showHeader"
+          :data="props.tableData" stripe v-loading="props.loading" element-loading-text="拼命加载中……" 
+          element-loading-spinner="el-icon-loading" element-loading-background="#ffffff"
+          element-loading-custom-class="loading_icon"
+          :row-class-name="props.tableRowClassName"
+          @row-click="(row,column)=> emit('rowClick',row)"
+          :span-method="props.spanMethod">
+            <!-- show-overflow-tooltip -->
+            <template 
+                v-for="(item,index) in props.tableColumns" 
+                :key="item.name"
+            >
+                <el-table-column 
+                  v-if="!item.hidden"
+                  :prop="item.name" 
+                  :min-width="item.width" 
+                  :label="item.label" 
+                  :fixed="item.fixed ? item.fixed : false" 
+                  :align="item.align" >
+
+                  <template v-if="item.customHeader" #header>
+                    <slot :name="'header-'+item.name"></slot>
+                  </template>
+                
+                  <template v-if="item.custom" #default="row">
+                    <slot :name="item.name" :row="row.row" :currentIndex="(row.$index + 1)">
+                    </slot>
+                  </template>
+              </el-table-column>
+
+            </template>
+            
+        </el-table>
+
+        <div class="page_pagination" v-if="!(hidePagination)">
+          <!-- background layout="sizes, prev, pager, next"  -->
+          <el-pagination 
+            :hide-on-single-page="props.hidesinglePage"
+            :current-page="props.currentPage" 
+            :page-size="props.pageSize" 
+            :page-sizes="[20, 50, 100]" 
+            background layout="prev, pager, next" 
+            :total="Number(props.totalNum)" 
+            @current-change="(val)=>emit('paginationChange',{type:'currentPage',val})" 
+            @size-change="(val)=>emit('paginationChange',{type:'pageSize',val})" />
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+interface ColumnProperty  {
+  name:String,
+  label:String,
+  width?:String,
+  fixed?:String,
+  align?:String,
+  minWidth?:String,
+  custom:String,
+  customHeader?:Boolean,
+  hidden:Boolean,
+}
+const props = defineProps({
+  tableData:{
+    required:true,
+    type:Array
+  },
+  tableColumns:{
+    required:true,
+    type:Array<ColumnProperty>
+  },
+  currentPage:{
+    type:Number,
+    default:1
+  },
+  pageSize:{
+    type:Number,
+    default:20
+  },
+  totalNum:{
+    type:Number,
+    default:0
+  },
+  loading:{
+    type:Boolean,
+    default:false
+  },
+  hidesinglePage:{
+    type:Boolean,
+  },
+  hidePagination:{
+    type:Boolean,
+    default:false
+  },
+  spanMethod:{
+    type:Function,
+    required:false
+  },
+  tableRowClassName:{
+    type:Function,
+    required:false
+  },
+  showHeader:{
+    type:Boolean,
+    default:true
+  }
+})
+const emit = defineEmits(['paginationChange','rowClick'])
+
+/*  
+//demo
+const HandleSpanMethod = ({row,column,rowIndex,columnIndex}) => {
+    if(['type'].includes(column.property)){
+        return {
+        rowspan: row.sameRow,
+        colspan: 1,
+        }
+    }
+}
+const HandleTableRowClassName = ({row,rowIndex}) => {
+  if(row.parentIndex % 2 == 0){
+    // 偶数行
+    return 'even_row'
+  }else{
+    // 奇数行
+    return 'odd_row'
+  }
+}
+*/
+
+</script>
+
+<style lang="scss" scoped>
+.page_table{
+  overflow: auto;
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-start;
+  .el-table{
+    flex-grow: 1;
+    :deep(.el-scrollbar__view){
+      height: 100%;
+    }
+  }
+
+  :deep(.el-table__body){
+      tbody tr.even_row td{
+        background-color: white;
+      }
+      tbody tr.odd_row td{
+        background-color: rgb(250, 250, 250);
+      }
+      // .hover-fixed-row
+      tbody tr.hover-row td{
+        background-color: rgb(245,247,250);
+      }
+      tbody tr td:last-child:not([rowspan="1"]){
+        border-left:solid rgba(235,238,245) 1px;
+      }
+  }
+}
+
+:deep(.loading_icon){
+  .el-loading-spinner{
+    .el-loading-text{
+      color:#2e64fa;
+    }
+  }
+}
+
+
+</style>

+ 1 - 0
src/layout/components/AppMain.vue

@@ -26,6 +26,7 @@ export default {
   height: calc(100vh - 65px);
 }
 .main_container {
+  flex: 1 1;
   padding: 20px;
   background: #F0F4FB;
   height: calc(100vh - 65px);

+ 5 - 0
src/main.ts

@@ -10,7 +10,12 @@ import router from './router' // 引入路由配置
 import { setupStore } from './store' // 引入 store 初始化方法
 
 import './styles/common.scss'
+import './styles/page.scss'
+import global from "@/utils/global.js";
+
+window.$global = global   //定义全局变量
 const app = createApp(App)
+
 Object.keys(ElIcons).forEach((key) => {
   app.component(key, (ElIcons as Record<string, any>)[key])
 })//ele icon

+ 22 - 2
src/router/index.ts

@@ -33,9 +33,29 @@ const routes: Array<RouteRecordRaw> = [
         path: 'examination',
         name: 'examination',
         component: () => import('@/views/examination/index.vue')
-      }
-    ]
+      },
+      {
+        path: 'userSearch',    //用户查询
+        name: 'userSearch',   
+        component: () => import('@/views/userSearch/index.vue')
+      },
+      {
+        path: 'logMonitor',   //日志监控
+        name: 'logMonitor',
+        component: () => import('@/views/logMonitor/index.vue')
+      },
+      {
+        path: 'logMonitorDetail',   //日志监控详情
+        name: 'logMonitorDetail',
+        component: () => import('@/views/logMonitor/LogMonitorDetail.vue')
+      },
 
+      {
+        path: 'performanceMonitor',    //业务监控
+        name: 'performanceMonitor',
+        component: () => import('@/views/performanceMonitor/index.vue')
+      },
+    ]
   },
   {
     path: '/:pathMatch(.*)*', // 捕获所有未匹配路径

+ 14 - 0
src/styles/common.scss

@@ -67,6 +67,20 @@ body {
   background: none;
 }
 
+
+
+.el-button.button_default_background {
+  background: #fff;
+  border: 1px solid  #DCDFE6;
+  color: #666;
+  &:hover {
+    background: #fff;
+  }
+  &.actiive {
+    border: none;
+  }
+}
+
 .el-button.button_background {
   background: #2E64FA;
   border: 1px solid  #2E64FA;

+ 51 - 0
src/styles/page.scss

@@ -0,0 +1,51 @@
+
+.custom_container{
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-start;
+  > * {
+    flex-shrink: 0;
+  }
+
+  .end_item{
+    flex-grow: 1;
+    display: flex;
+    flex-direction: column;
+    justify-content: flex-start;
+    > div:last-child{
+      flex-grow: 1;
+    }
+  }
+}
+
+
+.two_line_ellipsis{
+    width: 100%;
+
+    // overflow: hidden;
+    // text-overflow: ellipsis;
+    // white-space: nowrap;
+
+    // 多行溢出属性
+    /* 必须结合的属性:将对象作为弹性伸缩盒子模型显示 */
+    display: -webkit-box;
+    /* 必须结合的属性:设置或检索伸缩盒对象的子元素的排列方式 */
+    -webkit-box-orient: vertical;
+    /* 限制在一个块元素显示的文本的行数(这里设置为 2 行) */
+    -webkit-line-clamp: 2;
+    /* 溢出部分隐藏 */
+    overflow: hidden;
+    /*(可选)为了防止单词或长数字在边界处不换行导致直接被截断,可以加上以下属性 */
+    word-break: break-all;
+    &:hover{
+        cursor:pointer
+    }
+}
+
+
+.one_line_ellipsis{
+  overflow: hidden;
+  text-overflow: ellipsis; /* 超出显示省略号 */
+  white-space: nowrap;
+}
+

+ 998 - 0
src/utils/global.js

@@ -0,0 +1,998 @@
+// 全局公共js方法库 2024-12-08
+export default {
+  //获取考试背景对应的颜色
+  getExamBackgroundColor(type) {
+    let result = "#F56C6C";
+
+    const examTypesColor = [
+      {
+        type: 1,
+        title: "期末",
+        color: "#F56C6C",
+        background: "#F56C6C",
+      }, //期末
+      {
+        type: 2,
+        title: "期中",
+        color: "#3ba272",
+        background: "#3ba272",
+      }, //期中
+      {
+        type: 3,
+        title: "模拟",
+        color: "#ea7acb",
+        background: "#ea7acb",
+      }, //模拟
+      {
+        type: 4,
+        title: "月考",
+        color: "#fac858",
+        background: "#fac858",
+      }, //月考
+      {
+        type: 5,
+        title: "周测",
+        color: "#995fb3",
+        background: "#995fb3",
+      }, //周测
+      {
+        type: 6,
+        title: "平时",
+        color: "#72C0DD",
+        background: "#72C0DD",
+      }, //平时
+    ];
+    for (let i = 0; i < examTypesColor.length; i++) {
+      if (examTypesColor[i].type == type || examTypesColor[i].title == type) {
+        result = examTypesColor[i].background;
+        break;
+      }
+    }
+    // console.log("打印颜色值",type,result);
+    return result;
+  },
+
+  //获取五率对应的颜色值
+  getFiveRateColor(type)
+  {
+    let list=[
+      {
+        name: "top",
+        color: "#5470C6",
+      },
+      {
+        name: "excellent",
+        color: "#3BA272",
+      },
+      {
+        name: "good",
+        color: "#FAC858",
+      },
+      {
+        name: "pass",
+        color: "#995FB3",
+      },
+      {
+        name: "low",
+        color: "#EE6666",
+      }
+    ];//五率颜色配置
+    return list.find((item) => item.name === type)?.color || "#5470C6";//默认5470C6颜色
+  },
+  //获取五率对应的颜色值
+  getSchoolFiveRateColor(type)
+  {
+    let list=[
+      {
+        name: "high",
+        color: "#5470C6",
+      },
+      {
+        name: "good",
+        color: "#3BA272",
+      },
+      {
+        name: "fine",
+        color: "#FAC858",
+      },
+      {
+        name: "pass",
+        color: "#995FB3",
+      },
+      {
+        name: "low",
+        color: "#EE6666",
+      }
+    ];//五率颜色配置
+    return list.find((item) => item.name === type)?.color || "#5470C6";//默认5470C6颜色
+  },
+
+  // 获取分析报告成绩分析中默认的20个颜色
+  getScorePerformanceAnalysis() {
+    return [
+      "#5470C6",
+      "#3BA272",
+      "#EE6666",
+      "#FAC858",
+      "#995FB3",
+      "#72C0DD",
+      "#90CB75",
+      "#EA7ACB",
+      "#FC8451",
+      "#65789B",
+      "#178BD3",
+      "#26A616",
+      "#A6129E",
+      "#D3A141",
+      "#234098",
+      "#047640",
+      "#B43535",
+      "#848BDC",
+      "#D6AF83",
+      "#84313D",
+      //20个颜色一组
+      "#5470C6",
+      "#3BA272",
+      "#EE6666",
+      "#FAC858",
+      "#995FB3",
+      "#72C0DD",
+      "#90CB75",
+      "#EA7ACB",
+      "#FC8451",
+      "#65789B",
+      "#178BD3",
+      "#26A616",
+      "#A6129E",
+      "#D3A141",
+      "#234098",
+      "#047640",
+      "#B43535",
+      "#848BDC",
+      "#D6AF83",
+      "#84313D",
+      //
+      "#5470C6",
+      "#3BA272",
+      "#EE6666",
+      "#FAC858",
+      "#995FB3",
+      "#72C0DD",
+      "#90CB75",
+      "#EA7ACB",
+      "#FC8451",
+      "#65789B",
+      "#178BD3",
+      "#26A616",
+      "#A6129E",
+      "#D3A141",
+      "#234098",
+      "#047640",
+      "#B43535",
+      "#848BDC",
+      "#D6AF83",
+      "#84313D",
+    ];
+
+  },
+  // 获取分析报告成绩分析中默认的20个颜色
+  getCompareAnalysis() {
+    return [
+      "#EE6666",
+      "#5470C6",
+      "#3BA272",
+      "#FAC858",
+      "#995FB3",
+      "#72C0DD",
+      "#90CB75",
+      "#EA7ACB",
+      "#FC8451",
+      "#65789B",
+      "#178BD3",
+      "#26A616",
+      "#A6129E",
+      "#D3A141",
+      "#234098",
+      "#047640",
+      "#B43535",
+      "#848BDC",
+      "#D6AF83",
+      "#84313D",
+      //20个颜色一组
+      "#EE6666",
+      "#5470C6",
+      "#3BA272",
+      "#FAC858",
+      "#995FB3",
+      "#72C0DD",
+      "#90CB75",
+      "#EA7ACB",
+      "#FC8451",
+      "#65789B",
+      "#178BD3",
+      "#26A616",
+      "#A6129E",
+      "#D3A141",
+      "#234098",
+      "#047640",
+      "#B43535",
+      "#848BDC",
+      "#D6AF83",
+      "#84313D",
+    ];
+
+  },
+  //获取G组 G10-G1对应的颜色值
+  getGGroupColor() {
+    return [
+      {
+        name: "G1",
+        color: "#5470C6",
+      },
+      {
+        name: "G2",
+        color: "#90CB75",
+      },
+      {
+        name: "G3",
+        color: "#D3A141",
+      },
+      {
+        name: "G4",
+        color: "#EE6666",
+      },
+      {
+        name: "G5",
+        color: "#72C0DD",
+      },
+      {
+        name: "G6",
+        color: "#3BA272",
+      },
+      {
+        name: "G7",
+        color: "#FC8451",
+      },
+      {
+        name: "G8",
+        color: "#995FB3",
+      },
+      {
+        name: "G9",
+        color: "#EA7ACB",
+      },
+      {
+        name: "G10",
+        color: "#FAC858",
+      },
+    ];
+  },
+  //获取G组 g10-g1对应的颜色值
+  getgGroupColor() {
+    return [
+      {
+        name: "g1",
+        color: "#5470C6",
+      },
+      {
+        name: "g2",
+        color: "#90CB75",
+      },
+      {
+        name: "g3",
+        color: "#D3A141",
+      },
+      {
+        name: "g4",
+        color: "#EE6666",
+      },
+      {
+        name: "g5",
+        color: "#72C0DD",
+      },
+      {
+        name: "g6",
+        color: "#3BA272",
+      },
+      {
+        name: "g7",
+        color: "#FC8451",
+      },
+      {
+        name: "g8",
+        color: "#995FB3",
+      },
+      {
+        name: "g9",
+        color: "#EA7ACB",
+      },
+      {
+        name: "g10",
+        color: "#FAC858",
+      },
+    ];
+  },
+  //G组颜色
+  gGroupColor(){
+    return [
+      '#FAC858','#EA7ACB','#995FB3','#FC8451','#3BA272','#72C0DD','#EE6666','#D3A141','#90CB75','#5470C6',
+      '#65789B','#178BD3','#26A616','#A6129E','#234098','#047640','#B43535','#848BDC','#D6AF83','#84313D',
+    ]
+  },
+  // G组柱子颜色
+  gGroupBarColor(){
+    return ['#848BDC','#E999AA','#8CCCB1','#B5DDEE','#F4DBC6','#D2B2E2','#F4DFA0','#848BDC','#E999AA','#8CCCB1','#B5DDEE','#F4DBC6','#D2B2E2','#F4DFA0','#848BDC','#E999AA','#8CCCB1','#B5DDEE','#F4DBC6','#D2B2E2','#F4DFA0']
+  },
+  //知识点分层柱子颜色
+  knowledgeBarColor(){
+    return [
+      '#E999AA','#8CCCB1','#B5DDEE','#F4DBC6','#D2B2E2','#F4DFA0','#848BDC',
+      '#E999AA','#8CCCB1','#B5DDEE','#F4DBC6','#D2B2E2','#F4DFA0','#848BDC',
+      '#E999AA','#8CCCB1','#B5DDEE','#F4DBC6','#D2B2E2','#F4DFA0','#848BDC',
+      '#E999AA','#8CCCB1','#B5DDEE','#F4DBC6','#D2B2E2','#F4DFA0','#848BDC',
+      '#E999AA','#8CCCB1','#B5DDEE','#F4DBC6','#D2B2E2','#F4DFA0','#848BDC',
+    ];
+  },
+  //知识点分层层级颜色
+  knowledgeLayeredColor(){
+    return [
+      '#5470C6','#3BA272','#FAC858','#995FB3','#EE6666','#72C0DD','#90CB75','#EA7ACB','#FC8451','#65789B',
+      '#5470C6','#3BA272','#FAC858','#995FB3','#EE6666','#72C0DD','#90CB75','#EA7ACB','#FC8451','#65789B',
+      '#5470C6','#3BA272','#FAC858','#995FB3','#EE6666','#72C0DD','#90CB75','#EA7ACB','#FC8451','#65789B'
+    ]
+  },
+  //获取百分比  type  progress 表示进度条使用
+  getPercentage(num1, num2, type) {
+    let result = 0;
+    let percentage = 0;
+    if (num1 && num2) {
+      percentage = (parseFloat(num1) / parseFloat(num2)).toFixed(2);
+    }
+
+    result = Math.round(percentage * 100);
+    // console.log("num1,num2",num1,num2,percentage,result);
+    if (type == "progress") {
+      //进度条的值不能超100%
+      if (result > 100) {
+        result = 100;
+      }
+    }
+    if (result < 0) {
+      result = 0;
+    }
+
+    //    console.log("打印结果",result);
+    return result;
+  },
+
+  //获取当前日期 返回的格式为yyyy-MM-dd hh:mm:ss
+  getNowDate(format = "yyyy-mm-dd") {
+    const now = new Date();
+    const year = now.getFullYear();
+    const month = String(now.getMonth() + 1).padStart(2, "0");
+    const date = String(now.getDate()).padStart(2, "0");
+    const hours = String(now.getHours()).padStart(2, "0");
+    const minutes = String(now.getMinutes()).padStart(2, "0");
+    const seconds = String(now.getSeconds()).padStart(2, "0");
+
+    switch (format) {
+      case "yyyy-mm-dd":
+        return `${year}-${month}-${date}`;
+      case "yyyy-mm-dd hh:mm:ss":
+        return `${year}-${month}-${date} ${hours}:${minutes}:${seconds}`;
+      case "yyyy-mm-dd hh:mm":
+        return `${year}-${month}-${date} ${hours}:${minutes}`;
+      // 可以根据需要添加更多格式
+      case "yyyy年mm月dd日 hh:mm:ss":
+        return `${year}年${month}月${date}日 ${hours}:${minutes}:${seconds}`;
+      default:
+        return `${year}-${month}-${date} ${hours}:${minutes}:${seconds}`;
+    }
+  },
+
+  // 时间戳转换为时间字符串
+  getTimestamp(timestamp, format = 'YYYY-MM-DD HH:mm:ss') {
+    // console.log('timestamp', timestamp);
+     if (!timestamp && timestamp !== 0) return '';
+    
+    // 处理不同格式的时间戳
+    let timeValue = timestamp;
+    
+    // 如果是字符串,尝试转换为数字
+    if (typeof timeValue === 'string') {
+      timeValue = Number(timeValue);
+    }
+    
+    // 检查是否为有效数字
+    if (isNaN(timeValue) || timeValue < 0) return '';
+    
+    // 处理秒级时间戳(10位数字)
+    if (timeValue.toString().length === 10) {
+      timeValue = timeValue * 1000;
+    }
+    
+    // 处理毫秒级时间戳(13位数字)
+    const date = new Date(timeValue);
+    
+    // 检查日期是否有效
+    if (isNaN(date.getTime())) return '';
+    
+    const year = date.getFullYear();
+    const month = String(date.getMonth() + 1).padStart(2, '0');
+    const day = String(date.getDate()).padStart(2, '0');
+    const hours = String(date.getHours()).padStart(2, '0');
+    const minutes = String(date.getMinutes()).padStart(2, '0');
+    const seconds = String(date.getSeconds()).padStart(2, '0');
+    
+    return format
+      .replace('YYYY', year)
+      .replace('MM', month)
+      .replace('DD', day)
+      .replace('HH', hours)
+      .replace('mm', minutes)
+      .replace('ss', seconds);
+  },
+
+  // 保留小数点后几位 str  数值   fixed 小数点后保留位数
+  floatNum(str, fixed) {
+    // console.log("打印参数",str,fixed);
+    if (str == null || str == undefined || str == "") {
+      return 0;
+    } else {
+      const num = parseFloat(str);
+      if (isNaN(num)) {
+        throw new Error("Invalid number string");
+      }
+      return parseFloat(num.toFixed(fixed));
+    }
+  },
+
+  // 去掉小数点后面为0的 如果不为0 最多只保留2位小数
+  getFormatNumber(numberStr)
+  {
+    // 尝试将字符串转为数字
+    const num = parseFloat(numberStr);
+      
+    // 检查是否为NaN(非数字情况)
+    if (isNaN(num)) {
+        return numberStr;
+    }
+    
+    // 检查是否为整数(小数点后全为0)
+    if (num % 1 === 0) {
+        return num.toString();
+    }
+    
+    // 保留最多2位小数,并去除末尾的0
+    let formatted = num.toFixed(2);
+    
+    // 如果小数点后都是0,则返回整数形式
+    if (formatted.endsWith('.00')) {
+        return formatted.split('.')[0];
+    }
+    
+    // 去除末尾的0(如1.50 → 1.5)
+    formatted = formatted.replace(/\.?0+$/, '');
+    
+    return formatted;
+  },
+
+  //过滤掉html标签的方法
+  getStripHtmlTags(str) {
+    // 创建一个新的 div 元素
+    const tempDiv = document.createElement("div");
+    // 将 HTML 字符串设置为 div 的 innerHTML
+    tempDiv.innerHTML = str;
+    // 返回 div 的 textContent,这将去除所有 HTML 标签
+    return tempDiv.textContent || tempDiv.innerText;
+  },
+
+  //获取评阅类型 单评还是双评等
+  getRatingType(type) {
+    let result = "单评";
+    if (type == 1) {
+      result = "单评";
+    } else if (type == 2) {
+      result = "2评0补0审";
+    } else if (type == 3) {
+      result = "2评1补0审";
+    } else if (type == 4) {
+      result = "2评0补1审";
+    } else if (type == 5) {
+      result = "2评1补1审";
+    }
+
+    return result;
+  },
+
+  //根据起始题号,结束题号,题号间隔获取题号列表
+  getTopicNumberList(startNumber, endNumber, interval) {
+    const topicNumberList = [];
+    for (let i = startNumber; i <= endNumber; i += interval) {
+      topicNumberList.push(i);
+    }
+    // console.log("打印题号列表",topicNumberList);
+    return topicNumberList;
+  },
+
+  //获取科目图片名称 根据汉字
+  getSubjectsName(value,count,type) 
+  {
+    let name=value;
+    if(count==0)
+    {
+
+    }
+    else
+    {
+      switch (count) {
+        case 1:
+          name="其他";
+          break;
+        case 2:
+          name="二科";
+          break;
+        case 3:
+          name="三科";
+          break;
+        case 4:
+          name="四科";
+          break;
+        case 5:
+          name="五科";
+          break;
+        case 6:
+          name="六科";
+          break;
+        case 7:
+          name="七科";
+          break;
+        case 8:
+          name="八科";
+          break;
+        case 9:
+          name="九科";
+          break;
+      }
+    }
+
+    const subjectList = [
+      {
+        name: "生物",
+        image: "biology.png",
+        color: "#3BA272",
+      },
+      {
+        name: "化学",
+        image: "chemistry.png",
+        color: "#73C0DE",
+      },
+      {
+        name: "语文",
+        image: "chinese.png",
+        color: "#91CC75",
+      },
+      {
+        name: "道德与法治",
+        image: "daofa.png",
+        color: "#FF869D",
+      },
+      {
+        name: "八科",
+        image: "eight_subject.png",
+        color: "#3BA272",
+      },
+      {
+        name: "英语",
+        image: "english.png",
+        color: "#EE6666",
+      },
+      {
+        name: "五科",
+        image: "five_subject.png",
+        color: "#63A1FF",
+      },
+      {
+        name: "四科",
+        image: "four_subject.png",
+        color: "#FF869D",
+      },
+      {
+        name: "地理",
+        image: "geography.png",
+        color: "#9A60B4",
+      },
+      {
+        name: "历史",
+        image: "history.png",
+        color: "#FC8452",
+      },
+      {
+        name: "信息",
+        image: "information.png",
+        color: "#63A1FF",
+      },
+      {
+        name: "数学",
+        image: "math.png",
+        color: "#FAC858",
+      },
+      {
+        name: "九科",
+        image: "nine_subject.png",
+        color: "#EE6666",
+      },
+      {
+        name: "其他",
+        image: "other.png",
+        color: "#C495FA",
+      },
+      {
+        name: "物理",
+        image: "physics.png",
+        color: "#817BE2",
+      },
+      {
+        name: "政治",
+        image: "politics.png",
+        color: "#63A1FF",
+      },
+      {
+        name: "七科",
+        image: "seven_subject.png",
+        color: "#FC8452",
+      },
+      {
+        name: "六科",
+        image: "six_subject.png",
+        color: "#9A60B4",
+      },
+      {
+        name: "体育",
+        image: "sport.png",
+        color: "#FF8B8B",
+      },
+      {
+        name: "三科",
+        image: "three_subject.png",
+        color: "#5CC1F6",
+      },
+      {
+        name: "二科",
+        image: "two_subject.webp",
+        color: "#C495FA",
+      },
+      {
+        name: "总分",
+        image: "totalScore.png", 
+        color: "#325AD4",
+      },
+      {
+        name: "通用",
+        image: "tongyong.png",
+        color: "#5CC1F6",
+      },
+      {
+        name: "地理学考",
+        image: "geography_exam.webp",
+        color: "#9A60B4",
+      },
+      {
+        name: "化学学考",
+        image: "chemistry_exam.webp",
+        color: "#73C0DE",
+      },
+      {
+        name: "历史学考",
+        image: "history_exam.webp",
+        color: "#FC8452",
+      },
+      {
+        name: "生物学考",
+        image: "biology_exam.webp",
+        color: "#3BA272",
+      },
+      {
+        name: "物理学考",
+        image: "physics_exam.webp",
+        color: "#817BE2",
+      },
+      {
+        name: "政治学考",
+        image: "politics_exam.webp",
+        color: "#63A1FF",
+      }
+    ];
+    let result = subjectList.find((item) => item.name === name);
+    if(result)
+    {
+      if(type && type == 'color') {
+        return result.color;
+      }else {
+        return result.image;
+      }
+    }
+    else
+    {
+      if(type && type == 'color') {
+        return '#C495FA';
+      }else {
+        return "other.png"
+      }
+    }
+  },
+  //包含0的正整数 包含0的正整数但不是以0开头的多位正整数  '00'、'01' 和 '0001' 都不会被匹配
+  isNumber(str){
+    const regex = /^(?:[1-9][0-9]*|0)$/;
+    return regex.test(str);
+  },
+  // 大于等于 0 的数字(含整数、小数都行,但不能有负号)
+  isNotNegativeNumber(val){
+    // return /^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(val);
+    // 先去除首尾空格和中间所有空格
+    const trimmedVal = String(val).replace(/\s+/g, '');
+    return /^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(trimmedVal);
+  },
+  //根据当前值 获取最大值  echarts 图表使用
+  getMaxValue(value)
+  {
+    const thresholdMap =[
+        {
+            maxThreshold:0.3,//最大阈值0-0.6
+            bound:0.3,//边界值
+        },
+        {
+            maxThreshold:0.6,//最大阈值0-0.6
+            bound:0.6,//边界值
+        },
+        {
+            maxThreshold:1,//最大阈值 0.6 - 1
+            bound:1,//边界值
+        },
+        {
+            maxThreshold:1.5,//最大阈值 0.6 - 1
+            bound:1.5,//边界值
+        },
+        {
+            maxThreshold:2,//最大阈值 
+            bound:2,//边界值
+        },
+        {
+            maxThreshold:4,//最大阈值
+            bound:4,//边界值
+        },
+        {
+            maxThreshold:8,//最大阈值
+            bound:8,//边界值
+        },
+        {
+            maxThreshold:10,//最大阈值
+            bound:10,//边界值
+        },
+        {
+            maxThreshold:20,//最大阈值
+            bound:20,//边界值
+        },
+        {
+            maxThreshold:30,//最大阈值
+            bound:30,//边界值
+        },
+        {
+            maxThreshold:40,//最大阈值
+            bound:40,//边界值
+        },
+        {
+            maxThreshold:50,//最大阈值
+            bound:50,//边界值
+        },
+        {
+            maxThreshold:60,//最大阈值
+            bound:60,//边界值
+        },
+        {
+            maxThreshold:70,//最大阈值
+            bound:70,//边界值
+        },
+        {
+            maxThreshold:80,//最大阈值
+            bound:80,//边界值
+        },
+        {
+            maxThreshold:90,//最大阈值
+            bound:90,//边界值
+        },
+        {
+            maxThreshold:100,//最大阈值
+            bound:100,//边界值
+        },
+        {
+            maxThreshold:120,//最大阈值
+            bound:120,//边界值
+        },
+        {
+            maxThreshold:150,//最大阈值
+            bound:150,//边界值
+        },
+        {
+            maxThreshold:180,//最大阈值
+            bound:180,//边界值
+        },
+        {
+            maxThreshold:200,//最大阈值
+            bound:200,//边界值
+        },
+        {
+            maxThreshold:250,//最大阈值
+            bound:250,//边界值
+        },
+        {
+            maxThreshold:200,//最大阈值
+            bound:200,//边界值
+        },
+        {
+            maxThreshold:300,//最大阈值
+            bound:300,//边界值
+        },
+        {
+            maxThreshold:350,//最大阈值
+            bound:350,//边界值
+        },
+        {
+            maxThreshold:400,//最大阈值
+            bound:400,//边界值
+        },
+        {
+            maxThreshold:450,//最大阈值
+            bound:450,//边界值
+        },
+        {
+            maxThreshold:500,//最大阈值
+            bound:500,//边界值
+        },
+        {
+            maxThreshold:600,//最大阈值
+            bound:600,//边界值
+        },
+        {
+            maxThreshold:700,//最大阈值
+            bound:700,//边界值
+        },
+    ];//配置区间规则
+    
+    //查找匹配的边界值
+    const matchedRule = thresholdMap.find(({ maxThreshold }) => value < maxThreshold);
+
+    // 处理超过阈值的情况(≥5)
+    const bound = matchedRule 
+    ? matchedRule.bound 
+    : Math.ceil((value) / 10) * 10;
+    // console.log("打印",value);
+    // console.log("打印边界值",bound);
+    return  bound;
+    
+  },
+   //中间截断显示文本,保留前后各n位字符
+  truncateMiddleText(text,num,startStr,endStr) {
+    //num:不超过多少字符,全部展示 startStr:开头保留多少字符 endStr:结尾保留多少字符
+    if (text.length <= num) {
+      return text;
+    }
+    // 保留开头和结尾各5位字符
+    const start = text.substring(0, startStr);
+    const end = text.substring(text.length - endStr);
+    
+    return `${start}...${end}`;
+  },
+
+  //根据题库的试卷题目信息 获取答题卡所有小题的数据
+  getAllQuestionByKnowTopic(list)
+  {
+    console.log("公共方法库处理list:", list);
+    for (let i = 0; i < list.length; i++) 
+    {
+      const item = list[i];
+      if(item.type==1)
+      {
+        //选择题 处理
+        
+      }
+    
+    }
+  },
+
+
+  //手阅卡 根据分值 生成对应的数组列表
+  getNumberListByScore(score)
+  {
+    const TOTAL_SLOTS = 24;//格子数量  固定24个  后期可调整
+      const result = [];
+    
+    // 初始化24个格子
+    for (let i = 0; i < TOTAL_SLOTS; i++) {
+      result.push({ index: i, value: '', type: -1 }); // type为-1表示空
+    }
+    
+    if (score < 10) {
+      // 规则1: 分数小于10,从右开始显示 0.5, 0, 1, 2, ..., 到score
+      const values = ['0.5', '0'];
+      
+      // 添加1到score的所有整数
+      for (let i = 1; i <= score; i++) {
+        values.push(i.toString());
+      }
+      
+      // 从右向左填充(从最后一个格子开始)
+      for (let i = 0; i < values.length; i++) {
+        const slotIndex = TOTAL_SLOTS - 1 - i; // 从索引23开始
+        if (slotIndex >= 0) {
+          result[slotIndex].value = values[i];
+          // 设置type:0.5为小数(0),0-9为个位数(1)
+          result[slotIndex].type = values[i] === '0.5' ? 0 : 1;
+        }
+      }
+      
+    } else if (score >= 10 && score <= 22) {
+      // 规则2: 分数在10到22之间,从右开始显示 0.5, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12...直到score
+      const values = ['0.5', '0']; // 从0.5和0开始
+      
+      // 添加1到score的所有整数
+      for (let i = 1; i <= score; i++) {
+        values.push(i.toString());
+      }
+      
+      // 从右向左填充
+      for (let i = 0; i < values.length; i++) {
+        const slotIndex = TOTAL_SLOTS - 1 - i;
+        if (slotIndex >= 0) {
+          result[slotIndex].value = values[i];
+          // 规则2中,所有数值都是连续显示的,包括10-22,所以都视为个位数(1)
+          // 0.5为小数(0)
+          result[slotIndex].type = values[i] === '0.5' ? 0 : 1;
+        }
+      }
+      
+    } else if (score > 22) {
+      // 规则3: 分数大于22,从右开始显示 0.5, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,中间空一个格子,然后10,20,30...
+      const values = [];
+      
+      // 第一部分:0.5, 0-9
+      values.push('0.5'); // 最右边
+      values.push('0');
+      for (let i = 1; i <= 9; i++) {
+        values.push(i.toString());
+      }
+      
+      // 中间空一个格子
+      values.push('');
+      
+      // 计算十位数的最大值(不超过分数)
+      const maxTens = Math.floor(score / 10) * 10;
+      
+      // 添加十位数:10, 20, 30...
+      for (let tens = 10; tens <= maxTens; tens += 10) {
+        values.push(tens.toString());
+      }
+      
+      // 从右向左填充
+      for (let i = 0; i < values.length; i++) {
+        const slotIndex = TOTAL_SLOTS - 1 - i;
+        if (slotIndex >= 0) {
+          result[slotIndex].value = values[i];
+          
+          // 设置type
+          if (values[i] === '') {
+            result[slotIndex].type = -1; // 空
+          } else if (values[i] === '0.5') {
+            result[slotIndex].type = 0; // 小数
+          } else {
+            const num = parseInt(values[i], 10);
+            // 0-9为个位数,10,20,30...为十位数
+            result[slotIndex].type = num >= 10 ? 2 : 1;
+          }
+        }
+      }
+    }
+    
+    return result;
+  },
+};

+ 125 - 0
src/views/logMonitor/LogMonitorDetail.vue

@@ -0,0 +1,125 @@
+<template>
+    <div class="main_container custom_container">
+        <div class="page_item top_header">
+             <el-button  class="button_default_background" @click="router.go(-1)">
+                <el-icon class="img"><ArrowLeftBold /></el-icon>
+                返回
+            </el-button>
+
+            <div class="people_info">
+                <div>
+                    <span class="label">学校:</span>
+                    <span class="value">xxx</span>
+                </div>
+                <div>
+                    <span class="label">账户:</span>
+                    <span class="value">xxx</span>
+                </div>
+                <div><span class="label">姓名:</span>
+                    <span class="value">xxx</span>
+                </div>
+            </div>
+
+            <div class="page_search" >
+                <div class="search_content">
+                    <div class="content_left">
+                        <FormSearchGroup :searchList="peopleLogSearchList"></FormSearchGroup>
+                    </div>
+                    <div class="content_right">
+                        <div class="desc">
+                            默认展示最近3天操作记录
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+
+        <div class="page_jg_20"></div>
+
+        <div class="page_item end_item">
+            <HonorTimeLine :honorList="peopleLogList"></HonorTimeLine>
+        </div>
+    </div>
+</template>  
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+import FormSearchGroup from '@/baseComponents/FormSearchGroup.vue';
+import {peopleLogSearchList} from './formSearchGroup'
+import HonorTimeLine from '@/baseComponents/HonorTimeLine.vue';
+
+const router = useRouter()
+
+const peopleLogList = ref([
+    {
+        id:'1',
+        awardDatetime: '26-07-14',
+        certPicUrl:'',
+        honorTypeName: '日志名称',
+        honorName:'日志名称',
+        teacherName:'192.168.1.101',
+        radioData:[''],
+        inputData:[],    
+        createdAt:'',
+        reviewStatus:'String',
+    },
+    {
+        id:'1',
+        awardDatetime: '26-07-14',
+        certPicUrl:'',
+        honorTypeName: '日志名称',
+        honorName:'日志名称',
+        teacherName:'192.168.1.101',
+        radioData:[''],
+        inputData:[],    
+        createdAt:'',
+        reviewStatus:'String',
+    }
+
+])
+</script>
+
+
+
+
+<style lang="scss" scoped>
+
+.top_header{
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap:20px;
+    padding:10px 20px;
+
+    .people_info{
+        flex-shrink: 0;
+        display: flex;
+        gap:20px;
+        font-size: 14px;
+        .label{
+            color:#333;
+            font-weight: 600;
+        }
+        .value{
+            color: #999;
+        }
+    }
+
+    .page_search{
+        display: flex;
+        justify-content: flex-end;
+        .search_content{
+            flex: 0 0;
+            .content_right{
+                .desc{
+                    margin-left: 5px;
+                    font-size: 14px;
+                    width: 200px;  //必须加
+                }
+            }
+        }
+    }
+}
+</style>

+ 41 - 0
src/views/logMonitor/formSearchGroup.ts

@@ -0,0 +1,41 @@
+import { ref } from "vue";
+
+
+// 不同类别的日志筛选项
+export const differentTypeLogSearchList = ref([
+    {
+        key:'school',
+        type:'select',
+        placeholder:'请选择学校',
+        hidden:false,
+        defaultValue:'',
+        options:[],
+    },
+    {
+        key:'peopleType',
+        type:'select',
+        placeholder:'请选择人员类型',
+        hidden:false,
+        defaultValue:'',
+        options:[],
+    },
+    {
+        key:'account',
+        type:'input',
+        placeholder:'请输入账号',
+        hidden:false,
+        defaultValue:'',
+    },
+])
+
+export const peopleLogSearchList = ref([
+    {
+        key:'school',
+        type:'datePicker',
+        placeholder:'请选择学校',
+        hidden:false,
+        defaultValue:'',
+        options:[],
+    },
+
+])

+ 106 - 0
src/views/logMonitor/index.vue

@@ -0,0 +1,106 @@
+<template>
+    <div class="main_container custom_container">
+        <div class="page_item end_item">
+            <div class="page_search">
+                <div class="search_content">
+                    <div class="content_left">
+                        <div class="grade_title">后台操作日志监控</div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="page_jg_20"></div>
+
+            <div class="page_search">
+                <div class="search_content">
+                    <div class="content_left">
+                        <FormSearchGroup :searchList="differentTypeLogSearchList"></FormSearchGroup>
+                    </div>
+                </div>
+            </div>
+            <div class="page_jg_20"></div>
+            <Table 
+                :currentPage="currentPage"
+                :pageSize="pageSize"
+                :totalNum="totalNum"
+                :tableColumns="tableColumns" 
+                :tableData="logTableData">
+
+                <template #num="{row,currentIndex}">
+                    {{ currentIndex + pageSize * (currentPage - 1) }}
+                </template>
+                
+                <template #operation="{row}">
+                    <div class="table_row_option">
+                        <span class="option_button_editor" @click="GoToLogMonitorDetail(row)">查看详情</span>
+                    </div>
+                </template>
+            </Table>
+        </div>
+    </div>
+</template>  
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import FormSearchGroup from '@/baseComponents/FormSearchGroup.vue';
+import Table from '@/baseComponents/Table.vue';
+
+import {differentTypeLogSearchList} from './formSearchGroup'
+
+import {tableColumns} from './table'
+
+const router = useRouter()
+const currentPage = ref(1)
+const totalNum = ref(0)
+const pageSize = ref(20)
+
+
+const logTableData = ref([
+    {
+        id:'1',
+        school: '学校1',
+        account: 'teacher01',
+        name: '白语忱',
+        type: '教师',
+        operationDetail: '系统登录',
+        ip: '192.168.1.101',
+        operationTime: '2026-07-14 08:22:15',
+    },
+    {
+        id:'2',
+        school: '学校1',
+        account: 'stu_2026001',
+        name: '蔡宜萱',
+        type: '学生',
+        operationDetail: '查询成绩',
+        ip: '192.168.1.205',
+        operationTime: '2026-07-14 09:10:33',
+    },
+    {
+        id:'3',
+        school: '学校1',
+        account: 'teacher05',
+        name: '曹延熠',
+        type: '教师',
+        operationDetail: '提交纠错',
+        ip: '192.168.1.108',
+        operationTime: '2026-07-14 10:05:12',
+    }
+])
+
+
+const GoToLogMonitorDetail = (row)=>{
+    router.push(`logMonitorDetail?id=${row.id}`)
+}
+
+
+</script>
+
+
+
+
+<style lang="scss" scoped>
+
+</style>

+ 86 - 0
src/views/logMonitor/table.ts

@@ -0,0 +1,86 @@
+import { ref } from "vue";
+
+
+export const tableColumns = ref([
+    {
+        name: 'num', 
+        label: '序号',
+        width: 80,
+        fixed: '',
+        align: 'center',
+        minWidth: '',
+        custom: true,
+    },
+    {
+        name: 'school',
+        label: '学校',
+        width: 150,
+        fixed: '',
+        align: 'center',
+        minWidth: '',
+        custom: false,
+    },
+    {
+        name: 'account',
+        label: '账号',
+        width: 120,
+        fixed: '',
+        align: 'center',
+        minWidth: '',
+        custom: false,
+    },
+    {
+        name: 'name',
+        label: '姓名',
+        width: 100,
+        fixed: '',
+        align: 'center',
+        minWidth: '',
+        custom: false,
+    },
+    {
+        name: 'type', 
+        label: '类型',
+        width: 100,
+        fixed: '',
+        align: 'center',
+        minWidth: '',
+        custom: false,
+    },
+    {
+        name: 'operationDetail',
+        label: '操作详情',
+        width: 150,
+        fixed: '',
+        align: 'center',
+        minWidth: '',
+        custom: false,
+    },
+    {
+        name: 'ip', 
+        label: '访问IP',
+        width: 140,
+        fixed: '',
+        align: 'center',
+        minWidth: '',
+        custom: false,
+    },
+    {
+        name: 'operationTime', 
+        label: '操作时间',
+        width: 180,
+        fixed: '',
+        align: 'center',
+        minWidth: '',
+        custom: false,
+    },
+    {
+        name: 'operation', 
+        label: '操作',
+        width: 120,
+        fixed: 'right', 
+        align: 'center',
+        minWidth: '',
+        custom: true,
+    },
+]);

+ 252 - 0
src/views/performanceMonitor/index.vue

@@ -0,0 +1,252 @@
+<template>
+    <div class="main_container custom_container">
+        <div class="page_item end_item">
+            <div class="page_search">
+                <div class="search_content">
+                    <div class="content_left">
+                        <div class="grade_title">当前在线人数</div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="page_jg_20"></div>
+
+            <ul class="cardBoard">
+                <li v-for="card in cardBoardList" :key="card.label">
+                    <div class="label">{{ card.label }}</div>
+                    <div class="num">{{ card.value }}</div>
+                </li>
+            </ul>
+            
+            <div class="page_jg_20"></div>
+
+
+            <TabSearch tabType="line" :tabList="onlineTypeTablist" 
+                :defaultTabKey="defaultOnlineTypeTab">
+            </TabSearch>
+
+            <div class="page_jg_20"></div>
+            
+             <TabSearch tabType="box" :tabList="dateRangeTablist" 
+                :defaultTabKey="defaultDateRangeTab">
+            </TabSearch>
+
+            <div class="page_jg_20"></div>
+
+            <div class="chart_box">
+                <!--  某段时间的 某类型 在线人数统计 -->
+                <div class="chart">
+                    <div class="page_search">
+                        <div class="search_content">
+                            <div class="content_left">
+                                <div class="grade_title">{{ onLineType  }}在线人数统计</div>
+                            </div>
+                        </div>
+                    </div>
+                    <LineChart reportHeight="244px" :datax="somePeriodTimeLineData.datax"  
+                        :datay="somePeriodTimeLineData.datay" 
+                        :showCheckBox="false" >
+                    </LineChart>
+                </div>
+
+                <!-- 某一时刻的 某类型 在线人数统计  -->
+                <div class="chart">
+                    <div class="page_search">
+                        <div class="search_content">
+                            <div class="content_left">
+                                <div class="grade_title">{{onLineTime}}时 / {{ onLineType  }} 在线人数统计</div>
+                            </div>
+                        </div>
+                    </div>
+                    <MultipleBarCharts :datax="someoneTimeBarData.datax" 
+                        :datay="someoneTimeBarData.datay" :height="244">
+                    </MultipleBarCharts>
+                </div>
+
+                <div class="chart">
+                    <!-- 某个学校的当天在线人数统计 -->
+                    <div class="page_search">
+                        <div class="search_content">
+                            <div class="content_left">
+                                <div class="grade_title">{{schoolName}} / 当天在线人数统计</div>
+                            </div>
+                        </div>
+                    </div>
+
+                    <TabSearch :tabList="visualizationTypeTablist" 
+                        tabType="box" :defaultTabKey="defaultVisualizationTypeTab" 
+                        @tabChange="(val)=>HandleTabChange('visualizationType',val)">
+                    </TabSearch>
+
+
+                    <LineChart v-if="defaultVisualizationTypeTab == 'chart'" reportHeight="208px" :datax="someSchoolOneDayBarData.datax"  
+                        :datay="someSchoolOneDayBarData.datay" 
+                        :showCheckBox="false" >
+                    </LineChart>
+
+                    <template v-if="defaultVisualizationTypeTab == 'table'">
+
+                        <div class="page_search">
+                            <div class="search_content">
+                                <div class="content_left"></div>
+                                <div class="content_right">
+                                    <el-button class="button_refresh">导出</el-button>
+                                </div>
+                            </div>
+                        </div>
+                        <div class="page_jg_20"></div>
+                        <Table :tableColumns="tableColumns" 
+                            :tableData="someSchoolOneDayTableData" >
+                        </Table>
+                    </template>
+
+                    
+
+                </div>
+            </div>
+        </div>
+    </div>
+</template>  
+
+<script setup lang="ts">
+import TabSearch from '@/baseComponents/TabSearch.vue';
+import Table from '@/baseComponents/Table.vue';
+import { computed, reactive, ref } from 'vue';
+
+import { onlineTypeTablist, dateRangeTablist, visualizationTypeTablist } from './tabSearch';
+import {tableColumns} from './table'
+
+
+import MultipleBarCharts from '@/baseComponents/MultipleBarCharts.vue';
+import LineChart from '@/baseComponents/LineChart.vue';
+
+// 看板数据
+const boardMsg = reactive({
+    teacherOnline:1330,
+    studentOnline:1330,
+    scaneOnline:78,
+    examinationPaperOnline:78,
+    analysisOnline:78
+}) 
+// 看板列表
+const cardBoardList = computed(()=>[
+    {
+        label:'教师在线',
+        value:boardMsg.teacherOnline
+    },
+    {
+        label:'学生在线',
+        value:boardMsg.studentOnline
+    },
+    {
+        label:'使用扫描人数',
+        value:boardMsg.scaneOnline
+    },
+    {
+        label:'使用阅卷人数',
+        value:boardMsg.examinationPaperOnline
+    },
+    {
+        label:'使用分析人数',
+        value:boardMsg.analysisOnline
+    },
+])
+
+// 图表数据
+const onLineType = ref('学校教师')  // 在线类型
+const schoolName = ref('北京师范大学附属中学')  //学校名称
+
+// 某段时间的 某类型 在线人数统计
+const somePeriodTimeLineData = reactive({
+    datax:['0时','1时','2时','3时','4时','5时','6时','7时','8时','9时','10时'],
+    datay:[
+        [233,43,56,43,44,34,5,65,76,87,853],
+    ]
+})
+
+// 某一时刻的 某类型 在线人数统计
+const onLineTime = ref(0)
+const someoneTimeBarData = reactive({
+    datax:['北京师范大学附属中学','师达','交大分校','本部高中','本部初中'],
+    datay:[233,43,56,43,44]
+})
+
+// 某个学校的当天在线人数统计 折线图数据
+const someSchoolOneDayBarData = reactive({
+    datax:['0时','1时','2时','3时','4时','5时','6时','7时','8时','9时','10时'],
+    datay:[
+        [233,43,56,43,44,34,5,65,76,87,853],
+    ]
+})
+
+// 某个学校的当天在线人数统计 表格数据
+
+const someSchoolOneDayTableData = ref([
+    {
+        num: 1, // 如果使用 type="index",这个字段其实可以不需要
+        school: '北京师范大学附属中学',
+        account: 'teacher01',
+        name: '白语忱',
+        operationDetail: '系统登录',
+        operationTime: '2026-07-14 08:22:15',
+    },
+    {
+        num: 2,
+        school: '北京师范大学附属中学',
+        account: 'teacher05',
+        name: '曹延熠',
+        operationDetail: '系统登录',
+        operationTime: '2026-07-14 10:05:12',
+    },
+])
+
+
+// 处理tab切换相关操作
+const defaultOnlineTypeTab = ref('teacherOnline')  //  默认的在线类型
+const defaultDateRangeTab = ref('today')  // 默认的 时间范围
+const defaultVisualizationTypeTab = ref('chart')  //   默认的 某个学校的当天在线人数 的可视化展示类型
+const HandleTabChange = (type,val)=>{
+    switch(type){
+        case 'visualizationType':
+            defaultVisualizationTypeTab.value = val
+            break
+    }
+}
+
+</script>
+
+
+
+
+<style lang="scss" scoped>
+
+.cardBoard{
+    display: grid;
+    grid-template-columns: repeat(5,1fr);
+    gap:20px;
+    li{
+        background-color: #f2f2f2;
+        padding:20px;
+        border-radius: 8px;
+        .label{
+            font-size: 16px;
+        }
+        .num{
+            font-size: 34px;
+            font-weight: 600;
+        }
+    }
+}
+
+.chart_box{
+    display: flex;
+    flex-direction: column;
+    gap:10px;
+    .chart{
+        border:1px solid #dcdfe6;
+        min-height:320px;
+        padding:20px;
+    }
+}
+
+</style>

+ 63 - 0
src/views/performanceMonitor/tabSearch.ts

@@ -0,0 +1,63 @@
+import { ref } from "vue";
+
+
+// 在线类型列表
+export const onlineTypeTablist = ref([
+    {
+        label:'教师在线',
+        key:'teacherOnline',
+    },
+    {
+        label:'学生在线',
+        key:'studentOnline',
+    },
+    {
+        label:'阅卷在线',
+        key:'examinationPaperOnline',
+    },
+    {
+        label:'扫描在线',
+        key:'scaneOnline',
+    },
+    {
+        label:'分析在线',
+        key:'analysisOnline',
+    },
+])
+
+
+// 时间范围列表
+export const dateRangeTablist = ref([
+    {
+        label:'本日',
+        key:'today',
+    },
+    {
+        label:'3天',
+        key:'3',
+    },
+    {
+        label:'7天',
+        key:'7',
+    },
+    {
+        label:'14天',
+        key:'14',
+    },
+    {
+        label:'自定义',
+        key:'custom',
+    },
+])
+
+// 可视化类型列表
+export const visualizationTypeTablist = ref([
+    {
+        label:'图形',
+        key:'chart',
+    },
+     {
+        label:'表格',
+        key:'table',
+    },
+])

+ 58 - 0
src/views/performanceMonitor/table.ts

@@ -0,0 +1,58 @@
+import { ref } from "vue";
+
+export const tableColumns = ref([
+    {
+        name: 'num',
+        label: '序号',
+        width: 80, // 序号通常不需要太宽
+        fixed: '',
+        align: 'center',
+        minWidth: '',
+        custom: false,
+    },
+    {
+        name: 'school',
+        label: '学校',
+        width: 200, // 学校名称较长,建议增加宽度
+        fixed: '',
+        align: 'center',
+        minWidth: '',
+        custom: false,
+    },
+    {
+        name: 'account',
+        label: '账户',
+        width: 120,
+        fixed: '',
+        align: 'center',
+        minWidth: '',
+        custom: false,
+    },
+    {
+        name: 'name',
+        label: '姓名',
+        width: 100,
+        fixed: '',
+        align: 'center',
+        minWidth: '',
+        custom: false,
+    },
+    {
+        name: 'operationDetail',
+        label: '操作详情',
+        width: 150,
+        fixed: '',
+        align: 'center',
+        minWidth: '',
+        custom: false,
+    },
+    {
+        name: 'operationTime',
+        label: '操作时间',
+        width: 180, // 时间格式较长,需要足够宽度
+        fixed: '',
+        align: 'center',
+        minWidth: '',
+        custom: false,
+    }
+])

+ 19 - 0
src/views/userSearch/formSearchGroup.ts

@@ -0,0 +1,19 @@
+import { ref } from "vue";
+
+
+export const searchList = ref([
+    {
+        key:'account',
+        type:'input',
+        placeholder:'请输入账号',
+        hidden:false,
+        defaultValue:'',
+    },
+    {
+        key:'phone',
+        type:'input',
+        placeholder:'请输入手机号',
+        hidden:false,
+        defaultValue:'',
+    },
+])

+ 66 - 0
src/views/userSearch/index.vue

@@ -0,0 +1,66 @@
+<template>
+    <div class="main_container custom_container">
+        <div class="page_item end_item">
+            <div class="page_search">
+                <div class="search_content">
+                    <div class="content_left">
+                        <div class="grade_title">用户查询</div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="page_jg_20"></div>
+
+            <div class="page_search">
+                <div class="search_content">
+                    <div class="content_left">
+                        <FormSearchGroup :searchList="searchList"></FormSearchGroup>
+                    </div>
+                </div>
+            </div>
+
+            <div class="page_jg_20"></div>
+            <Table :currentPage="currentPage"
+                :pageSize="pageSize"
+                :totalNum="totalNum"
+                :tableColumns="tableColumns" 
+                :tableData="userTableData">
+            </Table>
+        </div>
+    </div>
+</template>  
+
+<script setup lang="ts">
+import { ref } from 'vue';
+
+import FormSearchGroup from '@/baseComponents/FormSearchGroup.vue';
+import Table from '@/baseComponents/Table.vue';
+
+import {searchList} from './formSearchGroup'
+
+import {tableColumns} from './table'
+
+const currentPage = ref(1)
+const totalNum = ref(0)
+const pageSize = ref(20)
+
+const userTableData = ref([
+    {
+        account: 'teacher01',
+        name: '白语忱',
+        userType: '教师',
+        school: '北京师范大学',
+        wechatName: '', // 图片中该列为空白
+        phone: '13459607083',
+        status: '启用',
+    }
+])
+
+</script>
+
+
+
+
+<style lang="scss" scoped>
+
+</style>

+ 68 - 0
src/views/userSearch/table.ts

@@ -0,0 +1,68 @@
+import { ref } from "vue";
+
+
+export const tableColumns = ref([
+    {
+        name: 'account',
+        label: '账号',
+        width: 120,
+        fixed: '',
+        align: 'center',
+        minWidth: '',
+        custom: false,
+    },
+    {
+        name: 'name',
+        label: '姓名',
+        width: 100,
+        fixed: '',
+        align: 'center',
+        minWidth: '',
+        custom: false,
+    },
+    {
+        name: 'userType',
+        label: '用户类型',
+        width: 120,
+        fixed: '',
+        align: 'center',
+        minWidth: '',
+        custom: false,
+    },
+    {
+        name: 'school',
+        label: '学校',
+        width: 150, 
+        fixed: '',
+        align: 'center',
+        minWidth: '',
+        custom: false,
+    },
+    {
+        name: 'wechatName',
+        label: '微信名',
+        width: 120,
+        fixed: '',
+        align: 'center',
+        minWidth: '',
+        custom: false,
+    },
+    {
+        name: 'phone',
+        label: '手机号',
+        width: 130,
+        fixed: '',
+        align: 'center',
+        minWidth: '',
+        custom: false,
+    },
+    {
+        name: 'status',
+        label: '状态',
+        width: 100,
+        fixed: '',
+        align: 'center',
+        minWidth: '',
+        custom: false,
+    },
+]);