kindergarten/reading-platform-frontend/src/views/school/DashboardView.vue

1178 lines
26 KiB
Vue
Raw Normal View History

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