Răsfoiți Sursa

日志监控、业务监控功能及样式修改

吴朋磊 1 săptămână în urmă
părinte
comite
a65dd8a65c

+ 1 - 2
src/baseComponents/FormSearchGroup.vue

@@ -1,10 +1,9 @@
 <!-- 需要配合 page_search   search_content    content_left  一起使用 -->
 
 <template>
-    <div class="simple_filter">
+    <div class="simple_filter" style="gap: 20px;">
         <template v-for="item in props.searchList" :key="item.key">
             <div class="filter_item">
-                <div v-if="item.label" class="filter_label">{{ item.label }}:</div>
                 <el-select v-if="item.type == 'select' && !item.hidden" v-model="searchData[item.key]"
                 @change="() => HandleSearchItemChange(item.key)" :placeholder="item.placeholder"
                 :disabled="formDisabled || item.disabled" :filterable="item.filterable === true"

+ 5 - 1
src/baseComponents/LineChart.vue

@@ -151,6 +151,10 @@ export default {
         reportHeight: {//报告高度
             type: String,
             default: ''
+        },
+        disableLabelRotate: {
+            type: Boolean,
+            default: false
         }
 
     },
@@ -377,7 +381,7 @@ export default {
                     },
                     axisLabel: {
                         interval: 0,//0 显示所有刻度
-                        rotate: singleSeriesWidth < 60 ? 45 : 0,
+                        rotate: this.disableLabelRotate ? 0 : (singleSeriesWidth < 60 ? 45 : 0),
                         fontSize: this.fontSize ? this.fontSize : 14,
                         color: this.fontColor || "#666",
                         fontWeight: 400,

+ 1 - 3
src/baseComponents/TabSearch.vue

@@ -119,10 +119,8 @@ const HandleTabChange = async (params: string) =>{
 .line_tab{
     display: flex;
     gap:24px;
-    padding-bottom: 10px;
     color:#666666;
     font-size:14px;
-
     >span{
         &:hover{
             cursor: pointer;
@@ -136,7 +134,7 @@ const HandleTabChange = async (params: string) =>{
         &::after{
             position: absolute;
             left: 0;
-            bottom: -10px;
+            bottom: -5px;
             content:'';
             width: 100%;
             height: 2px;

+ 1 - 1
src/baseComponents/Table.vue

@@ -8,7 +8,7 @@
           element-loading-custom-class="loading_icon"
           :row-class-name="props.tableRowClassName"
           @row-click="(row:any,column:any)=> emit('rowClick',row)"
-          :span-method="props.spanMethod">
+          :span-method="props.spanMethod" height="605px">
             <!-- show-overflow-tooltip -->
             <template 
                 v-for="(item,index) in props.tableColumns" 

+ 278 - 131
src/views/logMonitor/LogMonitorDetail.vue

@@ -1,60 +1,83 @@
 <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">{{ detailParams.schoolName }}</span>
-                </div>
-                <div>
-                    <span class="label">账户:</span>
-                    <span class="value">{{ detailParams.userAccount }}</span>
-                </div>
-                <div><span class="label">姓名:</span>
-                    <span class="value">{{ detailParams.userName }}</span>
-                </div>
-            </div>
-
-            <div class="page_search" >
-                <div class="search_content">
-                    <div class="content_left">
-                        <FormSearchGroup :searchList="peopleLogSearchList" @searchChange="handleSearchChange"></FormSearchGroup>
+    <div class="page_dialog">
+        <el-dialog v-model="dialogVisible" title="操作详情"  width="1200px" :close-on-click-modal="false" @open="handleOpen">
+            <div class="detail_content">
+                <div class="detail_info_bar">
+                    <!-- 左侧信息 -->
+                    <div class="info_left">
+                        <span class="info_item">{{ detailParams.schoolName }}</span>
+                        <span class="info_item">{{ detailParams.userAccount }}</span>
+                        <span class="info_item">{{ detailParams.userName }}</span>
                     </div>
-                    <div class="content_right">
-                        <div class="desc">
-                            默认展示最近3天操作记录
+                    <!-- 右侧查询时间范围 -->
+                    <div class="info_right">
+                        <span class="label">查询时间范围:</span>
+                        <el-date-picker
+                            v-model="dateRange"
+                            type="daterange"
+                            range-separator="至"
+                            start-placeholder="开始日期"
+                            end-placeholder="结束日期"
+                            value-format="YYYY-MM-DD"
+                            :shortcuts="dateShortcuts"
+                            :teleported="false"
+                            @change="handleDateChange"
+                        />
+                        <el-button type="primary" @click="handleQuery">查询</el-button>
+                    </div>
+                </div>
+                <!-- 时间线 -->
+                <div class="detail_timeline">
+                    <div v-if="peopleLogList.length > 0" class="timeline_list">
+                        <div
+                            v-for="(item, index) in peopleLogList"
+                            :key="index"
+                            class="timeline_item"
+                        >
+                            <!-- 左侧时间戳 -->
+                            <div class="timeline_time">{{ item.awardDatetime }}</div>
+                            <!-- 中间时间线节点 -->
+                            <div class="timeline_node">
+                                <div class="node_dot"></div>
+                            </div>
+                            <!-- 右侧内容 -->
+                            <div class="timeline_content">
+                                <div class="operation_name">{{ item.honorName }}</div>
+                                <div class="operation_ip">访问IP:{{ item.teacherName }}</div>
+                            </div>
                         </div>
                     </div>
+                    <div v-else class="no_data">
+                        <span>暂无数据</span>
+                    </div>
                 </div>
             </div>
-        </div>
-
-
-        <div class="page_jg_20"></div>
-
-        <div class="page_item end_item">
-            <HonorTimeLine :honorList="peopleLogList"></HonorTimeLine>
-        </div>
+        </el-dialog>
     </div>
-</template>  
+</template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted } from 'vue';
-import { useRoute, useRouter } from 'vue-router';
-import { ArrowLeftBold } from '@element-plus/icons-vue';
+import { ref, reactive, computed } from 'vue';
 import { ElMessage } from 'element-plus';
-import FormSearchGroup from '@/baseComponents/FormSearchGroup.vue';
-import { peopleLogSearchList } from './formSearchGroup';
-import HonorTimeLine from '@/baseComponents/HonorTimeLine.vue';
 import { getLogDetailApi } from '@/api/logMonitor';
 
-const route = useRoute()
-const router = useRouter()
+const props = defineProps({
+    modelValue: {
+        type: Boolean,
+        default: false
+    },
+    rowData: {
+        type: Object,
+        default: () => ({})
+    }
+})
+
+const emit = defineEmits(['update:modelValue'])
+
+const dialogVisible = computed({
+    get: () => props.modelValue,
+    set: (val) => emit('update:modelValue', val)
+})
 
 const detailParams = reactive({
     schoolId: 0,
@@ -66,15 +89,85 @@ const detailParams = reactive({
     userName: '',
 })
 
-// 详情搜索参数
 const detailSearchParams = reactive({
     startTime: '',
     endTime: '',
 })
 
+const dateRange = ref<string[]>([])
+
+const dateShortcuts = [
+    {
+        text: '最近3天',
+        value: () => {
+            const end = new Date()
+            const start = new Date()
+            start.setDate(start.getDate() - 2)
+            return [start, end]
+        }
+    },
+    {
+        text: '最近7天',
+        value: () => {
+            const end = new Date()
+            const start = new Date()
+            start.setDate(start.getDate() - 6)
+            return [start, end]
+        }
+    },
+    {
+        text: '最近30天',
+        value: () => {
+            const end = new Date()
+            const start = new Date()
+            start.setDate(start.getDate() - 29)
+            return [start, end]
+        }
+    }
+]
+
 const peopleLogList = ref<any[]>([])
 
-// 获取日志详情
+const handleOpen = () => {
+    const row = props.rowData
+    detailParams.schoolId = row.schoolId || 0
+    detailParams.userId = row.userId || 0
+    detailParams.accountTypeCode = row.accountTypeCode || 0
+    detailParams.moduleCode = row.moduleCode || 0
+    detailParams.schoolName = row.schoolName || ''
+    detailParams.userAccount = row.userAccount || ''
+    detailParams.userName = row.userName || ''
+
+    const defaultRange = getDefaultDateRange()
+    dateRange.value = defaultRange
+    detailSearchParams.startTime = defaultRange[0] + ' 00:00:00'
+    detailSearchParams.endTime = defaultRange[1] + ' 23:59:59'
+
+    getLogDetailData()
+}
+
+const handleDateChange = (val: string[]) => {
+    if (val && val.length === 2 && val[0] && val[1]) {
+        const start = new Date(val[0])
+        const end = new Date(val[1])
+        const diffDays = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24))
+        if (diffDays > 30) {
+            ElMessage.warning('日志选择最大范围30天')
+            dateRange.value = []
+            return
+        }
+        detailSearchParams.startTime = val[0] + ' 00:00:00'
+        detailSearchParams.endTime = val[1] + ' 23:59:59'
+    } else {
+        detailSearchParams.startTime = ''
+        detailSearchParams.endTime = ''
+    }
+}
+
+const handleQuery = () => {
+    getLogDetailData()
+}
+
 const getLogDetailData = async () => {
     try {
         const params = {
@@ -87,119 +180,173 @@ const getLogDetailData = async () => {
         }
         const res = await getLogDetailApi(params)
         const data = res.data || res
-        
+
         peopleLogList.value = (data || []).map((item: any, index: number) => ({
             id: String(index),
             awardDatetime: item.createTime || '',
-            certPicUrl: '',
-            honorTypeName: '',
             honorName: item.operation || '',
             teacherName: item.ip || '',
-            radioData: [],
-            inputData: [],
-            createdAt: item.createTime || '',
-            reviewStatus: '',
         }))
     } catch (err) {
         console.error('getLogDetailApi 请求失败:', err)
     }
 }
 
-// 处理搜索变化
-const handleSearchChange = (data: Record<string, any>, key: string) => {
-    if (key === 'dateRange') {
-        const range = data.dateRange
-        if (Array.isArray(range) && range.length === 2 && range[0] && range[1]) {
-            const start = new Date(range[0])
-            const end = new Date(range[1])
-            const diffDays = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24))
-            if (diffDays > 30) {
-                ElMessage.warning('日志选择最大范围30天')
-                return
-            }
-            detailSearchParams.startTime = formatDateTime(start, true)
-            detailSearchParams.endTime = formatDateTime(end, false)
-        } else {
-            detailSearchParams.startTime = ''
-            detailSearchParams.endTime = ''
-        }
-    }
-    getLogDetailData()
-}
-
-// 格式化日期时间,isStart为true时返回 00:00:00,为false时返回 23:59:59
-const formatDateTime = (date: Date, isStart: boolean) => {
-    if (!date) return ''
-    const year = date.getFullYear()
-    const month = String(date.getMonth() + 1).padStart(2, '0')
-    const day = String(date.getDate()).padStart(2, '0')
-    const time = isStart ? '00:00:00' : '23:59:59'
-    return `${year}-${month}-${day} ${time}`
-}
-
-// 获取默认日期范围(最近3天)
 const getDefaultDateRange = () => {
     const end = new Date()
     const start = new Date()
     start.setDate(start.getDate() - 2)
-    return [start, end]
+    const fmt = (d: Date) => {
+        const y = d.getFullYear()
+        const m = String(d.getMonth() + 1).padStart(2, '0')
+        const day = String(d.getDate()).padStart(2, '0')
+        return `${y}-${m}-${day}`
+    }
+    return [fmt(start), fmt(end)]
 }
-
-// 初始化
-onMounted(() => {
-    detailParams.schoolId = Number(route.query.schoolId) || 0
-    detailParams.userId = Number(route.query.userId) || 0
-    detailParams.accountTypeCode = Number(route.query.accountTypeCode) || 0
-    detailParams.moduleCode = Number(route.query.moduleCode) || 0
-    detailParams.schoolName = (route.query.schoolName as string) || ''
-    detailParams.userAccount = (route.query.userAccount as string) || ''
-    detailParams.userName = (route.query.userName as string) || ''
-
-    // 默认最近3天
-    const defaultRange = getDefaultDateRange()
-    peopleLogSearchList.value[0].defaultValue = defaultRange
-    detailSearchParams.startTime = formatDateTime(defaultRange[0], true)
-    detailSearchParams.endTime = formatDateTime(defaultRange[1], false)
-
-    getLogDetailData()
-})
 </script>
 
+<style lang="scss" scoped>
+.detail_content {
+    .detail_info_bar {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
 
+        .info_left {
+            display: flex;
+            gap: 10px;
 
+            .info_item {
+                font-size: 16px;
+                font-weight: 500;
+                color: #333;
+            }
+        }
 
-<style lang="scss" scoped>
-
-.top_header{
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    gap:20px;
-    padding:10px 20px;
+        .info_right {
+            display: flex;
+            align-items: center;
 
-    .people_info{
-        flex-shrink: 0;
-        display: flex;
-        gap:20px;
-        font-size: 14px;
-        .label{
-            color:#333;
-            font-weight: 600;
+            .label {
+                font-size: 14px;
+                color: #333;
+                line-height: 36px;
+            }
+            :deep(.el-input__wrapper) {
+                width: 230px !important;
+                height: 36px;
+                line-height: 36px;
+            }
+            :deep(.el-button) {
+                margin-left: 20px;
+                height: 36px;
+                background-color: #2E64FA;
+                color: #fff;
+            }
         }
-        .value{
+    }
+
+    .detail_timeline {
+        margin-top: 20px;
+        max-height: 500px;
+        overflow-y: auto;
+
+        .no_data {
+            padding: 60px 0;
+            text-align: center;
             color: #999;
+            font-size: 14px;
         }
-    }
 
-    .page_search{
-        .search_content{
-            justify-content: flex-end;
-            gap:20px;
-            .content_right{
-                .desc{
+        .timeline_list {
+            position: relative;
+
+            &::before {
+                content: '';
+                position: absolute;
+                left: 166px;
+                width: 1px;
+                top: 6px;
+                bottom: 0;
+                background-color: #DDD;
+            }
+
+            .timeline_item {
+                display: flex;
+                align-items: flex-start;
+                position: relative;
+
+                .timeline_time {
+                    width: 125px;
+                    flex-shrink: 0;
                     font-size: 14px;
+                    color: #666;
+                    font-weight: 500;
+                    line-height: 20px;
+                    padding-right: 20px;
+                }
+
+                .timeline_node {
+                    width: 12px;
+                    flex-shrink: 0;
+                    display: flex;
+                    justify-content: center;
+                    padding-top: 6px;
+
+                    .node_dot {
+                        width: 8px;
+                        height: 8px;
+                        border-radius: 50%;
+                        background-color: #DDD;
+                        flex-shrink: 0;
+                    }
+                }
+
+                .timeline_content {
+                    flex: 1;
+                    padding-left: 20px;
+                    padding-bottom: 20px;
+
+                    .operation_name {
+                        font-size: 14px;
+                        font-weight: 500;
+                        color: #333;
+                        line-height: 20px;
+                    }
+
+                    .operation_ip {
+                        font-size: 13px;
+                        color: #999;
+                        margin-top: 4px;
+                    }
+                }
+
+                &:last-child {
+                    align-items: stretch;
+
+                    &::after {
+                        content: '';
+                        position: absolute;
+                        left: 163px;
+                        top: 14px;
+                        width: 8px;
+                        height: calc(100% - 14px);
+                        background-color: #fff;
+                        z-index: 2;
+                    }
+
+                    .timeline_node {
+                        position: relative;
+                        z-index: 3;
+                        justify-content: flex-start;
+                        .node_dot{
+                            margin-left: 2px;
+                        }
+                    }
                 }
             }
+
         }
     }
 }

+ 32 - 23
src/views/logMonitor/formSearchGroup.ts

@@ -16,42 +16,51 @@ interface SearchItem {
 // 不同类别的日志筛选项
 export const differentTypeLogSearchList = ref<SearchItem[]>([
     {
-        key: 'accountType',
+        key: 'schoolId',
         type: 'select',
-        label: '账号类型',
-        placeholder: '请选择账号类型',
+        label: '',
+        placeholder: '全部学校',
         hidden: false,
-        defaultValue: 0,
+        // defaultValue: 0,
         options: [],
     },
     {
-        key: 'moduleType',
+        key: 'accountTypeCode',
         type: 'select',
-        label: '功能模块',
-        placeholder: '请选择功能模块',
+        label: '',
+        placeholder: '全部类型',
         hidden: false,
-        defaultValue: 0,
+        // defaultValue: 0,
         options: [],
     },
     {
-        key: 'operationType',
-        type: 'select',
-        label: '操作类型',
-        placeholder: '请选择操作类型',
+        key: 'userAccount',
+        type: 'input',
+        label: '',
+        placeholder: '请输入账号',
         hidden: false,
-        defaultValue: 0,
-        options: [],
-    },
-    {
-        key: 'account',
-        type: 'select',
-        label: '用户账号',
-        placeholder: '请选择用户账号',
-        hidden: false,
-        defaultValue: 0,
-        filterable: true,
+        defaultValue: '',
         options: [],
     },
+    // {
+    //     key: 'moduleType',
+    //     type: 'select',
+    //     label: '功能模块',
+    //     placeholder: '请选择功能模块',
+    //     hidden: false,
+    //     defaultValue: 0,
+    //     options: [],
+    // },
+    // {
+    //     key: 'operationType',
+    //     type: 'select',
+    //     label: '操作类型',
+    //     placeholder: '请选择操作类型',
+    //     hidden: false,
+    //     defaultValue: 0,
+    //     options: [],
+    // },
+    
 ])
 
 export const peopleLogSearchList = ref<SearchItem[]>([

+ 41 - 23
src/views/logMonitor/index.vue

@@ -1,6 +1,6 @@
 <template>
     <div class="main_container custom_container">
-        <div class="page_item end_item">
+        <div class="page_item end_item" >
             <div class="page_search">
                 <div class="search_content">
                     <div class="content_left">
@@ -12,13 +12,16 @@
             <div class="page_jg_20"></div>
 
             <div class="page_search">
-                <div class="search_content">
-                    <div class="content_left">
+                <div class="search_content" style="justify-content: flex-start; gap: 20px;">
+                    <div class="content_left" >
                         <FormSearchGroup :searchList="differentTypeLogSearchList" @searchChange="handleSearchChange"></FormSearchGroup>
                     </div>
+                    <div class="content_right">
+                        <el-button type="primary" @click="handleSearch">查询</el-button>
+                    </div>
                 </div>
             </div>
-            <div class="page_jg_20"></div>
+            <div class="page_jg_10"></div>
             <Table :currentPage="currentPage" :pageSize="pageSize" :totalNum="totalNum" :tableColumns="tableColumns"
                 :tableData="logTableData" @paginationChange="handlePaginationChange">
 
@@ -32,24 +35,25 @@
                     </div>
                 </template>
             </Table>
+            <LogMonitorDetail v-model="detailVisible" :rowData="currentRow"></LogMonitorDetail>
         </div>
     </div>
+
+    
 </template>
 
 <script setup lang="ts">
 import { ref, onMounted } from 'vue';
-import { useRouter } from 'vue-router';
 
 import FormSearchGroup from '@/baseComponents/FormSearchGroup.vue';
 import Table from '@/baseComponents/Table.vue';
+import LogMonitorDetail from './LogMonitorDetail.vue';
 
 import { differentTypeLogSearchList } from './formSearchGroup'
 
 import { tableColumns } from './table'
 
 import { getLogHeadApi, getLogPageApi } from '@/api/logMonitor'
-
-const router = useRouter()
 const currentPage = ref(1)
 const totalNum = ref(0)
 const pageSize = ref(20)
@@ -66,29 +70,24 @@ const searchParams = ref({
     userAccount: 0,
 })
 
-// 跳转日志详情页
+// 详情弹窗
+const detailVisible = ref(false)
+const currentRow = ref<any>({})
+
+// 打开日志详情弹窗
 const GoToLogMonitorDetail = (row: any) => {
-    router.push({
-        name: 'logMonitorDetail',
-        query: {
-            schoolId: row.schoolId,
-            userId: row.userId,
-            accountTypeCode: row.accountTypeCode,
-            moduleCode: row.moduleCode,
-            schoolName: row.schoolName,
-            userAccount: row.userAccount,
-            userName: row.userName,
-        }
-    })
+    currentRow.value = row
+    detailVisible.value = true
 }
 
 // 处理搜索变化
 const handleSearchChange = (data: Record<string, any>, key: string) => {
     const fieldMap: Record<string, string> = {
+        schoolId: 'schoolId',
         accountType: 'accountTypeCode',
         moduleType: 'moduleCode',
         operationType: 'operationTypeCode',
-        account: 'userAccount',
+        userAccount: 'userAccount',
     }
     
     if (fieldMap[key]) {
@@ -100,6 +99,12 @@ const handleSearchChange = (data: Record<string, any>, key: string) => {
     getLogTableData()
 }
 
+// 处理查询
+const handleSearch = () => {
+    currentPage.value = 1
+    getLogTableData()
+}
+
 // 获取日志表格数据
 const getLogTableData = async () => {
     try {
@@ -139,7 +144,7 @@ const getLogHead = async () => {
             accountType: (data.accountTypeList || []).map((item: any) => ({ label: item.name, value: item.code })),
             moduleType: (data.moduleTypeList || []).map((item: any) => ({ label: item.name, value: item.code })),
             operationType: (data.operationTypeList || []).map((item: any) => ({ label: item.name, value: item.code })),
-            account: (data.schoolList || []).map((item: any) => ({ label: item.schoolName, value: item.schoolId })),
+            schoolId: (data.schoolList || []).map((item: any) => ({ label: item.schoolName, value: item.schoolId })),
         }
 
         differentTypeLogSearchList.value = differentTypeLogSearchList.value.map(item => {
@@ -164,4 +169,17 @@ onMounted(async () => {
 
 
 
-<style lang="scss" scoped></style>
+<style lang="scss" scoped>
+.page_search{
+    .search_content{
+        .content_right{
+            :deep(.el-button){
+                width: 100px !important;
+                background-color: #2e64fa !important;
+                color: #fff !important;
+                border: none !important;
+            }
+        }
+    }
+}
+</style>

+ 408 - 192
src/views/performanceMonitor/index.vue

@@ -1,101 +1,132 @@
 <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 class="stat_cards">
+                <div v-for="card in cardBoardList" :key="card.label" class="stat_card">
+                    <div class="stat_left">
+                        <el-icon :size="13" :color="'#666'">
+                            <component :is="card.icon" />
+                        </el-icon>
+                        <span class="stat_name">{{ card.label }}</span>
                     </div>
+                    <div class="stat_value">{{ card.value }}</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_16"></div>
+            <div class="tabs_container">
+                <TabSearch tabType="box" :tabList="onlineTypeTablist" :defaultTabKey="defaultOnlineTypeTab"
+                    @tabChange="(val) => HandleTabChange('onlineType', val)">
+                </TabSearch>
+                <div style="height: 12px;"></div>
+                <div class="date_range_area">
+                    <TabSearch tabType="line" :tabList="dateRangeTablist" :defaultTabKey="defaultDateRangeTab"
+                        @tabChange="(val) => HandleTabChange('dateRange', val)">
+                    </TabSearch>
+                    <div  class="custom_date_picker">
+                        <el-date-picker
+                            v-model="customDateRange"
+                            type="daterange"
+                            range-separator="至"
+                            start-placeholder="开始日期"
+                            end-placeholder="结束日期"
+                            value-format="YYYY-MM-DD"
+                            :teleported="false"
+                            @change="handleCustomDateChange"
+                        />
+                    </div>
+                </div>
+            </div>
 
-            <!-- 图表 -->
+            <!-- 主折线图:某时间段在线人数统计 -->
             <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 class="chart_panel">
+                <div class="panel_title">
+                    {{ currentOnlineLabel }}人数统计
                 </div>
+                <LineChart
+                    reportHeight="280px"
+                    :datax="mainLineChartData.datax"
+                    :datay="mainLineChartData.datay"
+                    :title="[currentOnlineLabel + '在线']"
+                    :showCheckBox="false"
+                    :showBackground="true"
+                    :disableLabelRotate="true"
+                    isSmooth
+                    unit="人"
+                />
+            </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 class="page_jg_20"></div>
+            <div class="bottom_section">
+                <!-- 左侧:某时刻各学校在线人数 -->
+                <div class="chart_panel left_panel">
+                    <div class="panel_title">
+                        {{ currentHourLabel }}时 {{ currentOnlineLabel }}在线人数统计
                     </div>
-                    <MultipleBarCharts :datax="someoneTimeBarData.datax" :datay="someoneTimeBarData.datay"
-                        :height="244">
-                    </MultipleBarCharts>
+                    <MultipleBarCharts
+                        :datax="barChartData.datax"
+                        :datay="barChartData.datay"
+                        :height="260"
+                        :isClick="true"
+                        color="#2E64FA"
+                        :showDataZoom="false"
+                        @HandleChartClick="handleBarClick"
+                    />
                 </div>
 
-                <div class="chart">
-                    <!-- 某个学校的当天在线人数统计 -->
-                    <div class="page_search">
-                        <div class="search_content">
-                            <div class="content_left">
-                                <div class="grade_title">{{ schoolName }} / 当天在线人数统计</div>
+                <!-- 右侧:某学校当天在线人数 -->
+                <div class="chart_panel right_panel">
+                    <div class="panel_header">
+                        <div class="panel_title">{{ selectedSchoolName }} 当天在线人数统计</div>
+                        <div class="panel_actions">
+                            <el-button class="btn_export" @click="handleExport">
+                                <el-icon class="btn_icon"><Download /></el-icon>
+                                导出Excel
+                            </el-button>
+                            <div class="view_toggle">
+                                <span
+                                    :class="{ active: defaultVisualizationTypeTab === 'chart' }"
+                                    @click="defaultVisualizationTypeTab = 'chart'"
+                                >分析图</span>
+                                <span
+                                    :class="{ active: defaultVisualizationTypeTab === 'table' }"
+                                    @click="defaultVisualizationTypeTab = 'table'"
+                                >分析表</span>
                             </div>
                         </div>
                     </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'">
+                    <template v-if="defaultVisualizationTypeTab === 'chart'">
+                        <LineChart
+                            reportHeight="230px"
+                            :datax="schoolLineChartData.datax"
+                            :datay="schoolLineChartData.datay"
+                            :title="[selectedSchoolName + '在线']"
+                            :showCheckBox="false"
+                            :showBackground="true"
+                            :disableLabelRotate="true"
+                            isSmooth
+                            unit="人"
+                        />
+                    </template>
 
-                        <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>
+                    <template v-else>
+                        <div class="table_info">
+                            共 {{ schoolTableData.length }} 条数据
                         </div>
-                        <div class="page_jg_20"></div>
-                        <Table :tableColumns="tableColumns" :tableData="someSchoolOneDayTableData">
-                        </Table>
+                        <Table 
+                            :tableColumns="tableColumns" 
+                            :tableData="pagedTableData"
+                            :currentPage="currentPage"
+                            :pageSize="pageSize"
+                            :totalNum="schoolTableData.length"
+                            @paginationChange="handleTablePageChange"
+                        />
                     </template>
-
-
-
                 </div>
             </div>
         </div>
@@ -106,112 +137,178 @@
 import TabSearch from '@/baseComponents/TabSearch.vue';
 import Table from '@/baseComponents/Table.vue';
 import { computed, reactive, ref, onMounted } from 'vue';
+import { User, Reading, Tickets, EditPen, DataAnalysis, Download } from '@element-plus/icons-vue';
 
-import { onlineTypeTablist, dateRangeTablist, visualizationTypeTablist } from './tabSearch';
-import { tableColumns } from './table'
-import { getHeadData, getModuleList, getChartData } from '@/api/performanceMonitor'
-
+import { onlineTypeTablist, dateRangeTablist } from './tabSearch';
+import { tableColumns } from './table';
+import { getHeadData, getModuleList, getChartData } from '@/api/performanceMonitor';
 
 import MultipleBarCharts from '@/baseComponents/MultipleBarCharts.vue';
 import LineChart from '@/baseComponents/LineChart.vue';
 
-
-
-// 看板数据
+// ==================== 顶部看板 ====================
 const boardMsg = reactive({
     teacherOnline: 0,
     studentOnline: 0,
     scaneOnline: 0,
     examinationPaperOnline: 0,
-    analysisOnline: 0
+    analysisOnline: 0,
 })
-// 看板列表
+
 const cardBoardList = computed(() => [
-    {
-        label: '教师在线',
-        value: boardMsg.teacherOnline
-    },
-    {
-        label: '学生在线',
-        value: boardMsg.studentOnline
-    },
-    {
-        label: '使用扫描人数',
-        value: boardMsg.scaneOnline
-    },
-    {
-        label: '使用阅卷人数',
-        value: boardMsg.examinationPaperOnline
-    },
-    {
-        label: '使用分析人数',
-        value: boardMsg.analysisOnline
-    },
+    { label: '教师在线', value: boardMsg.teacherOnline, icon: User },
+    { label: '学生在线', value: boardMsg.studentOnline, icon: Reading },
+    { label: '扫描人数', value: boardMsg.scaneOnline, icon: Tickets },
+    { label: '阅卷人数', value: boardMsg.examinationPaperOnline, icon: EditPen },
+    { label: '分析人数', value: boardMsg.analysisOnline, icon: DataAnalysis },
 ])
 
-// 图表数据
-const onLineType = ref('学校教师')  // 在线类型
-const schoolName = ref('北京师范大学附属中学')  //学校名称
+// ==================== Tab 状态 ====================
+const defaultOnlineTypeTab = ref('1')
+const defaultDateRangeTab = ref('today')
+const defaultVisualizationTypeTab = ref('chart')
+
+const dateRangeToTimeType: Record<string, number> = {
+    today: 1,
+    '3': 2,
+    '7': 3,
+    '14': 4,
+    custom: 5,
+}
 
-// 某段时间的 某类型 在线人数统计
-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 currentOnlineLabel = computed(() => {
+    const found = onlineTypeTablist.value.find(
+        (item: any) => item.key === defaultOnlineTypeTab.value
+    )
+    return found?.label || '教师'
 })
 
-// 某一时刻的 某类型 在线人数统计
-const onLineTime = ref(0)
-const someoneTimeBarData = reactive({
-    datax: ['北京师范大学附属中学', '师达', '交大分校', '本部高中', '本部初中'],
-    datay: [233, 43, 56, 43, 44]
+// ==================== 自定义日期范围 ====================
+const customDateRange = ref<[string, string] | null>(null)
+
+const handleCustomDateChange = (val: [string, string] | null) => {
+    if (val && val.length === 2) {
+        fetchChartData()
+    }
+}
+
+// ==================== 图表数据 ====================
+const timeHours = ['0时','1时','2时','3时','4时','5时','6时','7时','8时','9时','10时','11时','12时','13时','14时','15时','16时','17时','18时','19时','20时','21时','22时','23时']
+
+const mainLineChartData = reactive({
+    datax: [...timeHours],
+    datay: [[0,2,5,3,4,3,5,8,15,25,40,60,80,100,120,150,180,200,220,210,190,160,120,80]],
 })
 
-// 某个学校的当天在线人数统计 折线图数据
-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 barChartData = reactive({
+    datax: ['北京师大', '师达', '五十七', '五十七', '五十七', '一分校', '北师大', '北师大', '北师大'],
+    datay: [96, 91, 96, 96, 97, 96, 96, 96, 96],
 })
 
-// 某个学校的当天在线人数统计 表格数据
+const onLineTime = ref(0)
+const currentHourLabel = computed(() => String(onLineTime.value))
 
-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',
-    },
+const selectedSchoolName = ref('北京师大')
+
+const schoolLineChartData = reactive({
+    datax: [...timeHours],
+    datay: [[0,2,5,3,4,3,5,8,15,25,40,60,80,100,120,150,180,200,220,210,190,160,120,80]],
+})
+
+const schoolTableData = ref([
+    { num: 1, school: '北京师大', account: '18738372732', name: '黎明村', operationDetail: '系统登录', operationTime: '26-01-03 10:05:11' },
+    { num: 2, school: '北京师大', account: '18738372732', name: '严谨', operationDetail: '系统登录', operationTime: '26-01-03 10:05:11' },
+    { num: 3, school: '北京师大', account: '18738372732', name: '庄重', operationDetail: '系统登录', operationTime: '26-01-03 10:05:11' },
+    { num: 4, school: '北京师大', account: '18738372732', name: '杨晓', operationDetail: '系统登录', operationTime: '26-01-03 10:05:11' },
+    { num: 5, school: '北京师大', account: '18738372732', name: '孙赏语', operationDetail: '系统登录', operationTime: '26-01-03 10:05:11' },
+    { num: 6, school: '北京师大', account: '18738372732', name: '赵梓萱', operationDetail: '系统登录', operationTime: '26-01-03 10:05:11' },
+    { num: 7, school: '北京师大', account: '18738372732', name: '朱玉哲', operationDetail: '系统登录', operationTime: '26-01-03 10:05:11' },
 ])
 
+const currentPage = ref(1)
+const pageSize = ref(20)
+
+const pagedTableData = computed(() => {
+    const start = (currentPage.value - 1) * pageSize.value
+    const end = start + pageSize.value
+    return schoolTableData.value.slice(start, end)
+})
+
+const handleTablePageChange = (payload: { type: string; val: number }) => {
+    if (payload.type === 'currentPage') {
+        currentPage.value = payload.val
+    } else if (payload.type === 'pageSize') {
+        pageSize.value = payload.val
+        currentPage.value = 1
+    }
+}
 
-// 处理tab切换相关操作
-const defaultOnlineTypeTab = ref('teacherOnline')  //  默认的在线类型
-const defaultDateRangeTab = ref('today')  // 默认的 时间范围
-const defaultVisualizationTypeTab = ref('chart')  //   默认的 某个学校的当天在线人数 的可视化展示类型
+// ==================== 交互处理 ====================
 const HandleTabChange = (type: string, val: string) => {
     switch (type) {
+        case 'onlineType':
+            defaultOnlineTypeTab.value = val
+            fetchChartData()
+            break
+        case 'dateRange':
+            defaultDateRangeTab.value = val
+            if (val !== 'custom') {
+                customDateRange.value = null
+                fetchChartData()
+            }
+            break
         case 'visualizationType':
             defaultVisualizationTypeTab.value = val
             break
     }
 }
 
+const handleBarClick = (index: number, name: string) => {
+    selectedSchoolName.value = name
+    currentPage.value = 1
+    const base = barChartData.datay[index] || 50
+    schoolLineChartData.datay = [[
+        Math.floor(Math.random() * 5),
+        Math.floor(Math.random() * 10),
+        Math.floor(base * 0.3),
+        Math.floor(base * 0.2),
+        Math.floor(base * 0.4),
+        Math.floor(base * 0.5),
+        Math.floor(base * 0.6),
+        Math.floor(base * 0.8),
+        Math.floor(base * 1.0),
+        Math.floor(base * 1.2),
+        Math.floor(base * 1.5),
+        Math.floor(base * 1.8),
+        Math.floor(base * 2.0),
+        Math.floor(base * 2.2),
+        Math.floor(base * 2.5),
+        Math.floor(base * 2.2),
+        Math.floor(base * 2.0),
+        Math.floor(base * 1.8),
+        Math.floor(base * 1.5),
+        Math.floor(base * 1.2),
+        Math.floor(base * 1.0),
+        Math.floor(base * 0.8),
+        Math.floor(base * 0.5),
+        Math.floor(base * 0.3),
+    ]]
+    const newRows = Array.from({ length: 50 }, (_, i) => ({
+        num: i + 1,
+        school: name,
+        account: `1873837273${String(i % 10).padStart(2, '0')}`,
+        name: ['黎明村', '严谨', '庄重', '杨晓', '孙赏语', '赵梓萱', '朱玉哲', '王小明', '李华', '张伟'][i % 10],
+        operationDetail: '系统登录',
+        operationTime: `26-01-03 10:05:${String(10 + (i % 50)).padStart(2, '0')}`,
+    }))
+    schoolTableData.value = newRows
+}
 
-// 获取头部数据
+const handleExport = () => {
+    console.log('导出Excel', selectedSchoolName.value)
+}
+
+// ==================== 数据请求 ====================
 const fetchHeadData = async () => {
     try {
         const res = await getHeadData()
@@ -226,45 +323,54 @@ const fetchHeadData = async () => {
     }
 }
 
-// 获取模块列表
 const fetchModuleList = async () => {
+    console.log('[fetchModuleList] 开始请求模块列表...')
     try {
         const res = await getModuleList()
-        // console.log('getModuleList 响应数据:', res)
+        console.log('[fetchModuleList] 接口原始响应:', JSON.stringify(res))
         const data = res.data || res
-        // 筛选"在线"相关的模块,映射为 tabList 格式
-        const onlineList = (data || []).map((item: any) => ({
-                key: String(item.code),
-                label: item.name
-            }))
+        console.log('[fetchModuleList] 解析后的 data:', JSON.stringify(data))
+        if (!Array.isArray(data)) {
+            console.warn('[fetchModuleList] data 不是数组,实际类型:', typeof data, data)
+            return
+        }
+        const onlineList = data.map((item: any, idx: number) => {
+            if (!item.code || !item.name) {
+                console.warn(`[fetchModuleList] 第${idx}项缺少 code 或 name:`, item)
+            }
+            return {
+                key: String(item.code ?? ''),
+                label: String(item.name ?? ''),
+            }
+        })
+        console.log('[fetchModuleList] 映射后的 onlineList:', JSON.stringify(onlineList))
         if (onlineList.length) {
             onlineTypeTablist.value = onlineList
             defaultOnlineTypeTab.value = onlineList[0].key
+            console.log('[fetchModuleList] 已设置 onlineTypeTablist,defaultOnlineTypeTab =', onlineList[0].key)
+        } else {
+            console.warn('[fetchModuleList] onlineList 为空,未更新标签列表')
         }
+        fetchChartData()
     } catch (err) {
-        console.error('getModuleList 请求失败:', err)
+        console.error('[fetchModuleList] 请求失败:', err)
     }
 }
 
-// 获取图表数据
 const fetchChartData = async () => {
     try {
+        const timeType = dateRangeToTimeType[defaultDateRangeTab.value] ?? 1
         const res = await getChartData({
-           schoolId: 0,
-           timeType: 0,
-           moduleCode: 0,
-           startTime: '',
-           endTime: '',
+            schoolId: 0,
+            timeType,
+            moduleCode: Number(defaultOnlineTypeTab.value) || 0,
+            startTime: customDateRange.value?.[0] ?? '',
+            endTime: customDateRange.value?.[1] ?? '',
         })
-        // console.log('getChartData 响应数据:', res)
+        console.log('getChartData 响应数据:', res)
         const data = res.data || res
         if (data) {
-            
-            // somePeriodTimeLineData.datax = data.map((item: any) => item.xname)
-            // somePeriodTimeLineData.datay = data.map((item: any) => item.count)
-
-            // somePeriodTimeLineData.datax = data.xname
-            // somePeriodTimeLineData.datay = data.count
+            // TODO: 根据后端返回数据结构更新图表
         }
     } catch (err) {
         console.error('getChartData 请求失败:', err)
@@ -274,42 +380,152 @@ const fetchChartData = async () => {
 onMounted(() => {
     fetchHeadData()
     fetchModuleList()
-    fetchChartData()
 })
-
 </script>
 
 <style lang="scss" scoped>
-.cardBoard {
+:deep(.page_item.end_item) {
+    background: transparent;
+    padding: 0;
+}
+
+.stat_cards {
     display: grid;
     grid-template-columns: repeat(5, 1fr);
-    gap: 20px;
+    gap: 16px;
+
+    .stat_card {
+        height: 72px;
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        padding: 0 20px;
+        background: #fff;
+        border-radius: 10px;
+        overflow: hidden;
+        border: 1px solid #E4E4E4;
+
+        .stat_left {
+            display: flex;
+            align-items: center;
+            gap: 8px;
+
+            .stat_name {
+                font-size: 16px;
+                color: #666666;
+            }
+        }
+
+        .stat_value {
+            font-size: 30px;
+            font-weight: 500;
+            color: #333333;
+            line-height: 1;
+        }
+    }
+}
+
+.tabs_container {
+    background: #fff;
+    border-radius: 10px;
+    padding: 10px 16px;
+
+
+    .date_range_area {
+        display: flex;
+        align-items: center;
+        gap: 12px;
 
-    li {
-        background-color: #f2f2f2;
-        padding: 20px;
-        border-radius: 8px;
+        .custom_date_picker {
+           :deep(.el-input__wrapper) {
+                width: 230px !important;
+                height: 36px;
+                line-height: 36px;
+            }
+        }
+    }
+}
+
+.chart_panel {
+    background: #fff;
+    border: 1px solid #edf0f5;
+    border-radius: 8px;
+    padding: 20px;
+
+    .panel_title {
+        font-size: 16px;
+        font-weight: 600;
+        color: #303133;
+        margin-bottom: 12px;
+    }
 
-        .label {
-            font-size: 16px;
+    .panel_header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: 12px;
+
+        .panel_title {
+            margin-bottom: 0;
         }
 
-        .num {
-            font-size: 34px;
-            font-weight: 600;
+        .panel_actions {
+            display: flex;
+            align-items: center;
+            gap: 12px;
+
+            .btn_export {
+                height: 32px;
+                border: 1px solid #dcdfe6;
+                background: #fff;
+                color: #606266;
+                display: flex;
+                align-items: center;
+                gap: 6px;
+
+                .btn_icon {
+                    font-size: 14px;
+                }
+            }
+
+            .view_toggle {
+                display: flex;
+                border: 1px solid #dcdfe6;
+                border-radius: 4px;
+                overflow: hidden;
+
+                span {
+                    padding: 6px 16px;
+                    font-size: 14px;
+                    color: #606266;
+                    cursor: pointer;
+                    background: #fff;
+                    transition: all 0.2s;
+
+                    &.active {
+                        background: #2E64FA;
+                        color: #fff;
+                    }
+                }
+            }
         }
     }
 }
 
-.chart_box {
-    display: flex;
-    flex-direction: column;
-    gap: 10px;
+.bottom_section {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    gap: 16px;
 
-    .chart {
-        border: 1px solid #dcdfe6;
-        min-height: 320px;
-        padding: 20px;
+    .left_panel,
+    .right_panel {
+        min-height: 360px;
     }
 }
-</style>
+
+.table_info {
+    font-size: 13px;
+    color: #909399;
+    margin-bottom: 8px;
+}
+</style>

+ 1 - 1
src/views/performanceMonitor/tabSearch.ts

@@ -2,7 +2,7 @@ import { ref } from "vue";
 
 
 // 在线类型列表
-export const onlineTypeTablist = ref([])
+export const onlineTypeTablist = ref<{ key: string; label: string }[]>([])
 
 
 // 时间范围列表