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

feat: 新增数据画像

上级 1f0c5c61
VITE_LOGIN_URL=http://172.16.3.203:1001/auth/login/index VITE_LOGIN_URL=http://172.16.152.125:1001/auth/login/index
VITE_QA_CENTER_URL=http://172.16.3.203:1004 VITE_QA_CENTER_URL=http://172.16.152.125:1004
VITE_BI_URL=http://172.16.3.203:1012 VITE_BI_URL=http://172.16.152.125:1012
VITE_STATIC_URL=https://saas-lab-api VITE_STATIC_URL=https://saas-lab-api
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
"blueimp-md5": "^2.19.0", "blueimp-md5": "^2.19.0",
"dayjs": "^1.11.3", "dayjs": "^1.11.3",
"echarts": "^5.3.2", "echarts": "^5.3.2",
"echarts-wordcloud": "^2.1.0",
"element-plus": "^2.2.9", "element-plus": "^2.2.9",
"js-base64": "^3.7.7", "js-base64": "^3.7.7",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
...@@ -1789,6 +1790,15 @@ ...@@ -1789,6 +1790,15 @@
"zrender": "5.3.1" "zrender": "5.3.1"
} }
}, },
"node_modules/echarts-wordcloud": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/echarts-wordcloud/-/echarts-wordcloud-2.1.0.tgz",
"integrity": "sha512-Kt1JmbcROgb+3IMI48KZECK2AP5lG6bSsOEs+AsuwaWJxQom31RTNd6NFYI01E/YaI1PFZeueaupjlmzSQasjQ==",
"license": "ISC",
"peerDependencies": {
"echarts": "^5.0.1"
}
},
"node_modules/echarts/node_modules/tslib": { "node_modules/echarts/node_modules/tslib": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
...@@ -6610,6 +6620,12 @@ ...@@ -6610,6 +6620,12 @@
} }
} }
}, },
"echarts-wordcloud": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/echarts-wordcloud/-/echarts-wordcloud-2.1.0.tgz",
"integrity": "sha512-Kt1JmbcROgb+3IMI48KZECK2AP5lG6bSsOEs+AsuwaWJxQom31RTNd6NFYI01E/YaI1PFZeueaupjlmzSQasjQ==",
"requires": {}
},
"ee-first": { "ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
......
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
"blueimp-md5": "^2.19.0", "blueimp-md5": "^2.19.0",
"dayjs": "^1.11.3", "dayjs": "^1.11.3",
"echarts": "^5.3.2", "echarts": "^5.3.2",
"echarts-wordcloud": "^2.1.0",
"element-plus": "^2.2.9", "element-plus": "^2.2.9",
"js-base64": "^3.7.7", "js-base64": "^3.7.7",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
......
...@@ -196,26 +196,28 @@ export const menus: IMenuItem[] = [ ...@@ -196,26 +196,28 @@ export const menus: IMenuItem[] = [
tag: '', tag: '',
icon: DataAnalysis, icon: DataAnalysis,
name: '课程资源数据画像', name: '课程资源数据画像',
path: path: '/teach/chart/resource',
import.meta.env.VITE_BI_URL + // path:
'/bi/?proc=1&action=viewer&hback=true&isInPreview=true&db=!7d2b!!8346!!6559!!80b2!e-SaaS!2f!!8d44!!6e90!!6570!!636e!!753b!!50cf!.db&platform=PC&browserType=chrome', // import.meta.env.VITE_BI_URL +
// '/bi/?proc=1&action=viewer&hback=true&isInPreview=true&db=!7d2b!!8346!!6559!!80b2!e-SaaS!2f!!8d44!!6e90!!6570!!636e!!753b!!50cf!.db&platform=PC&browserType=chrome',
}, },
{ {
tag: '', tag: '',
icon: DataAnalysis, icon: DataAnalysis,
name: '在线学习数据画像', name: '在线学习数据画像',
path: path: '/teach/chart/learning',
import.meta.env.VITE_BI_URL + // path:
'/bi/?proc=1&action=viewer&hback=true&isInPreview=true&db=!7d2b!!8346!!6559!!80b2!e-SaaS!2f!!5b66!!4e60!!884c!!4e3a!!753b!!50cf!.db&platform=PC&browserType=chrome', // import.meta.env.VITE_BI_URL +
}, // '/bi/?proc=1&action=viewer&hback=true&isInPreview=true&db=!7d2b!!8346!!6559!!80b2!e-SaaS!2f!!5b66!!4e60!!884c!!4e3a!!753b!!50cf!.db&platform=PC&browserType=chrome',
{ },
tag: '', // {
icon: DataAnalysis, // tag: '',
name: '生源地分布', // icon: DataAnalysis,
path: // name: '生源地分布',
import.meta.env.VITE_BI_URL + // path:
'/bi/?proc=1&action=viewer&hback=true&isInPreview=true&db=!7d2b!!8346!!6559!!80b2!e-SaaS!2f!!751f!!6e90!!5730!!5206!!5e03!.db&platform=PC&browserType=chrome', // import.meta.env.VITE_BI_URL +
}, // '/bi/?proc=1&action=viewer&hback=true&isInPreview=true&db=!7d2b!!8346!!6559!!80b2!e-SaaS!2f!!751f!!6e90!!5730!!5206!!5e03!.db&platform=PC&browserType=chrome',
// },
], ],
}, },
] ]
...@@ -11,7 +11,7 @@ const router = useRouter() ...@@ -11,7 +11,7 @@ const router = useRouter()
const route = useRoute() const route = useRoute()
const menuList = $computed<IMenuItem[]>(() => { const menuList = $computed<IMenuItem[]>(() => {
const found = menus.find(item => route.fullPath.includes(item.path)) const found = menus.find(item => route.fullPath.startsWith(item.path))
return found?.children || [] return found?.children || []
}) })
const defaultActive = computed(() => { const defaultActive = computed(() => {
......
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/teach/chart',
component: AppLayout,
children: [
{ path: 'resource', component: () => import('./views/Resource.vue') },
{ path: 'learning', component: () => import('./views/Learning.vue') },
],
},
]
<script setup>
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'
// 近30天学习数据
const learningStats = ref({
studySessions: 0, // 学习人次
studyUsers: 0, // 学习人数
studyDuration: 0, // 学习时长(小时)
studyCourses: 0, // 学习课程数量
studyResources: 0, // 学习资源数量
})
// 学习行为分布数据
const behaviorData = ref([])
// 词云数据
const studentWordCloud = ref([])
const courseWordCloud = ref([])
const resourceWordCloud = ref([])
// 最近学习列表
const recentStudyList = ref([])
// 走势图数据
const studyTrendData = ref({
dates: [],
sessions: [],
users: [],
})
const resourceStudyData = ref({
dates: [],
values: [],
})
const courseStudyData = ref({
dates: [],
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 initCharts = () => {
console.log('开始初始化图表...')
// 学习行为分布图
const behaviorEl = document.getElementById('behaviorChart')
console.log('behaviorChart元素:', behaviorEl)
if (behaviorEl) {
if (behaviorChart) {
behaviorChart.dispose()
}
behaviorChart = echarts.init(behaviorEl)
const option = {
title: {
text: '学习行为分布',
left: 'center',
textStyle: { fontSize: 16 },
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)',
},
legend: {
orient: 'vertical',
left: 'left',
},
series: [
{
name: '学习行为',
type: 'pie',
radius: '50%',
data: behaviorData.value,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
}
behaviorChart.setOption(option)
}
// 学生词云
const studentWordEl = document.getElementById('studentWordCloud')
if (studentWordEl) {
if (studentWordCloudChart) {
studentWordCloudChart.dispose()
}
studentWordCloudChart = echarts.init(studentWordEl)
const option = {
title: {
text: '活跃学生词云',
left: 'center',
textStyle: { fontSize: 16 },
},
series: [
{
type: 'wordCloud',
gridSize: 2,
sizeRange: [12, 50],
rotationRange: [-90, 90],
shape: 'pentagon',
width: '100%',
height: '100%',
textStyle: {
fontFamily: 'sans-serif',
fontWeight: 'bold',
color: function () {
return (
'rgb(' +
[
Math.round(Math.random() * 255),
Math.round(Math.random() * 255),
Math.round(Math.random() * 255),
].join(',') +
')'
)
},
},
data: studentWordCloud.value,
},
],
}
studentWordCloudChart.setOption(option)
}
// 课程词云
const courseWordEl = document.getElementById('courseWordCloud')
if (courseWordEl) {
if (courseWordCloudChart) {
courseWordCloudChart.dispose()
}
courseWordCloudChart = echarts.init(courseWordEl)
const option = {
title: {
text: '热门课程词云',
left: 'center',
textStyle: { fontSize: 16 },
},
series: [
{
type: 'wordCloud',
gridSize: 2,
sizeRange: [12, 50],
rotationRange: [-90, 90],
shape: 'circle',
width: '100%',
height: '100%',
textStyle: {
fontFamily: 'sans-serif',
fontWeight: 'bold',
color: function () {
return (
'rgb(' +
[
Math.round(Math.random() * 255),
Math.round(Math.random() * 255),
Math.round(Math.random() * 255),
].join(',') +
')'
)
},
},
data: courseWordCloud.value,
},
],
}
courseWordCloudChart.setOption(option)
}
// 资源词云
const resourceWordEl = document.getElementById('resourceWordCloud')
if (resourceWordEl) {
if (resourceWordCloudChart) {
resourceWordCloudChart.dispose()
}
resourceWordCloudChart = echarts.init(resourceWordEl)
const option = {
title: {
text: '热门资源词云',
left: 'center',
textStyle: { fontSize: 16 },
},
series: [
{
type: 'wordCloud',
gridSize: 2,
sizeRange: [12, 50],
rotationRange: [-90, 90],
shape: 'diamond',
width: '100%',
height: '100%',
textStyle: {
fontFamily: 'sans-serif',
fontWeight: 'bold',
color: function () {
return (
'rgb(' +
[
Math.round(Math.random() * 255),
Math.round(Math.random() * 255),
Math.round(Math.random() * 255),
].join(',') +
')'
)
},
},
data: resourceWordCloud.value,
},
],
}
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)
if (studyTrendEl) {
if (studyTrendChart) {
studyTrendChart.dispose()
}
studyTrendChart = echarts.init(studyTrendEl)
const option = {
title: {
text: '在线学习人次人数走势',
left: 'center',
top: '5%',
textStyle: { fontSize: 16 },
},
tooltip: {
trigger: 'axis',
},
legend: {
data: ['学习人次', '学习人数'],
top: '15%',
},
grid: {
top: '25%',
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
data: studyTrendData.value.dates,
axisLabel: {
rotate: 45,
},
},
yAxis: [
{
type: 'value',
name: '人次',
position: 'left',
},
{
type: 'value',
name: '人数',
position: 'right',
},
],
series: [
{
name: '学习人次',
type: 'line',
data: studyTrendData.value.sessions,
smooth: true,
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(64, 158, 255, 0.3)' },
{ offset: 1, color: 'rgba(64, 158, 255, 0.1)' },
]),
},
},
{
name: '学习人数',
type: 'line',
yAxisIndex: 1,
data: studyTrendData.value.users,
smooth: true,
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(103, 194, 58, 0.3)' },
{ offset: 1, color: 'rgba(103, 194, 58, 0.1)' },
]),
},
},
],
}
studyTrendChart.setOption(option)
}
// 资源学习数量走势图
const resourceStudyEl = document.getElementById('resourceStudyChart')
if (resourceStudyEl) {
if (resourceStudyChart) {
resourceStudyChart.dispose()
}
resourceStudyChart = echarts.init(resourceStudyEl)
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: resourceStudyData.value.dates,
axisLabel: {
rotate: 45,
},
},
yAxis: {
type: 'value',
},
series: [
{
name: '资源学习数量',
type: 'bar',
data: resourceStudyData.value.values,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#83bff6' },
{ offset: 0.5, color: '#188df0' },
{ offset: 1, color: '#188df0' },
]),
},
},
],
}
resourceStudyChart.setOption(option)
}
// 课程学习数量走势图
const courseStudyEl = document.getElementById('courseStudyChart')
if (courseStudyEl) {
if (courseStudyChart) {
courseStudyChart.dispose()
}
courseStudyChart = echarts.init(courseStudyEl)
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: courseStudyData.value.dates,
axisLabel: {
rotate: 45,
},
},
yAxis: {
type: 'value',
},
series: [
{
name: '课程学习数量',
type: 'bar',
data: courseStudyData.value.values,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#f7ba2c' },
{ offset: 0.5, color: '#ff9f7f' },
{ offset: 1, color: '#ff9f7f' },
]),
},
},
],
}
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)
}
}
// 窗口大小改变时重新调整图表
const handleResize = () => {
behaviorChart?.resize()
studentWordCloudChart?.resize()
courseWordCloudChart?.resize()
resourceWordCloudChart?.resize()
mapChart?.resize()
studyTrendChart?.resize()
resourceStudyChart?.resize()
courseStudyChart?.resize()
durationChart?.resize()
}
// 清理所有图表实例
const disposeAllCharts = () => {
behaviorChart?.dispose()
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()
// 确保DOM完全渲染后再初始化图表
setTimeout(() => {
console.log('DOM已加载,开始初始化图表')
initCharts()
}, 500)
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
// 组件销毁时清理所有图表实例
disposeAllCharts()
window.removeEventListener('resize', handleResize)
})
</script>
<template>
<div class="learning-portrait">
<div class="page-header">
<h1>在线学习数据画像</h1>
<p>全面了解在线学习情况和学习行为分析</p>
</div>
<!-- 近30天学习数据统计 -->
<div class="stats-section">
<h2>近30天学习数据统计</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon sessions">
<User />
</div>
<div class="stat-content">
<div class="stat-number">{{ learningStats.studySessions.toLocaleString() }}</div>
<div class="stat-label">学习人次</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon users">
<UserFilled />
</div>
<div class="stat-content">
<div class="stat-number">{{ learningStats.studyUsers.toLocaleString() }}</div>
<div class="stat-label">学习人数</div>
</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 />
</div>
<div class="stat-content">
<div class="stat-number">{{ learningStats.studyCourses }}</div>
<div class="stat-label">学习课程数量</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon resources">
<Files />
</div>
<div class="stat-content">
<div class="stat-number">{{ learningStats.studyResources.toLocaleString() }}</div>
<div class="stat-label">学习资源数量</div>
</div>
</div>
</div>
</div>
<!-- 学习行为分析 -->
<div class="analysis-section">
<h2>学习行为分析</h2>
<div class="charts-row">
<div class="chart-container">
<div id="behaviorChart" class="chart"></div>
</div>
<div class="chart-container">
<div id="studentWordCloud" class="chart"></div>
</div>
<div class="chart-container">
<div id="courseWordCloud" class="chart"></div>
</div>
<div class="chart-container">
<div id="resourceWordCloud" class="chart"></div>
</div>
</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>
<div class="charts-row">
<div class="chart-container large">
<div id="studyTrendChart" class="chart"></div>
</div>
<div class="chart-container large">
<div id="resourceStudyChart" class="chart"></div>
</div>
</div>
<div class="charts-row">
<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>
</template>
<style lang="scss" scoped>
.learning-portrait {
padding: 20px;
background-color: #f8f8f8;
min-height: 100vh;
.page-header {
text-align: center;
margin-bottom: 30px;
h1 {
font-size: 28px;
color: #303133;
margin-bottom: 10px;
}
p {
font-size: 16px;
color: #606266;
}
}
.stats-section,
.analysis-section,
.distribution-section,
.recent-section,
.trend-section {
margin-bottom: 40px;
h2 {
font-size: 20px;
color: #303133;
margin-bottom: 20px;
padding-left: 10px;
border-left: 4px solid #409eff;
}
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.stat-card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
transition: transform 0.3s ease;
&:hover {
transform: translateY(-2px);
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
svg {
width: 24px;
height: 24px;
color: #ba8b45;
}
&.sessions,
&.users,
&.duration,
&.courses,
&.resources {
background: #f5ebda;
}
}
.stat-content {
flex: 1;
.stat-number {
font-size: 32px;
font-weight: bold;
color: #303133;
line-height: 1;
margin-bottom: 5px;
}
.stat-label {
font-size: 14px;
color: #909399;
}
}
}
.charts-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
.chart-container {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
&.large {
grid-column: span 2;
}
.chart {
width: 100%;
height: 300px;
}
}
.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) {
.learning-portrait {
padding: 10px;
.stats-grid {
grid-template-columns: 1fr;
}
.charts-row {
grid-template-columns: 1fr;
.chart-container.large {
grid-column: span 1;
}
}
}
}
</style>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
import { VideoCamera, Suitcase, FolderOpened, Files, Monitor } from '@element-plus/icons-vue'
// 统计数据
const statsData = ref({
// 基础数据
videoCount: 0,
coursewareCount: 0,
lessonPlanCount: 0,
otherResourceCount: 0,
courseCount: 0,
// 今日新增数据
todayNewVideo: 0,
todayNewCourseware: 0,
todayNewLessonPlan: 0,
todayNewOtherResource: 0,
todayNewCourse: 0,
})
// 图表数据
const resourceTypeData = ref([])
const courseTypeData = ref([])
const electiveTypeData = ref([])
// 走势图数据
const courseTrendData = ref({
dates: [],
values: [],
})
const videoUploadTrendData = ref({
dates: [],
values: [],
})
// 图表实例
let resourceTypeChart = null
let courseTypeChart = null
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 initCharts = () => {
// 资源类型占比图
const resourceTypeEl = document.getElementById('resourceTypeChart')
if (resourceTypeEl) {
if (resourceTypeChart) {
resourceTypeChart.dispose()
}
resourceTypeChart = echarts.init(resourceTypeEl)
const option = {
title: {
text: '资源类型占比',
left: 'center',
textStyle: { fontSize: 16 },
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)',
},
legend: {
orient: 'vertical',
left: 'left',
},
series: [
{
name: '资源类型',
type: 'pie',
radius: '50%',
data: resourceTypeData.value,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
}
resourceTypeChart.setOption(option)
}
// 课程类型占比图
const courseTypeEl = document.getElementById('courseTypeChart')
if (courseTypeEl) {
if (courseTypeChart) {
courseTypeChart.dispose()
}
courseTypeChart = echarts.init(courseTypeEl)
const option = {
title: {
text: '课程类型占比',
left: 'center',
textStyle: { fontSize: 16 },
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)',
},
legend: {
orient: 'vertical',
left: 'left',
},
series: [
{
name: '课程类型',
type: 'pie',
radius: '50%',
data: courseTypeData.value,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
}
courseTypeChart.setOption(option)
}
// 选课类型占比图
const electiveTypeEl = document.getElementById('electiveTypeChart')
if (electiveTypeEl) {
if (electiveTypeChart) {
electiveTypeChart.dispose()
}
electiveTypeChart = echarts.init(electiveTypeEl)
const option = {
title: {
text: '选课类型占比',
left: 'center',
textStyle: { fontSize: 16 },
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)',
},
legend: {
orient: 'vertical',
left: 'left',
},
series: [
{
name: '选课类型',
type: 'pie',
radius: '50%',
data: electiveTypeData.value,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
}
electiveTypeChart.setOption(option)
}
// 创建课程数量走势图
const courseTrendEl = document.getElementById('courseTrendChart')
if (courseTrendEl) {
if (courseTrendChart) {
courseTrendChart.dispose()
}
courseTrendChart = echarts.init(courseTrendEl)
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: courseTrendData.value.dates,
axisLabel: {
rotate: 45,
},
},
yAxis: {
type: 'value',
},
series: [
{
name: '课程数量',
type: 'line',
data: courseTrendData.value.values,
smooth: true,
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(64, 158, 255, 0.3)' },
{ offset: 1, color: 'rgba(64, 158, 255, 0.1)' },
]),
},
},
],
}
courseTrendChart.setOption(option)
}
// 视频上传数量走势图
const videoUploadTrendEl = document.getElementById('videoUploadTrendChart')
if (videoUploadTrendEl) {
if (videoUploadTrendChart) {
videoUploadTrendChart.dispose()
}
videoUploadTrendChart = echarts.init(videoUploadTrendEl)
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: videoUploadTrendData.value.dates,
axisLabel: {
rotate: 45,
},
},
yAxis: {
type: 'value',
},
series: [
{
name: '视频数量',
type: 'line',
data: videoUploadTrendData.value.values,
smooth: true,
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(103, 194, 58, 0.3)' },
{ offset: 1, color: 'rgba(103, 194, 58, 0.1)' },
]),
},
},
],
}
videoUploadTrendChart.setOption(option)
}
}
// 窗口大小改变时重新调整图表
const handleResize = () => {
resourceTypeChart?.resize()
courseTypeChart?.resize()
electiveTypeChart?.resize()
courseTrendChart?.resize()
videoUploadTrendChart?.resize()
}
// 清理所有图表实例
const disposeAllCharts = () => {
resourceTypeChart?.dispose()
courseTypeChart?.dispose()
electiveTypeChart?.dispose()
courseTrendChart?.dispose()
videoUploadTrendChart?.dispose()
// 重置为null
resourceTypeChart = null
courseTypeChart = null
electiveTypeChart = null
courseTrendChart = null
videoUploadTrendChart = null
}
onMounted(() => {
loadData()
setTimeout(() => {
initCharts()
}, 100)
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
// 组件销毁时清理所有图表实例
disposeAllCharts()
window.removeEventListener('resize', handleResize)
})
</script>
<template>
<div class="resource-portrait">
<div class="page-header">
<h1>课程资源数据画像</h1>
<p>全面了解课程资源的分布情况和增长趋势</p>
</div>
<!-- 基础统计数据 -->
<div class="stats-section">
<h2>基础数据统计</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon video">
<VideoCamera />
</div>
<div class="stat-content">
<div class="stat-number">{{ statsData.videoCount }}</div>
<div class="stat-label">视频资源数量</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon courseware">
<Suitcase />
</div>
<div class="stat-content">
<div class="stat-number">{{ statsData.coursewareCount }}</div>
<div class="stat-label">课件资源数量</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon lesson-plan">
<FolderOpened />
</div>
<div class="stat-content">
<div class="stat-number">{{ statsData.lessonPlanCount }}</div>
<div class="stat-label">教案资源数量</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon other">
<Files />
</div>
<div class="stat-content">
<div class="stat-number">{{ statsData.otherResourceCount }}</div>
<div class="stat-label">其他资源数量</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon course">
<Monitor />
</div>
<div class="stat-content">
<div class="stat-number">{{ statsData.courseCount }}</div>
<div class="stat-label">课程数量</div>
</div>
</div>
</div>
</div>
<!-- 今日新增数据 -->
<div class="stats-section">
<h2>今日新增数据</h2>
<div class="stats-grid">
<div class="stat-card new">
<div class="stat-icon video">
<VideoCamera />
</div>
<div class="stat-content">
<div class="stat-number">{{ statsData.todayNewVideo }}</div>
<div class="stat-label">今日新增视频</div>
</div>
</div>
<div class="stat-card new">
<div class="stat-icon courseware">
<Suitcase />
</div>
<div class="stat-content">
<div class="stat-number">{{ statsData.todayNewCourseware }}</div>
<div class="stat-label">今日新增课件</div>
</div>
</div>
<div class="stat-card new">
<div class="stat-icon lesson-plan">
<FolderOpened />
</div>
<div class="stat-content">
<div class="stat-number">{{ statsData.todayNewLessonPlan }}</div>
<div class="stat-label">今日新增教案</div>
</div>
</div>
<div class="stat-card new">
<div class="stat-icon other">
<Files />
</div>
<div class="stat-content">
<div class="stat-number">{{ statsData.todayNewOtherResource }}</div>
<div class="stat-label">今日新增其他资源</div>
</div>
</div>
<div class="stat-card new">
<div class="stat-icon course">
<Monitor />
</div>
<div class="stat-content">
<div class="stat-number">{{ statsData.todayNewCourse }}</div>
<div class="stat-label">今日新增课程</div>
</div>
</div>
</div>
</div>
<!-- 图表分析区域 -->
<div class="charts-section">
<h2>数据分析图表</h2>
<!-- 饼图区域 -->
<div class="charts-row">
<div class="chart-container">
<div id="resourceTypeChart" class="chart"></div>
</div>
<div class="chart-container">
<div id="courseTypeChart" class="chart"></div>
</div>
<div class="chart-container">
<div id="electiveTypeChart" class="chart"></div>
</div>
</div>
<!-- 走势图区域 -->
<div class="charts-row">
<div class="chart-container large">
<div id="courseTrendChart" class="chart"></div>
</div>
<div class="chart-container large">
<div id="videoUploadTrendChart" class="chart"></div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.resource-portrait {
padding: 20px;
background-color: #f8f8f8;
min-height: 100vh;
.page-header {
text-align: center;
margin-bottom: 30px;
h1 {
font-size: 28px;
color: #303133;
margin-bottom: 10px;
}
p {
font-size: 16px;
color: #606266;
}
}
.stats-section {
margin-bottom: 40px;
h2 {
font-size: 20px;
color: #303133;
margin-bottom: 20px;
padding-left: 10px;
border-left: 4px solid #409eff;
}
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.stat-card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
transition: transform 0.3s ease;
&:hover {
transform: translateY(-2px);
}
&.new {
border-left: 4px solid #67c23a;
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
svg {
width: 24px;
height: 24px;
color: #ba8b45;
}
&.video,
&.courseware,
&.lesson-plan,
&.other,
&.course {
background: #f5ebda;
}
}
.stat-content {
flex: 1;
.stat-number {
font-size: 32px;
font-weight: bold;
color: #303133;
line-height: 1;
margin-bottom: 5px;
}
.stat-label {
font-size: 14px;
color: #909399;
}
}
}
.charts-section {
h2 {
font-size: 20px;
color: #303133;
margin-bottom: 20px;
padding-left: 10px;
border-left: 4px solid #409eff;
}
}
.charts-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
.chart-container {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
&.large {
grid-column: span 2;
}
.chart {
width: 100%;
height: 300px;
}
}
}
@media (max-width: 768px) {
.resource-portrait {
padding: 10px;
.stats-grid {
grid-template-columns: 1fr;
}
.charts-row {
grid-template-columns: 1fr;
.chart-container.large {
grid-column: span 1;
}
}
}
}
</style>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论