Files
012-kaopeilian/frontend/src/views/dashboard/index.vue
111 998211c483 feat: 初始化考培练系统项目
- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
2026-01-24 19:33:28 +08:00

478 lines
10 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="dashboard-container">
<!-- 欢迎卡片 -->
<div class="welcome-card card">
<div class="welcome-content">
<h1 class="welcome-title">欢迎回来{{ userName }}</h1>
<p class="welcome-desc">今天是您学习的第 <span class="highlight">{{ learningDays }}</span> 继续加油</p>
</div>
<div class="welcome-image">
<el-icon :size="120" color="#667eea">
<TrophyBase />
</el-icon>
</div>
</div>
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card card" v-for="stat in stats" :key="stat.title">
<div class="stat-icon" :style="{ backgroundColor: stat.bgColor }">
<el-icon :size="24" :color="stat.color">
<component :is="stat.icon" />
</el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-title">{{ stat.title }}</div>
<div class="stat-trend" :class="stat.trend > 0 ? 'up' : 'down'">
<el-icon :size="12">
<component :is="stat.trend > 0 ? 'Top' : 'Bottom'" />
</el-icon>
{{ Math.abs(stat.trend) }}%
</div>
</div>
</div>
</div>
<!-- 快捷操作 -->
<div class="quick-actions">
<h2 class="section-title">快捷操作</h2>
<div class="action-grid">
<div class="action-card card" v-for="action in quickActions" :key="action.title" @click="handleAction(action)">
<el-icon :size="32" :color="action.color">
<component :is="action.icon" />
</el-icon>
<div class="action-title">{{ action.title }}</div>
<div class="action-desc">{{ action.desc }}</div>
</div>
</div>
</div>
<!-- 最近考试 -->
<div class="recent-exams">
<h2 class="section-title">最近考试</h2>
<div v-if="recentExams.length > 0" class="exam-list">
<div class="exam-item card" v-for="exam in recentExams" :key="exam.id">
<div class="exam-info">
<h3 class="exam-title">{{ exam.title }}</h3>
<div class="exam-meta">
<span class="meta-item">
<el-icon><Clock /></el-icon>
{{ exam.time }}
</span>
<span class="meta-item">
<el-icon><Document /></el-icon>
{{ exam.questions }}
</span>
<span class="meta-item">
<el-icon><Reading /></el-icon>
{{ exam.courseName }}
</span>
<span v-if="exam.score != null" class="meta-item">
<el-icon><TrendCharts /></el-icon>
得分: {{ exam.score }}
</span>
</div>
</div>
<div class="exam-actions">
<el-button type="primary" size="small" @click="startExam(exam)">
开始考试
</el-button>
</div>
</div>
</div>
<div v-else class="empty-exams">
<el-empty description="暂无考试记录" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { authManager } from '@/utils/auth'
import { getUserStatistics, getRecentExams } from '@/api/dashboard'
const router = useRouter()
// 获取当前用户信息
const currentUser = computed(() => authManager.getCurrentUser())
const userName = computed(() => currentUser.value?.full_name || currentUser.value?.username || '用户')
const learningDays = ref(0)
// 统计数据
const stats = ref([
{
title: '完成考试',
value: '0',
icon: 'Document',
color: '#667eea',
bgColor: 'rgba(102, 126, 234, 0.1)',
trend: 0
},
{
title: '练习题数',
value: '0',
icon: 'Edit',
color: '#f56c6c',
bgColor: 'rgba(245, 108, 108, 0.1)',
trend: 0
},
{
title: '平均得分',
value: '0',
icon: 'TrendCharts',
color: '#e6a23c',
bgColor: 'rgba(230, 162, 60, 0.1)',
trend: 0
},
{
title: '学习时长',
value: '0h',
icon: 'Timer',
color: '#67c23a',
bgColor: 'rgba(103, 194, 58, 0.1)',
trend: 0
}
])
// 加载统计数据
const loadStatistics = async () => {
try {
const res = await getUserStatistics()
if (res.code === 200 && res.data) {
// 更新统计卡片
stats.value[0].value = String(res.data.examsCompleted || 0)
stats.value[1].value = String(res.data.practiceQuestions || 0)
stats.value[2].value = String(res.data.averageScore || 0)
stats.value[3].value = `${res.data.totalHours || 0}h`
// 更新学习天数
learningDays.value = res.data.learningDays || 0
}
} catch (error) {
console.error('加载统计数据失败:', error)
}
}
// 快捷操作
const quickActions = ref([
{
title: '智能工牌分析',
desc: 'AI能力评估与成长路径规划',
icon: 'TrendCharts',
color: '#e6a23c',
path: '/trainee/growth-path'
},
{
title: '课程中心',
desc: '查看可用课程',
icon: 'Collection',
color: '#67c23a',
path: '/trainee/course-center'
},
{
title: '查分中心',
desc: '查看成绩和分析报告',
icon: 'DataAnalysis',
color: '#409eff',
path: '/trainee/score-report'
},
{
title: 'AI陪练',
desc: '智能陪练系统',
icon: 'ChatLineRound',
color: '#f56c6c',
path: '/trainee/ai-practice-center'
}
])
// 最近考试
const recentExams = ref<any[]>([])
// 加载最近考试
const loadRecentExams = async () => {
try {
const res = await getRecentExams(3)
if (res.code === 200 && res.data) {
recentExams.value = res.data
}
} catch (error) {
console.error('加载最近考试失败:', error)
}
}
/**
* 处理快捷操作点击
*/
const handleAction = (action: any) => {
router.push(action.path)
}
/**
* 开始考试
*/
const startExam = (exam: any) => {
router.push({
path: '/trainee/exam',
query: { courseId: exam.courseId }
})
}
// 页面挂载时加载数据
onMounted(() => {
loadStatistics()
loadRecentExams()
})
</script>
<style lang="scss" scoped>
.dashboard-container {
max-width: 1400px;
margin: 0 auto;
}
// 欢迎卡片
.welcome-card {
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
margin-bottom: 24px;
padding: 32px;
.welcome-content {
.welcome-title {
font-size: 28px;
font-weight: 600;
margin-bottom: 12px;
}
.welcome-desc {
font-size: 16px;
opacity: 0.9;
.highlight {
font-size: 24px;
font-weight: 600;
color: #ffd700;
}
}
}
.welcome-image {
opacity: 0.8;
}
}
// 统计卡片
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 32px;
.stat-card {
display: flex;
align-items: center;
gap: 20px;
padding: 24px;
transition: all 0.3s ease;
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.stat-icon {
width: 56px;
height: 56px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.stat-content {
flex: 1;
.stat-value {
font-size: 28px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.stat-title {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.stat-trend {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
&.up {
color: #67c23a;
background-color: rgba(103, 194, 58, 0.1);
}
&.down {
color: #f56c6c;
background-color: rgba(245, 108, 108, 0.1);
}
}
}
}
}
// 通用标题
.section-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
}
// 快捷操作
.quick-actions {
margin-bottom: 32px;
.action-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
.action-card {
text-align: center;
padding: 32px 24px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
.action-title {
color: #667eea;
}
}
.action-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 12px 0 8px;
transition: color 0.3s;
}
.action-desc {
font-size: 14px;
color: #666;
}
}
}
}
// 最近考试
.recent-exams {
.exam-list {
display: flex;
flex-direction: column;
gap: 16px;
.exam-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
}
.exam-info {
.exam-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.exam-meta {
display: flex;
gap: 20px;
flex-wrap: wrap;
.meta-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 14px;
color: #666;
.el-icon {
color: #999;
}
}
}
}
.exam-actions {
.el-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 8px;
}
}
}
}
.empty-exams {
padding: 40px 20px;
text-align: center;
}
}
// 响应式
@media (max-width: 768px) {
.welcome-card {
flex-direction: column;
text-align: center;
.welcome-image {
margin-top: 20px;
}
}
.stats-grid {
grid-template-columns: 1fr;
}
.action-grid {
grid-template-columns: repeat(2, 1fr) !important;
}
.exam-item {
flex-direction: column;
align-items: flex-start !important;
gap: 16px;
.exam-actions {
width: 100%;
.el-button {
width: 100%;
}
}
}
}
</style>