- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
478 lines
10 KiB
Vue
478 lines
10 KiB
Vue
<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> |