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