提交 fd7bc1bd authored 作者: 王鹏飞's avatar 王鹏飞

feat: 对接数据可视化接口

上级 840c9345
import httpRequest from '@/utils/axios'
export function getResourceChart(params?: Record<string, any>) {
return httpRequest.get('/api/resource/v1/teaching/chart/resource', { params })
}
export function getLearningChart(params?: Record<string, any>) {
return httpRequest.get('/api/resource/v1/teaching/chart/learning', { params })
}
export function getTeachingChart(params?: Record<string, any>) {
return httpRequest.get('/api/resource/v1/teaching/chart/teaching', { params })
}
export function getOriginChart(params?: Record<string, any>) {
return httpRequest.get('/api/resource/v1/teaching/chart/origin', { params })
}
......@@ -2,13 +2,13 @@
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
import 'echarts-wordcloud'
import { User, UserFilled, Clock, Monitor, Files } from '@element-plus/icons-vue'
import { User, UserFilled, Monitor, Files } from '@element-plus/icons-vue'
import { getLearningChart } from '../api'
// 近30天学习数据
const learningStats = ref({
studySessions: 0, // 学习人次
studyUsers: 0, // 学习人数
studyDuration: 0, // 学习时长(小时)
studyCourses: 0, // 学习课程数量
studyResources: 0, // 学习资源数量
})
......@@ -21,9 +21,6 @@ const studentWordCloud = ref([])
const courseWordCloud = ref([])
const resourceWordCloud = ref([])
// 最近学习列表
const recentStudyList = ref([])
// 走势图数据
const studyTrendData = ref({
dates: [],
......@@ -41,139 +38,25 @@ const courseStudyData = ref({
values: [],
})
const durationData = ref({
dates: [],
values: [],
})
// 全国分布数据
const mapData = ref([])
// 图表实例
let behaviorChart = null
let studentWordCloudChart = null
let courseWordCloudChart = null
let resourceWordCloudChart = null
let mapChart = null
let studyTrendChart = null
let resourceStudyChart = null
let courseStudyChart = null
let durationChart = null
// 模拟数据加载
const loadData = () => {
// 近30天学习数据
learningStats.value = {
studySessions: 15680,
studyUsers: 3240,
studyDuration: 28450,
studyCourses: 180,
studyResources: 1250,
}
// 学习行为分布
behaviorData.value = [
{ name: '观看视频', value: 8560 },
{ name: '下载课件', value: 3240 },
{ name: '在线测试', value: 1890 },
{ name: '讨论互动', value: 1230 },
{ name: '作业提交', value: 760 },
]
// 学生词云数据
studentWordCloud.value = [
{ name: '计算机科学', value: 100 },
{ name: '软件工程', value: 85 },
{ name: '数据科学', value: 75 },
{ name: '人工智能', value: 70 },
{ name: '网络安全', value: 65 },
{ name: '机器学习', value: 60 },
{ name: '前端开发', value: 55 },
{ name: '后端开发', value: 50 },
{ name: '移动开发', value: 45 },
{ name: '云计算', value: 40 },
]
// 课程词云数据
courseWordCloud.value = [
{ name: 'Vue.js开发', value: 95 },
{ name: 'React框架', value: 88 },
{ name: 'Node.js后端', value: 82 },
{ name: 'Python编程', value: 78 },
{ name: 'Java基础', value: 75 },
{ name: '数据库设计', value: 70 },
{ name: '算法与数据结构', value: 68 },
{ name: '系统设计', value: 65 },
{ name: '微服务架构', value: 60 },
{ name: 'DevOps实践', value: 55 },
]
// 资源词云数据
resourceWordCloud.value = [
{ name: '教学视频', value: 90 },
{ name: 'PPT课件', value: 85 },
{ name: '实验指导', value: 80 },
{ name: '习题集', value: 75 },
{ name: '案例分析', value: 70 },
{ name: '参考书籍', value: 65 },
{ name: '代码示例', value: 60 },
{ name: '项目模板', value: 55 },
{ name: '在线工具', value: 50 },
{ name: '学习笔记', value: 45 },
]
// 最近学习列表
recentStudyList.value = [
{ course: 'Vue.js前端开发实战', student: '张三', duration: '2小时30分', time: '2024-01-15 14:30' },
{ course: 'React组件化开发', student: '李四', duration: '1小时45分', time: '2024-01-15 13:20' },
{ course: 'Node.js后端开发', student: '王五', duration: '3小时15分', time: '2024-01-15 11:45' },
{ course: 'Python数据分析', student: '赵六', duration: '2小时10分', time: '2024-01-15 10:30' },
{ course: 'Java Spring Boot', student: '钱七', duration: '1小时55分', time: '2024-01-15 09:15' },
{ course: '数据库设计与优化', student: '孙八', duration: '2小时40分', time: '2024-01-14 16:20' },
{ course: '算法与数据结构', student: '周九', duration: '3小时05分', time: '2024-01-14 14:10' },
{ course: '微服务架构设计', student: '吴十', duration: '2小时20分', time: '2024-01-14 11:30' },
{ course: 'Docker容器化', student: '郑十一', duration: '1小时50分', time: '2024-01-14 09:45' },
{ course: 'Kubernetes集群管理', student: '王十二', duration: '2小时35分', time: '2024-01-13 15:20' },
]
// 生成最近30天的走势数据
const dates = []
const sessions = []
const users = []
const resourceValues = []
const courseValues = []
const durationValues = []
const today = new Date()
for (let i = 29; i >= 0; i--) {
const date = new Date(today)
date.setDate(date.getDate() - i)
dates.push(date.toISOString().split('T')[0])
sessions.push(Math.floor(Math.random() * 200) + 300)
users.push(Math.floor(Math.random() * 50) + 80)
resourceValues.push(Math.floor(Math.random() * 30) + 20)
courseValues.push(Math.floor(Math.random() * 15) + 10)
durationValues.push(Math.floor(Math.random() * 100) + 200)
}
studyTrendData.value = { dates, sessions, users }
resourceStudyData.value = { dates, values: resourceValues }
courseStudyData.value = { dates, values: courseValues }
durationData.value = { dates, values: durationValues }
// 全国分布数据(模拟)
mapData.value = [
{ name: '北京', value: 1200 },
{ name: '上海', value: 980 },
{ name: '广东', value: 850 },
{ name: '江苏', value: 720 },
{ name: '浙江', value: 680 },
{ name: '山东', value: 560 },
{ name: '四川', value: 480 },
{ name: '湖北', value: 420 },
{ name: '河南', value: 380 },
{ name: '湖南', value: 350 },
]
const loadData = async () => {
const { data } = await getLearningChart()
learningStats.value = { ...learningStats.value, ...(data.stats || {}) }
behaviorData.value = data.behaviorData || []
studentWordCloud.value = data.studentRanking || []
courseWordCloud.value = data.courseRanking || []
resourceWordCloud.value = data.resourceRanking || []
studyTrendData.value = data.studyTrendData || { dates: [], sessions: [], users: [] }
resourceStudyData.value = data.resourceStudyData || { dates: [], values: [] }
courseStudyData.value = data.courseStudyData || { dates: [], values: [] }
}
// 初始化图表
......@@ -230,7 +113,7 @@ const initCharts = () => {
studentWordCloudChart = echarts.init(studentWordEl)
const option = {
title: {
text: '活跃学生词云',
text: '活跃学生 Top10',
left: 'center',
textStyle: { fontSize: 16 },
},
......@@ -274,7 +157,7 @@ const initCharts = () => {
courseWordCloudChart = echarts.init(courseWordEl)
const option = {
title: {
text: '热门课程词云',
text: '热门课程 Top10',
left: 'center',
textStyle: { fontSize: 16 },
},
......@@ -318,7 +201,7 @@ const initCharts = () => {
resourceWordCloudChart = echarts.init(resourceWordEl)
const option = {
title: {
text: '热门资源词云',
text: '热门资源 Top10',
left: 'center',
textStyle: { fontSize: 16 },
},
......@@ -353,55 +236,6 @@ const initCharts = () => {
resourceWordCloudChart.setOption(option)
}
// 全国分布图 - 使用柱状图代替地图
const mapEl = document.getElementById('mapChart')
console.log('mapChart元素:', mapEl)
if (mapEl) {
if (mapChart) {
mapChart.dispose()
}
mapChart = echarts.init(mapEl)
const option = {
title: {
text: '在线学生全国分布',
left: 'center',
textStyle: { fontSize: 16 },
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
xAxis: {
type: 'category',
data: mapData.value.map((item) => item.name),
axisLabel: {
rotate: 45,
},
},
yAxis: {
type: 'value',
name: '学生人数',
},
series: [
{
name: '学生分布',
type: 'bar',
data: mapData.value.map((item) => item.value),
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#83bff6' },
{ offset: 0.5, color: '#188df0' },
{ offset: 1, color: '#188df0' },
]),
},
},
],
}
mapChart.setOption(option)
}
// 学习人次人数走势图
const studyTrendEl = document.getElementById('studyTrendChart')
console.log('studyTrendChart元素:', studyTrendEl)
......@@ -585,58 +419,6 @@ const initCharts = () => {
courseStudyChart.setOption(option)
}
// 学习时长走势图
const durationEl = document.getElementById('durationChart')
if (durationEl) {
if (durationChart) {
durationChart.dispose()
}
durationChart = echarts.init(durationEl)
const option = {
title: {
text: '学习时长走势',
left: 'center',
top: '5%',
textStyle: { fontSize: 16 },
},
tooltip: {
trigger: 'axis',
},
grid: {
top: '20%',
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
data: durationData.value.dates,
axisLabel: {
rotate: 45,
},
},
yAxis: {
type: 'value',
name: '时长(小时)',
},
series: [
{
name: '学习时长',
type: 'line',
data: durationData.value.values,
smooth: true,
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 99, 132, 0.3)' },
{ offset: 1, color: 'rgba(255, 99, 132, 0.1)' },
]),
},
},
],
}
durationChart.setOption(option)
}
}
// 窗口大小改变时重新调整图表
......@@ -645,11 +427,9 @@ const handleResize = () => {
studentWordCloudChart?.resize()
courseWordCloudChart?.resize()
resourceWordCloudChart?.resize()
mapChart?.resize()
studyTrendChart?.resize()
resourceStudyChart?.resize()
courseStudyChart?.resize()
durationChart?.resize()
}
// 清理所有图表实例
......@@ -658,26 +438,22 @@ const disposeAllCharts = () => {
studentWordCloudChart?.dispose()
courseWordCloudChart?.dispose()
resourceWordCloudChart?.dispose()
mapChart?.dispose()
studyTrendChart?.dispose()
resourceStudyChart?.dispose()
courseStudyChart?.dispose()
durationChart?.dispose()
// 重置为null
behaviorChart = null
studentWordCloudChart = null
courseWordCloudChart = null
resourceWordCloudChart = null
mapChart = null
studyTrendChart = null
resourceStudyChart = null
courseStudyChart = null
durationChart = null
}
onMounted(() => {
loadData()
onMounted(async () => {
await loadData()
// 确保DOM完全渲染后再初始化图表
setTimeout(() => {
......@@ -726,16 +502,6 @@ onUnmounted(() => {
</div>
</div>
<div class="stat-card">
<div class="stat-icon duration">
<Clock />
</div>
<div class="stat-content">
<div class="stat-number">{{ learningStats.studyDuration.toLocaleString() }}</div>
<div class="stat-label">学习时长(小时)</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon courses">
<Monitor />
......@@ -777,34 +543,6 @@ onUnmounted(() => {
</div>
</div>
<!-- 全国分布 -->
<div class="distribution-section">
<h2>在线学生全国分布</h2>
<div class="charts-row">
<div class="chart-container large">
<div id="mapChart" class="chart"></div>
</div>
</div>
</div>
<!-- 最近学习列表 -->
<div class="recent-section">
<h2>最近学习列表 Top10</h2>
<div class="recent-study-list">
<div v-for="(item, index) in recentStudyList" :key="index" class="list-item">
<div class="rank">{{ index + 1 }}</div>
<div class="content">
<div class="course-name">{{ item.course }}</div>
<div class="student-info">
<span class="student">{{ item.student }}</span>
<span class="duration">{{ item.duration }}</span>
</div>
<div class="time">{{ item.time }}</div>
</div>
</div>
</div>
</div>
<!-- 走势图分析 -->
<div class="trend-section">
<h2>学习趋势分析</h2>
......@@ -822,9 +560,6 @@ onUnmounted(() => {
<div class="chart-container large">
<div id="courseStudyChart" class="chart"></div>
</div>
<div class="chart-container large">
<div id="durationChart" class="chart"></div>
</div>
</div>
</div>
</div>
......@@ -854,8 +589,6 @@ onUnmounted(() => {
.stats-section,
.analysis-section,
.distribution-section,
.recent-section,
.trend-section {
margin-bottom: 40px;
......@@ -904,7 +637,6 @@ onUnmounted(() => {
&.sessions,
&.users,
&.duration,
&.courses,
&.resources {
background: #f5ebda;
......@@ -956,72 +688,6 @@ onUnmounted(() => {
}
}
.recent-study-list {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
max-height: 500px;
overflow-y: auto;
.list-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.rank {
width: 30px;
height: 30px;
border-radius: 50%;
background: linear-gradient(135deg, #409eff, #67c23a);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 15px;
font-size: 14px;
}
.content {
flex: 1;
.course-name {
font-size: 14px;
color: #303133;
font-weight: 500;
margin-bottom: 4px;
}
.student-info {
display: flex;
justify-content: space-between;
margin-bottom: 2px;
.student {
font-size: 12px;
color: #606266;
}
.duration {
font-size: 12px;
color: #67c23a;
font-weight: 500;
}
}
.time {
font-size: 11px;
color: #909399;
}
}
}
}
}
@media (max-width: 768px) {
......
<template>
<div class="origin-container">
<!-- 页面标题 -->
<div class="header">
<h1>
<el-icon class="header-icon"><Location /></el-icon>
生源地分布可视化
</h1>
<div class="page-header">
<h1>生源地分布可视化</h1>
<p>全面了解学生来源地域分布情况与统计分析</p>
</div>
<!-- 筛选器 -->
<div class="filter-bar">
<div class="filter-group">
<el-icon class="filter-icon" style="color: var(--primary-color)"><Calendar /></el-icon>
<label>学期:</label>
<select v-model="filters.year" @change="onFilterChange">
<option value="2024">2024学年</option>
<option value="2023">2023学年</option>
<option value="2022">2022学年</option>
</select>
</div>
<div class="filter-group">
<el-icon class="filter-icon" style="color: var(--secondary-color)"><School /></el-icon>
<label>年级:</label>
<select v-model="filters.grade" @change="onFilterChange">
<option value="all">全部年级</option>
<option value="1">一年级</option>
<option value="2">二年级</option>
<option value="3">三年级</option>
<option value="4">四年级</option>
</select>
</div>
<div class="filter-group">
<el-icon class="filter-icon" style="color: var(--success-color)"><Grid /></el-icon>
<label>专业:</label>
<select v-model="filters.major" @change="onFilterChange">
<option value="all">全部专业</option>
<option value="cs">计算机科学</option>
<option value="ee">电子工程</option>
<option value="ba">工商管理</option>
<option value="med">医学</option>
</select>
</div>
</div>
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card">
......@@ -73,15 +35,6 @@
<div class="stat-label">覆盖城市</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%)">
<el-icon class="stat-icon-inner"><Location /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ stats.internationalCountries }}</div>
<div class="stat-label">国际学生国家</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%)">
<el-icon class="stat-icon-inner"><TrendCharts /></el-icon>
......@@ -112,10 +65,10 @@
</div>
</div>
<!-- 地图可视化 -->
<!-- 省份排行可视化 -->
<div class="map-section">
<h2 class="section-title">全国生源地图分布</h2>
<div ref="chinaMapChart" class="map-container"></div>
<h2 class="section-title">全国生源省份排行</h2>
<div ref="provinceRankChart" class="map-container"></div>
</div>
<!-- 详细数据表格 -->
......@@ -128,8 +81,6 @@
<th>省份</th>
<th>学生人数</th>
<th>占比</th>
<th>趋势</th>
<th>分布可视化</th>
</tr>
</thead>
<tbody>
......@@ -151,19 +102,6 @@
<td>
<strong>{{ item.percentage }}%</strong>
</td>
<td :style="{ color: getTrendColor(item.trend) }">
<el-icon class="trend-icon">
<ArrowUp v-if="item.trend.startsWith('+')" />
<ArrowDown v-else-if="item.trend.startsWith('-')" />
<Minus v-else />
</el-icon>
{{ item.trend }}
</td>
<td>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: `${item.percentage}%` }"></div>
</div>
</td>
</tr>
</tbody>
</table>
......@@ -175,27 +113,20 @@
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import {
Location,
Calendar,
School,
Grid,
User,
LocationInformation,
OfficeBuilding,
TrendCharts,
PieChart,
Histogram,
ArrowUp,
ArrowDown,
Minus,
} from '@element-plus/icons-vue'
import { getOriginChart } from '../api'
// 类型定义
interface ProvinceData {
name: string
value: number
percentage: number
trend: string
rank: number
}
......@@ -203,63 +134,41 @@ interface Stats {
totalStudents: number
provinces: number
cities: number
internationalCountries: number
localPercentage: number
}
interface Filters {
year: string
grade: string
major: string
}
// 响应式数据
const regionPieChart = ref<HTMLElement>()
const cityBarChart = ref<HTMLElement>()
const chinaMapChart = ref<HTMLElement>()
const filters = reactive<Filters>({
year: '2024',
grade: 'all',
major: 'all',
})
const provinceRankChart = ref<HTMLElement>()
const stats = reactive<Stats>({
totalStudents: 8650,
provinces: 31,
cities: 256,
internationalCountries: 12,
localPercentage: 68,
totalStudents: 0,
provinces: 0,
cities: 0,
localPercentage: 0,
})
const provinceData = ref<ProvinceData[]>([
{ name: '广东', value: 5882, percentage: 68, trend: '+5%', rank: 1 },
{ name: '湖南', value: 432, percentage: 5, trend: '+2%', rank: 2 },
{ name: '湖北', value: 389, percentage: 4.5, trend: '+3%', rank: 3 },
{ name: '江西', value: 346, percentage: 4, trend: '+1%', rank: 4 },
{ name: '河南', value: 302, percentage: 3.5, trend: '-1%', rank: 5 },
{ name: '四川', value: 259, percentage: 3, trend: '+4%', rank: 6 },
{ name: '广西', value: 216, percentage: 2.5, trend: '+2%', rank: 7 },
{ name: '福建', value: 173, percentage: 2, trend: '0%', rank: 8 },
{ name: '浙江', value: 156, percentage: 1.8, trend: '+1%', rank: 9 },
{ name: '江苏', value: 138, percentage: 1.6, trend: '+2%', rank: 10 },
{ name: '其他', value: 357, percentage: 4.1, trend: '+1%', rank: 11 },
])
const provinceData = ref<ProvinceData[]>([])
const regionData = ref<{ name: string; value: number }[]>([])
const cityData = ref<{ name: string; value: number }[]>([])
// 图表实例
let regionPieChartInstance: echarts.ECharts | null = null
let cityBarChartInstance: echarts.ECharts | null = null
let chinaMapChartInstance: echarts.ECharts | null = null
let provinceRankChartInstance: echarts.ECharts | null = null
// 工具函数
const getRankClass = (rank: number): string => {
return rank <= 3 ? `rank-${rank}` : 'rank-other'
}
const getTrendColor = (trend: string): string => {
if (trend.startsWith('+')) return '#10b981'
if (trend.startsWith('-')) return '#ef4444'
return '#6b7280'
const loadData = async () => {
const { data } = await getOriginChart()
Object.assign(stats, data.stats || {})
provinceData.value = data.provinceData || []
regionData.value = data.regionData || []
cityData.value = data.cityData || []
}
// 初始化区域分布饼图
......@@ -301,14 +210,7 @@ const initRegionPieChart = () => {
fontWeight: 'bold',
},
},
data: [
{ value: 5882, name: '省内' },
{ value: 1214, name: '华中地区' },
{ value: 518, name: '西南地区' },
{ value: 432, name: '华东地区' },
{ value: 389, name: '华北地区' },
{ value: 215, name: '其他地区' },
],
data: regionData.value,
},
],
}
......@@ -337,14 +239,14 @@ const initCityBarChart = () => {
},
yAxis: {
type: 'category',
data: ['惠州', '东莞', '佛山', '中山', '珠海', '江门', '肇庆', '汕头', '湛江', '深圳'],
data: cityData.value.map((item) => item.name),
axisLabel: { fontSize: 11 },
},
series: [
{
name: '学生人数',
type: 'bar',
data: [856, 923, 1245, 678, 534, 489, 412, 367, 325, 1053],
data: cityData.value.map((item) => item.value),
itemStyle: {
borderRadius: [0, 8, 8, 0],
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
......@@ -363,134 +265,14 @@ const initCityBarChart = () => {
cityBarChartInstance.setOption(option)
}
// 初始化中国地图
const initChinaMap = async () => {
if (!chinaMapChart.value) return
chinaMapChartInstance = echarts.init(chinaMapChart.value)
const mapData = [
{ name: '广东', value: 5882 },
{ name: '湖南', value: 432 },
{ name: '湖北', value: 389 },
{ name: '江西', value: 346 },
{ name: '河南', value: 302 },
{ name: '四川', value: 259 },
{ name: '广西', value: 216 },
{ name: '福建', value: 173 },
{ name: '浙江', value: 156 },
{ name: '江苏', value: 138 },
{ name: '安徽', value: 125 },
{ name: '山东', value: 112 },
{ name: '重庆', value: 98 },
{ name: '贵州', value: 87 },
{ name: '云南', value: 76 },
]
// 初始化省份排行图
const initProvinceRankChart = () => {
if (!provinceRankChart.value) return
const option = {
title: {
text: '全国生源分布热力图',
left: 'center',
top: 20,
textStyle: {
fontSize: 16,
color: '#2C3E50',
},
},
tooltip: {
trigger: 'item',
formatter: function (params: any) {
if (params.value) {
return params.name + '<br/>学生人数: ' + params.value + '人'
}
return params.name + '<br/>暂无数据'
},
},
visualMap: {
min: 0,
max: 6000,
text: ['高', '低'],
realtime: false,
calculable: true,
inRange: {
color: ['#fef2f2', '#fecaca', '#f87171', '#dc2626', '#C21F30'],
},
bottom: 50,
left: 20,
},
series: [
{
name: '生源分布',
type: 'scatter',
coordinateSystem: 'geo',
data: [
{ name: '广东', value: [113.23, 23.16, 5882] },
{ name: '湖南', value: [112.59, 28.21, 432] },
{ name: '湖北', value: [114.31, 30.52, 389] },
{ name: '江西', value: [115.89, 28.68, 346] },
{ name: '河南', value: [113.65, 34.76, 302] },
{ name: '四川', value: [104.06, 30.67, 259] },
{ name: '广西', value: [108.33, 22.84, 216] },
{ name: '福建', value: [119.3, 26.08, 173] },
{ name: '浙江', value: [120.19, 30.26, 156] },
{ name: '江苏', value: [118.78, 32.04, 138] },
],
symbolSize: function (val: number[]) {
return Math.sqrt(val[2]) / 2
},
itemStyle: {
color: '#C21F30',
shadowBlur: 10,
shadowColor: 'rgba(194, 31, 48, 0.5)',
},
label: {
formatter: '{b}',
position: 'top',
show: false,
},
emphasis: {
label: {
show: true,
},
},
},
],
geo: {
map: 'china',
roam: true,
scaleLimit: {
min: 1,
max: 3,
},
label: {
show: false,
},
itemStyle: {
areaColor: '#f3f3f3',
borderColor: '#ccc',
},
emphasis: {
itemStyle: {
areaColor: '#e6f2ff',
},
label: {
show: true,
color: '#333',
},
},
},
}
provinceRankChartInstance = echarts.init(provinceRankChart.value)
try {
// 尝试加载中国地图数据
const response = await fetch('https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json')
const chinaJson = await response.json()
echarts.registerMap('china', chinaJson)
chinaMapChartInstance.setOption(option)
} catch (error) {
// 如果加载失败,显示备用图表
console.log('地图加载失败,使用备用可视化')
const backupOption = {
const mapData = provinceData.value
const option = {
title: {
text: '省份生源排行',
left: 'center',
......@@ -538,30 +320,34 @@ const initChinaMap = async () => {
},
],
}
chinaMapChartInstance.setOption(backupOption)
}
provinceRankChartInstance.setOption(option)
}
// 筛选器变化处理
const onFilterChange = () => {
console.log('筛选器变化:', filters)
// 这里可以添加数据刷新逻辑
// 例如:重新获取数据、更新图表等
const renderCharts = async () => {
regionPieChartInstance?.dispose()
cityBarChartInstance?.dispose()
provinceRankChartInstance?.dispose()
regionPieChartInstance = null
cityBarChartInstance = null
provinceRankChartInstance = null
await nextTick()
initRegionPieChart()
initCityBarChart()
initProvinceRankChart()
}
// 窗口大小变化处理
const handleResize = () => {
regionPieChartInstance?.resize()
cityBarChartInstance?.resize()
chinaMapChartInstance?.resize()
provinceRankChartInstance?.resize()
}
// 生命周期
onMounted(async () => {
await loadData()
await nextTick()
initRegionPieChart()
initCityBarChart()
await initChinaMap()
await renderCharts()
window.addEventListener('resize', handleResize)
})
......@@ -569,7 +355,7 @@ onMounted(async () => {
onUnmounted(() => {
regionPieChartInstance?.dispose()
cityBarChartInstance?.dispose()
chinaMapChartInstance?.dispose()
provinceRankChartInstance?.dispose()
window.removeEventListener('resize', handleResize)
})
</script>
......@@ -593,34 +379,20 @@ onUnmounted(() => {
--shadow-hover: 0 10px 25px rgba(0, 0, 0, 0.1);
}
.header {
.page-header {
text-align: center;
margin-bottom: 2rem;
padding: 2rem;
background: white;
border-radius: 16px;
box-shadow: var(--shadow);
margin-bottom: 30px;
}
.header h1 {
font-size: 2rem;
.page-header h1 {
font-size: 28px;
color: var(--dark-color);
margin-bottom: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
.header-icon {
color: var(--primary-color);
font-size: 2.5rem;
margin-right: 0.5rem;
margin-bottom: 10px;
}
.header p {
.page-header p {
color: #7f8c8d;
font-size: 1rem;
font-size: 16px;
}
.stats-grid {
......@@ -853,64 +625,12 @@ onUnmounted(() => {
box-shadow: 0 1px 3px rgba(194, 31, 48, 0.3);
}
.filter-bar {
background: white;
padding: 1.5rem;
border-radius: 12px;
box-shadow: var(--shadow);
margin-bottom: 2rem;
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.filter-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.filter-icon {
font-size: 1.1rem;
}
.filter-group label {
color: var(--dark-color);
font-weight: 500;
font-size: 0.95rem;
}
.filter-group select {
padding: 0.5rem 1rem;
border: 2px solid var(--border-color);
border-radius: 8px;
font-size: 0.95rem;
color: var(--dark-color);
background: white;
cursor: pointer;
transition: all 0.3s;
min-width: 120px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.filter-group select:hover {
border-color: var(--primary-color);
box-shadow: 0 2px 6px rgba(194, 31, 48, 0.15);
}
.filter-group select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(194, 31, 48, 0.1), 0 2px 6px rgba(194, 31, 48, 0.15);
}
@media (max-width: 768px) {
.origin-container {
padding: 1rem;
}
.header h1 {
.page-header h1 {
font-size: 1.5rem;
}
......
......@@ -2,6 +2,7 @@
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
import { VideoCamera, Suitcase, FolderOpened, Files, Monitor } from '@element-plus/icons-vue'
import { getResourceChart } from '../api'
// 统计数据
const statsData = ref({
......@@ -43,68 +44,14 @@ let electiveTypeChart = null
let courseTrendChart = null
let videoUploadTrendChart = null
// 模拟数据加载
const loadData = () => {
// 基础统计数据
statsData.value = {
videoCount: 1250,
coursewareCount: 890,
lessonPlanCount: 650,
otherResourceCount: 320,
courseCount: 180,
todayNewVideo: 15,
todayNewCourseware: 8,
todayNewLessonPlan: 12,
todayNewOtherResource: 5,
todayNewCourse: 3,
}
// 资源类型占比数据
resourceTypeData.value = [
{ name: '视频资源', value: 1250 },
{ name: '课件资源', value: 890 },
{ name: '教案资源', value: 650 },
{ name: '其他资源', value: 320 },
]
// 课程类型占比数据
courseTypeData.value = [
{ name: '必修课', value: 120 },
{ name: '选修课', value: 45 },
{ name: '实践课', value: 10 },
{ name: '理论课', value: 5 },
]
// 选课类型占比数据
electiveTypeData.value = [
{ name: '公共选修', value: 25 },
{ name: '专业选修', value: 15 },
{ name: '通识选修', value: 5 },
]
// 生成最近30天的走势数据
const dates = []
const courseValues = []
const videoValues = []
const today = new Date()
for (let i = 29; i >= 0; i--) {
const date = new Date(today)
date.setDate(date.getDate() - i)
dates.push(date.toISOString().split('T')[0])
courseValues.push(Math.floor(Math.random() * 10) + 1)
videoValues.push(Math.floor(Math.random() * 20) + 5)
}
courseTrendData.value = {
dates,
values: courseValues,
}
videoUploadTrendData.value = {
dates,
values: videoValues,
}
const loadData = async () => {
const { data } = await getResourceChart()
statsData.value = { ...statsData.value, ...(data.stats || {}) }
resourceTypeData.value = data.resourceTypeData || []
courseTypeData.value = data.courseTypeData || []
electiveTypeData.value = data.electiveTypeData || []
courseTrendData.value = data.courseTrendData || { dates: [], values: [] }
videoUploadTrendData.value = data.videoUploadTrendData || { dates: [], values: [] }
}
// 初始化图表
......@@ -359,8 +306,8 @@ const disposeAllCharts = () => {
videoUploadTrendChart = null
}
onMounted(() => {
loadData()
onMounted(async () => {
await loadData()
setTimeout(() => {
initCharts()
}, 100)
......
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { nextTick, ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
import { User, School, Trophy, TrendCharts, Monitor, Document } from '@element-plus/icons-vue'
import { getTeachingChart } from '../api'
// 教学数据统计
const teachingStats = ref({
......@@ -33,48 +34,46 @@ let scoreDistributionChart = null
let subjectScoreChart = null
let scoreTrendChart = null
// 加载模拟数据
const loadData = () => {
// 教学数据统计
teachingStats.value = {
totalTeachers: 128,
totalStudents: 2850,
totalCourses: 76,
totalClasses: 38,
avgScore: 87.2,
completionRate: 94.5,
}
// 学习进度数据
learningProgressData.value = {
students: ['计算机1班', '数学1班', '物理1班', '化学1班', '英语1班', '语文1班'],
progress: [92, 88, 85, 90, 87, 83],
}
// 成绩分析数据
scoreAnalysisData.value = {
distribution: [
{ name: '优秀(90-100)', value: 35, count: 998 },
{ name: '良好(80-89)', value: 40, count: 1140 },
{ name: '中等(70-79)', value: 20, count: 570 },
{ name: '及格(60-69)', value: 4, count: 114 },
{ name: '不及格(<60)', value: 1, count: 28 },
],
subjects: [
{ name: '数学', score: 89.5 },
{ name: '英语', score: 85.2 },
{ name: '计算机', score: 92.8 },
{ name: '物理', score: 87.6 },
{ name: '化学', score: 88.9 },
{ name: '语文', score: 84.3 },
],
trends: {
months: ['1月', '2月', '3月', '4月', '5月', '6月'],
scores: [85.2, 86.8, 87.5, 88.1, 87.9, 87.2],
},
}
const loadData = async () => {
const { data } = await getTeachingChart()
teachingStats.value = { ...teachingStats.value, ...(data.stats || {}) }
learningProgressData.value = data.learningProgressData || { students: [], progress: [] }
scoreAnalysisData.value = data.scoreAnalysisData || { distribution: [], subjects: [], trends: { months: [], scores: [] } }
}
const toChartNumber = (value) => Number(value) || 0
const roundedMax = (value, minValue) => {
const current = Math.max(toChartNumber(value), minValue)
const magnitude = 10 ** Math.max(0, Math.floor(Math.log10(current)) - 1)
return Math.ceil((current * 1.2) / magnitude) * magnitude
}
const overviewMetrics = () => [
{
name: '教师数量',
value: toChartNumber(teachingStats.value.totalTeachers),
max: roundedMax(teachingStats.value.totalTeachers, 10),
},
{
name: '学生数量',
value: toChartNumber(teachingStats.value.totalStudents),
max: roundedMax(teachingStats.value.totalStudents, 100),
},
{
name: '课程数量',
value: toChartNumber(teachingStats.value.totalCourses),
max: roundedMax(teachingStats.value.totalCourses, 10),
},
{
name: '班级数量',
value: toChartNumber(teachingStats.value.totalClasses),
max: roundedMax(teachingStats.value.totalClasses, 10),
},
{ name: '平均成绩', value: toChartNumber(teachingStats.value.avgScore), max: 100 },
{ name: '完成率', value: toChartNumber(teachingStats.value.completionRate), max: 100 },
]
// 初始化图表
const initCharts = () => {
// 教学数据总览
......@@ -93,16 +92,9 @@ const initCharts = () => {
},
tooltip: {},
radar: {
indicator: [
{ name: '教师数量', max: 150 },
{ name: '学生数量', max: 3000 },
{ name: '课程数量', max: 100 },
{ name: '班级数量', max: 50 },
{ name: '平均成绩', max: 100 },
{ name: '完成率', max: 100 },
],
indicator: overviewMetrics().map(({ name, max }) => ({ name, max })),
center: ['50%', '60%'],
radius: '70%',
radius: '62%',
},
series: [
{
......@@ -110,14 +102,7 @@ const initCharts = () => {
type: 'radar',
data: [
{
value: [
teachingStats.value.totalTeachers,
teachingStats.value.totalStudents,
teachingStats.value.totalCourses,
teachingStats.value.totalClasses,
teachingStats.value.avgScore,
teachingStats.value.completionRate,
],
value: overviewMetrics().map(({ value }) => value),
name: '当前数据',
areaStyle: { color: 'rgba(64, 158, 255, 0.3)' },
},
......@@ -232,7 +217,7 @@ const initCharts = () => {
scoreDistributionChart.setOption(option)
}
// 学科成绩柱状图
// 课程成绩柱状图
const subjectEl = document.getElementById('subjectScoreChart')
if (subjectEl) {
if (subjectScoreChart) {
......@@ -241,7 +226,7 @@ const initCharts = () => {
subjectScoreChart = echarts.init(subjectEl)
const option = {
title: {
text: '各学科平均成绩',
text: '课程平均成绩 Top10',
left: 'center',
top: '5%',
textStyle: { fontSize: 16 },
......@@ -359,11 +344,10 @@ const disposeAllCharts = () => {
scoreTrendChart = null
}
onMounted(() => {
loadData()
setTimeout(() => {
onMounted(async () => {
await loadData()
await nextTick()
initCharts()
}, 500)
window.addEventListener('resize', handleResize)
})
......
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论