kindergarten/reading-platform-frontend/src/views/school/DashboardView.vue
tonytech 7f757b6a63 初始提交:幼儿园阅读平台三端代码
- reading-platform-backend:NestJS 后端
- reading-platform-frontend:Vue3 前端
- reading-platform-java:Spring Boot 服务端
2026-02-28 17:51:15 +08:00

1178 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="school-dashboard">
<!-- 欢迎横幅 -->
<div class="welcome-banner">
<div class="banner-content">
<div class="banner-text">
<h1><HomeOutlined /> 校园阅读管理中心</h1>
<p>让每一个孩子都能享受阅读的快乐智慧成长每一天</p>
</div>
<div class="banner-decorations">
<span class="decoration"><BookOutlined /></span>
<span class="decoration"><StarOutlined /></span>
<span class="decoration"><BgColorsOutlined /></span>
<span class="decoration"><SmileOutlined /></span>
</div>
</div>
</div>
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card" v-for="(stat, index) in statCards" :key="index">
<div class="stat-icon" :style="{ background: stat.gradient }">
<component :is="stat.icon" />
</div>
<div class="stat-info">
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
</div>
</div>
</div>
<!-- 趋势图和分布图 -->
<div class="charts-grid">
<div class="content-card trend-card">
<div class="card-header">
<span class="card-icon"><LineChartOutlined /></span>
<h3>授课趋势</h3>
</div>
<div class="card-body" :class="{ 'is-loading': trendLoading }">
<a-spin v-if="trendLoading" />
<div v-else ref="trendChartRef" class="chart-container"></div>
</div>
</div>
<div class="content-card distribution-card">
<div class="card-header">
<span class="card-icon"><BarChartOutlined /></span>
<h3>课程分布</h3>
</div>
<div class="card-body" :class="{ 'is-loading': distributionLoading }">
<a-spin v-if="distributionLoading" />
<div v-else ref="distributionChartRef" class="chart-container"></div>
</div>
</div>
</div>
<!-- 主要内容区域 -->
<div class="content-grid">
<!-- 近期活动 -->
<div class="content-card activities-card">
<div class="card-header">
<span class="card-icon"><CalendarOutlined /></span>
<h3>近期课程活动</h3>
</div>
<div class="card-body" :class="{ 'is-loading': loading }">
<a-spin v-if="loading" />
<div v-else-if="recentActivities.length === 0" class="empty-state">
<span class="empty-icon"><InboxOutlined /></span>
<p>暂无近期活动</p>
</div>
<div v-else class="activity-list">
<div
v-for="item in recentActivities"
:key="item.id"
class="activity-item"
>
<div class="activity-avatar">
<BookOutlined />
</div>
<div class="activity-content">
<div class="activity-title">{{ item.title }}</div>
<div class="activity-time">{{ formatTime(item.time) }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- 教师活跃度排行 -->
<div class="content-card teachers-card">
<div class="card-header">
<span class="card-icon"><TrophyOutlined /></span>
<h3>教师活跃度排行</h3>
</div>
<div class="card-body" :class="{ 'is-loading': loading }">
<a-spin v-if="loading" />
<div v-else-if="activeTeachers.length === 0" class="empty-state">
<span class="empty-icon"><TeamOutlined /></span>
<p>暂无数据</p>
</div>
<div v-else class="teacher-list">
<div
v-for="(item, index) in activeTeachers"
:key="item.id"
class="teacher-item"
>
<div class="rank-badge" :class="'rank-' + (index + 1)">
{{ index + 1 }}
</div>
<div class="teacher-info">
<div class="teacher-name">{{ item.name }}</div>
<div class="teacher-lessons">
<span class="lesson-icon"><ReadOutlined /></span>
授课 {{ item.lessonCount }} 次
</div>
</div>
<div class="teacher-medal">
<component :is="getMedalIcon(index)" :style="getMedalStyle(index)" />
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 课程使用统计 -->
<div class="course-stats-card">
<div class="card-header">
<span class="card-icon"><BarChartOutlined /></span>
<h3>课程使用统计</h3>
<div class="header-extra">
<a-range-picker
v-model:value="dateRange"
@change="loadCourseStats"
:placeholder="['开始日期', '结束日期']"
style="width: 240px;"
/>
</div>
</div>
<div class="card-body" :class="{ 'is-loading': courseStatsLoading }">
<a-spin v-if="courseStatsLoading" />
<div v-else-if="courseStats.length === 0" class="empty-state">
<span class="empty-icon"><LineChartOutlined /></span>
<p>暂无课程使用数据</p>
</div>
<div v-else class="course-list">
<div
v-for="(item, index) in courseStats"
:key="item.courseId"
class="course-item"
>
<div class="course-rank" :class="'top-' + (index + 1)">
<TrophyFilled v-if="index < 3" class="rank-crown" :style="getTrophyColor(index)" />
<span v-else>{{ index + 1 }}</span>
</div>
<div class="course-name">{{ item.courseName }}</div>
<div class="course-progress">
<div class="progress-bar">
<div
class="progress-fill"
:style="{
width: getUsagePercent(item.usageCount) + '%',
background: getProgressGradient(index)
}"
></div>
</div>
<span class="progress-value">{{ item.usageCount }}次</span>
</div>
</div>
</div>
</div>
</div>
<!-- 数据导出 -->
<div class="export-section">
<div class="content-card export-card">
<div class="card-header">
<span class="card-icon"><DownloadOutlined /></span>
<h3>数据导出</h3>
</div>
<div class="card-body">
<div class="export-grid">
<div class="export-item" @click="handleExportLessons">
<div class="export-icon" style="background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);">
<ReadOutlined />
</div>
<div class="export-info">
<div class="export-title">授课记录</div>
<div class="export-desc">导出所有授课记录数据</div>
</div>
</div>
<div class="export-item" @click="handleExportTeacherStats">
<div class="export-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<SolutionOutlined />
</div>
<div class="export-info">
<div class="export-title">教师绩效</div>
<div class="export-desc">导出教师绩效统计数据</div>
</div>
</div>
<div class="export-item" @click="handleExportStudentStats">
<div class="export-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
<UserOutlined />
</div>
<div class="export-info">
<div class="export-title">学生统计</div>
<div class="export-desc">导出学生统计数据</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue';
import {
HomeOutlined,
BookOutlined,
StarOutlined,
BgColorsOutlined,
SmileOutlined,
SolutionOutlined,
TeamOutlined,
UserOutlined,
ReadOutlined,
CalendarOutlined,
InboxOutlined,
TrophyOutlined,
TrophyFilled,
StarFilled,
BarChartOutlined,
LineChartOutlined,
DownloadOutlined,
} from '@ant-design/icons-vue';
import * as echarts from 'echarts';
import { message } from 'ant-design-vue';
import {
getSchoolStats,
getActiveTeachers,
getRecentActivities,
getCourseUsageStats,
getLessonTrend,
getCourseDistribution,
exportLessons,
exportTeacherStats,
exportStudentStats,
} from '@/api/school';
import type { SchoolStats, LessonTrendItem, CourseDistributionItem } from '@/api/school';
import type { Component } from 'vue';
import { Dayjs } from 'dayjs';
const loading = ref(false);
const courseStatsLoading = ref(false);
const trendLoading = ref(false);
const distributionLoading = ref(false);
// Chart refs
const trendChartRef = ref<HTMLElement>();
const distributionChartRef = ref<HTMLElement>();
let trendChart: echarts.ECharts | null = null;
let distributionChart: echarts.ECharts | null = null;
const stats = ref<SchoolStats>({
teacherCount: 0,
studentCount: 0,
classCount: 0,
lessonCount: 0,
});
const recentActivities = ref<Array<{ id: number; type: string; title: string; time: string }>>([]);
const activeTeachers = ref<Array<{ id: number; name: string; lessonCount: number }>>([]);
const courseStats = ref<Array<{ courseId: number; courseName: string; usageCount: number }>>([]);
const dateRange = ref<[Dayjs, Dayjs] | undefined>(undefined);
// 图表数据
const lessonTrendData = ref<LessonTrendItem[]>([]);
const courseDistributionData = ref<CourseDistributionItem[]>([]);
const statCards = computed(() => [
{
icon: SolutionOutlined,
label: '教师总数',
value: stats.value.teacherCount,
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
},
{
icon: UserOutlined,
label: '学生总数',
value: stats.value.studentCount,
gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)'
},
{
icon: HomeOutlined,
label: '班级总数',
value: stats.value.classCount,
gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)'
},
{
icon: ReadOutlined,
label: '授课次数',
value: stats.value.lessonCount,
gradient: 'linear-gradient(135deg, #FF8C42 0%, #FFB347 100%)'
}
]);
const formatTime = (time: string) => {
const date = new Date(time);
const now = new Date();
const diff = now.getTime() - date.getTime();
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 60) {
return `${minutes}分钟前`;
} else if (hours < 24) {
return `${hours}小时前`;
} else if (days < 7) {
return `${days}天前`;
} else {
return date.toLocaleDateString();
}
};
const getMedalIcon = (index: number): Component => {
const icons = [TrophyOutlined, StarFilled, StarOutlined];
return icons[index] || StarOutlined;
};
const getMedalStyle = (index: number) => {
const styles = [
{ color: '#FFD700', fontSize: '20px' },
{ color: '#FFA500', fontSize: '18px' },
{ color: '#87CEEB', fontSize: '16px' }
];
return styles[index] || { color: '#B2BEC3', fontSize: '14px' };
};
const getTrophyColor = (index: number) => {
const colors = [
{ color: '#FFD700' }, // Gold
{ color: '#C0C0C0' }, // Silver
{ color: '#CD7F32' } // Bronze
];
return colors[index] || { color: '#636E72' };
};
const getProgressGradient = (index: number) => {
const gradients = [
'linear-gradient(90deg, #FF8C42 0%, #FFB347 100%)',
'linear-gradient(90deg, #667eea 0%, #764ba2 100%)',
'linear-gradient(90deg, #f093fb 0%, #f5576c 100%)',
'linear-gradient(90deg, #4facfe 0%, #00f2fe 100%)',
'linear-gradient(90deg, #43e97b 0%, #38f9d7 100%)',
];
return gradients[index % gradients.length];
};
const getUsagePercent = (count: number) => {
if (courseStats.value.length === 0) return 0;
const max = Math.max(...courseStats.value.map(s => s.usageCount));
return max > 0 ? Math.round((count / max) * 100) : 0;
};
// 初始化授课趋势图表
const initTrendChart = (data: LessonTrendItem[]) => {
if (!trendChartRef.value) return;
if (trendChart) {
trendChart.dispose();
}
trendChart = echarts.init(trendChartRef.value);
// 如果没有数据,显示空状态
if (!data || data.length === 0) {
trendChart.setOption({
title: {
text: '暂无数据',
left: 'center',
top: 'center',
textStyle: {
color: '#999',
fontSize: 14,
},
},
});
return;
}
const option: echarts.EChartsOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
},
},
legend: {
data: ['授课次数', '学生数'],
top: 0,
right: 10,
itemWidth: 16,
itemHeight: 10,
textStyle: {
fontSize: 12,
color: '#666',
},
},
grid: {
left: '3%',
right: '4%',
top: '18%',
bottom: '8%',
containLabel: true,
},
xAxis: {
type: 'category',
data: data.map((d) => d.month),
axisLine: {
lineStyle: {
color: '#E5E7EB',
},
},
},
yAxis: [
{
type: 'value',
name: '授课次数',
position: 'left',
splitLine: {
lineStyle: {
color: '#F3F4F6',
},
},
},
{
type: 'value',
name: '学生数',
position: 'right',
splitLine: {
show: false,
},
},
],
series: [
{
name: '授课次数',
type: 'bar',
data: data.map((d) => d.lessonCount),
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#FF8C42' },
{ offset: 1, color: '#FFB347' },
]),
borderRadius: [4, 4, 0, 0],
},
},
{
name: '学生数',
type: 'line',
yAxisIndex: 1,
data: data.map((d) => d.studentCount),
smooth: true,
itemStyle: {
color: '#667eea',
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(102, 126, 234, 0.3)' },
{ offset: 1, color: 'rgba(102, 126, 234, 0)' },
]),
},
},
],
};
trendChart.setOption(option);
};
// 初始化课程分布饼图
const initDistributionChart = (data: CourseDistributionItem[]) => {
if (!distributionChartRef.value) return;
if (distributionChart) {
distributionChart.dispose();
}
distributionChart = echarts.init(distributionChartRef.value);
const option: echarts.EChartsOption = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}次 ({d}%)',
},
legend: {
orient: 'vertical',
right: '5%',
top: 'center',
},
series: [
{
type: 'pie',
radius: ['40%', '70%'],
center: ['35%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 8,
borderColor: '#fff',
borderWidth: 2,
},
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: data.map((item, index) => ({
...item,
itemStyle: {
color: [
'#FF8C42',
'#667eea',
'#f093fb',
'#4facfe',
'#43e97b',
'#fa709a',
'#fee140',
'#30cfd0',
][index % 8],
},
})),
},
],
};
distributionChart.setOption(option);
};
// 窗口大小变化时重绘图表
const handleResize = () => {
trendChart?.resize();
distributionChart?.resize();
};
const loadData = async () => {
loading.value = true;
try {
const [statsData, teachersData, activitiesData] = await Promise.all([
getSchoolStats(),
getActiveTeachers(5),
getRecentActivities(10),
]);
stats.value = statsData;
activeTeachers.value = teachersData;
recentActivities.value = activitiesData;
} catch (error) {
console.error('Failed to load dashboard data:', error);
} finally {
loading.value = false;
}
};
const loadCourseStats = async () => {
courseStatsLoading.value = true;
try {
const data = await getCourseUsageStats();
courseStats.value = data.slice(0, 10);
} catch (error) {
console.error('Failed to load course stats:', error);
} finally {
courseStatsLoading.value = false;
}
};
// 加载授课趋势数据
const loadTrendData = async () => {
trendLoading.value = true;
try {
const data = await getLessonTrend(6);
lessonTrendData.value = data;
} catch (error) {
console.error('Failed to load trend data:', error);
} finally {
trendLoading.value = false;
}
// 在 loading 结束后初始化图表
await nextTick();
if (lessonTrendData.value.length > 0) {
initTrendChart(lessonTrendData.value);
}
};
// 加载课程分布数据
const loadDistributionData = async () => {
distributionLoading.value = true;
try {
const data = await getCourseDistribution();
courseDistributionData.value = data;
} catch (error) {
console.error('Failed to load distribution data:', error);
} finally {
distributionLoading.value = false;
}
// 在 loading 结束后初始化图表
await nextTick();
if (courseDistributionData.value.length > 0) {
initDistributionChart(courseDistributionData.value);
}
};
// 导出功能
const handleExportLessons = async () => {
try {
message.loading({ content: '正在导出...', key: 'export' });
await exportLessons();
message.success({ content: '导出成功', key: 'export' });
} catch (error) {
message.error({ content: '导出失败', key: 'export' });
}
};
const handleExportTeacherStats = async () => {
try {
message.loading({ content: '正在导出...', key: 'export' });
await exportTeacherStats();
message.success({ content: '导出成功', key: 'export' });
} catch (error) {
message.error({ content: '导出失败', key: 'export' });
}
};
const handleExportStudentStats = async () => {
try {
message.loading({ content: '正在导出...', key: 'export' });
await exportStudentStats();
message.success({ content: '导出成功', key: 'export' });
} catch (error) {
message.error({ content: '导出失败', key: 'export' });
}
};
onMounted(() => {
loadData();
loadCourseStats();
loadTrendData();
loadDistributionData();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
trendChart?.dispose();
distributionChart?.dispose();
});
</script>
<style scoped>
.school-dashboard {
padding: 0;
min-height: 100vh;
background: linear-gradient(180deg, #FFF8F0 0%, #FFFFFF 100%);
}
/* 欢迎横幅 */
.welcome-banner {
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 50%, #FFD93D 100%);
border-radius: 20px;
padding: 32px 40px;
margin-bottom: 24px;
position: relative;
overflow: hidden;
}
.banner-content {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
z-index: 1;
}
.banner-text h1 {
color: white;
font-size: 28px;
font-weight: 700;
margin: 0 0 8px 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.banner-text p {
color: rgba(255, 255, 255, 0.9);
font-size: 16px;
margin: 0;
}
.banner-decorations {
display: flex;
gap: 16px;
}
.decoration {
font-size: 36px;
animation: float 3s ease-in-out infinite;
}
.decoration:nth-child(2) { animation-delay: 0.5s; }
.decoration:nth-child(3) { animation-delay: 1s; }
.decoration:nth-child(4) { animation-delay: 1.5s; }
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
/* 统计卡片 */
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 24px;
}
.stat-card {
background: white;
border-radius: 16px;
padding: 24px;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
color: white;
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #2D3436;
line-height: 1.2;
}
.stat-label {
font-size: 14px;
color: #636E72;
margin-top: 4px;
}
/* 内容网格 */
.content-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
margin-bottom: 24px;
}
/* 图表网格 */
.charts-grid {
display: grid;
grid-template-columns: 3fr 2fr;
gap: 24px;
margin-bottom: 24px;
}
.trend-card {
grid-column: 1;
}
.distribution-card {
grid-column: 2;
}
.chart-container {
width: 100%;
height: 300px;
}
/* 卡片通用样式 */
.content-card,
.course-stats-card {
background: white;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
}
.card-header {
display: flex;
align-items: center;
gap: 12px;
padding: 20px 24px;
border-bottom: 1px solid #F5F5F5;
}
.card-icon {
font-size: 24px;
color: #FF8C42;
display: flex;
align-items: center;
}
.card-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #2D3436;
flex: 1;
}
.header-extra {
margin-left: auto;
}
.card-body {
padding: 20px 24px;
min-height: 200px;
}
.card-body.is-loading {
display: flex;
align-items: center;
justify-content: center;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
color: #B2BEC3;
}
.empty-icon {
font-size: 48px;
margin-bottom: 12px;
color: #B2BEC3;
display: flex;
align-items: center;
justify-content: center;
}
.empty-state p {
margin: 0;
font-size: 14px;
}
/* 活动列表 */
.activity-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.activity-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: #FAFAFA;
border-radius: 12px;
transition: all 0.2s ease;
}
.activity-item:hover {
background: #FFF8F0;
}
.activity-avatar {
width: 40px;
height: 40px;
border-radius: 10px;
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.activity-title {
font-size: 14px;
font-weight: 500;
color: #2D3436;
}
.activity-time {
font-size: 12px;
color: #B2BEC3;
margin-top: 4px;
}
/* 教师列表 */
.teacher-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.teacher-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: #FAFAFA;
border-radius: 12px;
transition: all 0.2s ease;
}
.teacher-item:hover {
background: #FFF8F0;
}
.rank-badge {
width: 28px;
height: 28px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
color: white;
background: #B2BEC3;
}
.rank-badge.rank-1 { background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%); }
.rank-badge.rank-2 { background: linear-gradient(135deg, #C0C0C0 0%, #A8A8A8 100%); }
.rank-badge.rank-3 { background: linear-gradient(135deg, #CD7F32 0%, #B8860B 100%); }
.teacher-info {
flex: 1;
}
.teacher-name {
font-size: 14px;
font-weight: 500;
color: #2D3436;
}
.teacher-lessons {
font-size: 12px;
color: #636E72;
margin-top: 4px;
display: flex;
align-items: center;
gap: 4px;
}
.lesson-icon {
font-size: 14px;
color: #636E72;
display: flex;
align-items: center;
}
.teacher-medal {
display: flex;
align-items: center;
justify-content: center;
}
/* 课程统计 */
.course-stats-card {
margin-bottom: 24px;
}
/* 导出区域 */
.export-section {
margin-bottom: 24px;
}
.export-card .card-body {
padding: 24px;
}
.export-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.export-item {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
background: #FAFAFA;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid transparent;
}
.export-item:hover {
background: #FFF8F0;
border-color: #FFD4B8;
transform: translateY(-2px);
}
.export-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
flex-shrink: 0;
}
.export-info {
flex: 1;
}
.export-title {
font-size: 15px;
font-weight: 600;
color: #2D3436;
}
.export-desc {
font-size: 12px;
color: #636E72;
margin-top: 4px;
}
.course-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.course-item {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 16px;
background: #FAFAFA;
border-radius: 12px;
transition: all 0.2s ease;
}
.course-item:hover {
background: #FFF8F0;
}
.course-rank {
width: 32px;
text-align: center;
font-size: 14px;
font-weight: 600;
color: #636E72;
}
.rank-crown {
font-size: 24px;
}
.course-name {
flex: 1;
font-size: 14px;
font-weight: 500;
color: #2D3436;
}
.course-progress {
display: flex;
align-items: center;
gap: 12px;
width: 200px;
}
.progress-bar {
flex: 1;
height: 8px;
background: #F0F0F0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s ease;
}
.progress-value {
font-size: 12px;
color: #636E72;
white-space: nowrap;
min-width: 40px;
text-align: right;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.charts-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.welcome-banner {
padding: 24px;
}
.banner-text h1 {
font-size: 22px;
}
.banner-decorations {
display: none;
}
.stats-grid {
grid-template-columns: 1fr;
}
.charts-grid {
grid-template-columns: 1fr;
}
.export-grid {
grid-template-columns: 1fr;
}
.charts-grid {
grid-template-columns: 1fr;
}
.content-grid {
grid-template-columns: 1fr;
}
.course-progress {
width: 120px;
}
}
</style>