All checks were successful
continuous-integration/drone/push Build is passing
- 新增数据库表: growth_path_nodes, user_growth_path_progress, user_node_completions - 新增 Model: GrowthPathNode, UserGrowthPathProgress, UserNodeCompletion - 新增 Service: GrowthPathService(管理端CRUD、学员端进度追踪) - 新增 API: 学员端获取成长路径、管理端CRUD - 前端学员端从API动态加载成长路径数据 - 更新管理端API接口定义
2379 lines
65 KiB
Vue
2379 lines
65 KiB
Vue
<template>
|
||
<div class="growth-path-container">
|
||
<!-- 个人信息栏 -->
|
||
<div class="personal-info card">
|
||
<div class="info-left">
|
||
<el-avatar :size="80" :src="userInfo.avatar">
|
||
<el-icon :size="40"><UserFilled /></el-icon>
|
||
</el-avatar>
|
||
<div class="info-content">
|
||
<h2 class="user-name">{{ userInfo.name }}</h2>
|
||
<div class="user-meta">
|
||
<span class="position">{{ userInfo.position }}</span>
|
||
<span class="separator">|</span>
|
||
<span class="level">Lv.{{ userInfo.level }}</span>
|
||
<span class="separator">|</span>
|
||
<span class="exp">经验值:{{ userInfo.exp }}/{{ userInfo.nextLevelExp }}</span>
|
||
</div>
|
||
<el-progress :percentage="expPercentage" :stroke-width="10" />
|
||
</div>
|
||
</div>
|
||
<div class="info-right">
|
||
<el-button type="primary" @click="viewProfile">
|
||
<el-icon class="el-icon--left"><User /></el-icon>
|
||
个人中心
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="main-content">
|
||
<!-- 能力雷达图 -->
|
||
<div class="ability-radar card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">能力评估</h3>
|
||
<el-button type="primary" size="small" @click="analyzeSmartBadgeData" :loading="analyzing">
|
||
<el-icon><TrendCharts /></el-icon>
|
||
AI 分析智能工牌数据
|
||
</el-button>
|
||
</div>
|
||
<div class="radar-chart" ref="radarChartRef"></div>
|
||
|
||
<!-- AI 能力分析详细反馈 -->
|
||
<div v-if="abilityFeedback.length > 0" class="ability-feedback">
|
||
<div class="feedback-header">
|
||
<el-icon><MagicStick /></el-icon>
|
||
<span>AI 详细分析</span>
|
||
</div>
|
||
<div class="feedback-list">
|
||
<div
|
||
v-for="item in abilityFeedback"
|
||
:key="item.name"
|
||
class="feedback-item"
|
||
:class="{ 'weak': item.score < 80, 'good': item.score >= 80 && item.score < 90, 'excellent': item.score >= 90 }"
|
||
>
|
||
<div class="feedback-header-row">
|
||
<span class="dimension-name">{{ item.name }}</span>
|
||
<span class="dimension-score">{{ item.score }}分</span>
|
||
</div>
|
||
<p class="feedback-text">{{ item.feedback }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- AI 智能推荐学习模块 -->
|
||
<div class="ai-learning-hub-inner">
|
||
<!-- 模块头部 -->
|
||
<div class="hub-header">
|
||
<div class="header-left">
|
||
<div class="ai-avatar">
|
||
<div class="avatar-glow"></div>
|
||
<el-icon :size="28"><Cpu /></el-icon>
|
||
</div>
|
||
<div class="header-text">
|
||
<h2 class="hub-title">AI 智能学习助手</h2>
|
||
<p class="hub-subtitle">基于能力评估的个性化课程推荐</p>
|
||
</div>
|
||
</div>
|
||
<div class="header-actions">
|
||
<el-button
|
||
type="primary"
|
||
:loading="analyzing"
|
||
@click="refreshRecommendations"
|
||
class="refresh-btn"
|
||
>
|
||
<el-icon><MagicStick /></el-icon>
|
||
重新分析
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 推荐统计 -->
|
||
<div class="recommendation-stats">
|
||
<div class="stat-card">
|
||
<div class="stat-icon">
|
||
<el-icon><TrendCharts /></el-icon>
|
||
</div>
|
||
<div class="stat-content">
|
||
<div class="stat-number">{{ recommendedCourses.length }}</div>
|
||
<div class="stat-label">推荐课程</div>
|
||
</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-icon">
|
||
<el-icon><Trophy /></el-icon>
|
||
</div>
|
||
<div class="stat-content">
|
||
<div class="stat-number">{{ getAverageImprovement() }}</div>
|
||
<div class="stat-label">平均提升</div>
|
||
</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-icon">
|
||
<el-icon><Clock /></el-icon>
|
||
</div>
|
||
<div class="stat-content">
|
||
<div class="stat-number">{{ getTotalHours() }}h</div>
|
||
<div class="stat-label">总学时</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 推荐课程列表 -->
|
||
<div class="recommendations-section">
|
||
<div class="section-header">
|
||
<h3 class="section-title">为您量身定制的学习路径</h3>
|
||
<div class="filter-tabs">
|
||
<el-radio-group v-model="selectedPriority" size="small" @change="filterByPriority">
|
||
<el-radio-button label="all">全部</el-radio-button>
|
||
<el-radio-button label="high">高优先级</el-radio-button>
|
||
<el-radio-button label="medium">中优先级</el-radio-button>
|
||
</el-radio-group>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 课程卡片网格 -->
|
||
<div class="course-grid">
|
||
<div
|
||
v-for="(course, index) in filteredCourses"
|
||
:key="course.id"
|
||
class="smart-course-card"
|
||
:class="{ 'featured': course.priority === 'high' }"
|
||
:style="{ '--delay': index * 0.1 + 's' }"
|
||
@click="viewCourseDetail(course)"
|
||
>
|
||
<!-- 卡片装饰 -->
|
||
<div class="card-decoration">
|
||
<div class="decoration-line"></div>
|
||
<div class="priority-indicator" :class="course.priority">
|
||
<el-icon><Star /></el-icon>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 课程封面 -->
|
||
<div class="smart-cover">
|
||
<div class="cover-gradient"></div>
|
||
<div class="cover-content">
|
||
<div class="course-icon">
|
||
<el-icon :size="40"><Document /></el-icon>
|
||
</div>
|
||
<div class="match-score">
|
||
<div class="score-ring">
|
||
<svg class="score-circle" viewBox="0 0 36 36">
|
||
<path class="circle-bg" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
|
||
<path class="circle" :stroke-dasharray="`${course.matchScore}, 100`" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
|
||
</svg>
|
||
<div class="score-text">{{ course.matchScore }}%</div>
|
||
</div>
|
||
<div class="score-label">匹配度</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 课程信息 -->
|
||
<div class="smart-info">
|
||
<div class="course-header">
|
||
<h4 class="course-title">{{ course.name }}</h4>
|
||
<div class="priority-tag" :class="course.priority">
|
||
{{ getPriorityText(course.priority) }}
|
||
</div>
|
||
</div>
|
||
|
||
<p class="course-description">{{ course.description }}</p>
|
||
|
||
<!-- AI 分析结果 -->
|
||
<div class="ai-analysis">
|
||
<div class="analysis-header">
|
||
<el-icon><MagicStick /></el-icon>
|
||
<span>AI 分析结果</span>
|
||
</div>
|
||
<div class="analysis-content">
|
||
<div class="weakness-tags">
|
||
<el-tag
|
||
v-for="weakness in course.targetWeakPoints"
|
||
:key="weakness"
|
||
type="warning"
|
||
size="small"
|
||
effect="light"
|
||
>
|
||
<el-icon><Warning /></el-icon>
|
||
{{ weakness }}
|
||
</el-tag>
|
||
</div>
|
||
<div class="improvement-badge">
|
||
<el-icon><TrendCharts /></el-icon>
|
||
<span>预期提升 +{{ course.expectedImprovement }}分</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 课程元数据 -->
|
||
<div class="course-metadata">
|
||
<div class="meta-item">
|
||
<el-icon><Timer /></el-icon>
|
||
<span>{{ course.duration }}小时</span>
|
||
</div>
|
||
<div class="meta-item">
|
||
<el-icon><Medal /></el-icon>
|
||
<span>{{ getDifficultyText(course.difficulty) }}</span>
|
||
</div>
|
||
<div class="meta-item">
|
||
<el-icon><UserFilled /></el-icon>
|
||
<span>{{ course.learnerCount }}人在学</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 学习进度条 -->
|
||
<div class="progress-section" v-if="course.progress > 0">
|
||
<div class="progress-header">
|
||
<span class="progress-label">学习进度</span>
|
||
<span class="progress-value">{{ course.progress }}%</span>
|
||
</div>
|
||
<el-progress
|
||
:percentage="course.progress"
|
||
:stroke-width="6"
|
||
:show-text="false"
|
||
color="#667eea"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 智能操作按钮 -->
|
||
<div class="smart-actions">
|
||
<el-button
|
||
type="primary"
|
||
class="primary-action"
|
||
@click.stop="enterCourse(course)"
|
||
>
|
||
<el-icon><VideoPlay /></el-icon>
|
||
进入学习
|
||
</el-button>
|
||
|
||
<div class="action-grid">
|
||
<!-- 播课功能暂时关闭 -->
|
||
<el-button
|
||
v-if="false"
|
||
class="action-btn"
|
||
@click.stop="playAudio(course)"
|
||
>
|
||
<el-icon><Headset /></el-icon>
|
||
播课
|
||
</el-button>
|
||
|
||
<el-button
|
||
class="action-btn"
|
||
@click.stop="chatWithCourse(course)"
|
||
>
|
||
<el-icon><ChatDotRound /></el-icon>
|
||
对话
|
||
</el-button>
|
||
|
||
<el-button
|
||
v-if="course.progress >= 80"
|
||
class="action-btn exam-btn"
|
||
@click.stop="startExam(course)"
|
||
>
|
||
<el-icon><Edit /></el-icon>
|
||
考试
|
||
</el-button>
|
||
|
||
<el-button
|
||
v-if="course.examPassed"
|
||
class="action-btn practice-btn"
|
||
@click.stop="startPractice(course)"
|
||
>
|
||
<el-icon><Microphone /></el-icon>
|
||
陪练
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 空状态 -->
|
||
<div v-if="filteredCourses.length === 0" class="empty-recommendations">
|
||
<div class="empty-icon">
|
||
<el-icon :size="64" :class="{ 'rotating': analyzing }">
|
||
<MagicStick v-if="!analyzing" />
|
||
<Loading v-else />
|
||
</el-icon>
|
||
</div>
|
||
<h3 class="empty-title">{{ analyzing ? 'AI 正在分析中...' : '暂无推荐课程' }}</h3>
|
||
<p class="empty-description">
|
||
{{ analyzing ? '正在分析您的智能工牌数据,为您推荐最适合的课程' : '暂无智能工牌数据,请先使用智能工牌记录对话' }}
|
||
</p>
|
||
<el-button v-if="!analyzing" type="primary" @click="analyzeSmartBadgeData">
|
||
<el-icon><Refresh /></el-icon>
|
||
重新分析
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 成长路径图 -->
|
||
<div class="growth-tree card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">我的成长路径</h3>
|
||
<div class="legend">
|
||
<span class="legend-item completed">
|
||
<i class="dot"></i>已完成
|
||
</span>
|
||
<span class="legend-item current">
|
||
<i class="dot"></i>当前任务
|
||
</span>
|
||
<span class="legend-item locked">
|
||
<i class="dot"></i>未解锁
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tree-container">
|
||
<div class="tree-level" v-for="(level, index) in growthPath" :key="index">
|
||
<div class="level-header">
|
||
<span class="level-name">{{ level.name }}</span>
|
||
<span class="level-progress">{{ level.completed }}/{{ level.total }}</span>
|
||
</div>
|
||
<div class="level-nodes">
|
||
<div
|
||
class="tree-node"
|
||
:class="node.status"
|
||
v-for="node in level.nodes"
|
||
:key="node.id"
|
||
@click="handleNodeClick(node)"
|
||
>
|
||
<div class="node-icon">
|
||
<el-icon :size="24">
|
||
<component :is="getNodeIcon(node.status)" />
|
||
</el-icon>
|
||
</div>
|
||
<div class="node-content">
|
||
<h4 class="node-title">{{ node.title }}</h4>
|
||
<p class="node-desc">{{ node.description }}</p>
|
||
<div class="node-progress" v-if="node.status === 'current'">
|
||
<el-progress :percentage="node.progress" :stroke-width="6" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { ElMessage } from 'element-plus'
|
||
import {
|
||
UserFilled,
|
||
User,
|
||
TrendCharts,
|
||
MagicStick, // AI分析图标
|
||
Cpu // 机器人图标
|
||
} from '@element-plus/icons-vue'
|
||
import * as echarts from 'echarts'
|
||
import { analyzeYanjiBadge, getCourseDetail, getGrowthPath, startGrowthPath } from '@/api/trainee'
|
||
import type { TraineeGrowthPath, TraineeGrowthPathStage } from '@/api/trainee'
|
||
import { getCurrentUserProfile } from '@/api/user'
|
||
|
||
const router = useRouter()
|
||
|
||
// 雷达图引用
|
||
const radarChartRef = ref()
|
||
let radarChart: any = null
|
||
|
||
// 能力数据
|
||
const abilityData = ref([
|
||
{ name: '专业知识', value: 88, max: 100 },
|
||
{ name: '沟通技巧', value: 92, max: 100 },
|
||
{ name: '操作技能', value: 85, max: 100 },
|
||
{ name: '客户服务', value: 90, max: 100 },
|
||
{ name: '安全意识', value: 83, max: 100 },
|
||
{ name: '应变能力', value: 80, max: 100 }
|
||
])
|
||
|
||
// 定义能力反馈接口
|
||
interface AbilityFeedback {
|
||
name: string
|
||
score: number
|
||
feedback: string
|
||
}
|
||
|
||
// AI 能力分析详细反馈(来自Dify)
|
||
const abilityFeedback = ref<AbilityFeedback[]>([])
|
||
|
||
// AI 分析和推荐相关
|
||
const analyzing = ref(false)
|
||
const selectedPriority = ref('all')
|
||
|
||
// 计算属性:过滤后的课程
|
||
const filteredCourses = computed(() => {
|
||
if (selectedPriority.value === 'all') {
|
||
return recommendedCourses.value
|
||
}
|
||
return recommendedCourses.value.filter(course => course.priority === selectedPriority.value)
|
||
})
|
||
|
||
// 定义推荐课程接口
|
||
interface RecommendedCourse {
|
||
id: number
|
||
name: string
|
||
description: string
|
||
duration: number
|
||
difficulty: string
|
||
learnerCount: number
|
||
priority: string
|
||
matchScore: number
|
||
targetWeakPoints: string[]
|
||
expectedImprovement: number
|
||
progress: number
|
||
examPassed: boolean
|
||
}
|
||
|
||
// AI 推荐课程数据(初始为空,等待AI分析后填充)
|
||
const recommendedCourses = ref<RecommendedCourse[]>([])
|
||
|
||
// 用户信息
|
||
const userInfo = ref({
|
||
name: '加载中...',
|
||
position: '加载中...',
|
||
level: 1,
|
||
exp: 0,
|
||
nextLevelExp: 1000,
|
||
avatar: '',
|
||
role: 'trainee', // 用户角色
|
||
phone: '' // 用户手机号
|
||
})
|
||
|
||
// 经验值百分比
|
||
const expPercentage = computed(() => {
|
||
return Math.round((userInfo.value.exp / userInfo.value.nextLevelExp) * 100)
|
||
})
|
||
|
||
// 成长路径数据(从API加载)
|
||
const growthPathData = ref<TraineeGrowthPath | null>(null)
|
||
const growthPathLoading = ref(false)
|
||
|
||
// 将API数据转换为前端展示格式
|
||
const growthPath = computed(() => {
|
||
if (!growthPathData.value || !growthPathData.value.stages) {
|
||
// 返回默认空数据或占位数据
|
||
return [{
|
||
name: '暂无成长路径',
|
||
completed: 0,
|
||
total: 0,
|
||
nodes: []
|
||
}]
|
||
}
|
||
|
||
return growthPathData.value.stages.map(stage => ({
|
||
name: stage.name,
|
||
completed: stage.completed,
|
||
total: stage.total,
|
||
nodes: stage.nodes.map(node => ({
|
||
id: node.id,
|
||
courseId: node.course_id,
|
||
title: node.title,
|
||
description: node.description || '',
|
||
status: node.status, // locked/unlocked/in_progress/completed
|
||
progress: node.progress
|
||
}))
|
||
}))
|
||
})
|
||
|
||
/**
|
||
* 加载成长路径数据
|
||
*/
|
||
const loadGrowthPath = async () => {
|
||
growthPathLoading.value = true
|
||
try {
|
||
const response = await getGrowthPath()
|
||
if (response.code === 200 && response.data) {
|
||
growthPathData.value = response.data
|
||
}
|
||
} catch (error) {
|
||
console.error('加载成长路径失败:', error)
|
||
// 失败时不显示错误,保持空状态
|
||
} finally {
|
||
growthPathLoading.value = false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 开始学习成长路径
|
||
*/
|
||
const handleStartGrowthPath = async () => {
|
||
if (!growthPathData.value) return
|
||
|
||
try {
|
||
const response = await startGrowthPath(growthPathData.value.id)
|
||
if (response.code === 200) {
|
||
ElMessage.success('已开始学习成长路径')
|
||
// 重新加载数据
|
||
await loadGrowthPath()
|
||
}
|
||
} catch (error: any) {
|
||
console.error('开始学习失败:', error)
|
||
ElMessage.error(error.response?.data?.detail || '开始学习失败')
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 初始化雷达图
|
||
*/
|
||
const initRadarChart = () => {
|
||
if (!radarChartRef.value) return
|
||
|
||
if (!radarChart) {
|
||
radarChart = echarts.init(radarChartRef.value)
|
||
}
|
||
|
||
const option = {
|
||
title: {
|
||
text: '能力评估雷达图',
|
||
left: 'center',
|
||
textStyle: {
|
||
fontSize: 16,
|
||
fontWeight: 'normal',
|
||
color: '#333'
|
||
}
|
||
},
|
||
radar: {
|
||
indicator: abilityData.value.map(item => ({
|
||
name: item.name,
|
||
max: item.max
|
||
})),
|
||
center: ['50%', '55%'],
|
||
radius: '65%',
|
||
axisName: {
|
||
fontSize: 12,
|
||
color: '#666'
|
||
},
|
||
splitArea: {
|
||
areaStyle: {
|
||
color: ['rgba(114, 172, 209, 0.2)', 'rgba(114, 172, 209, 0.4)']
|
||
}
|
||
}
|
||
},
|
||
series: [{
|
||
name: '能力评估',
|
||
type: 'radar',
|
||
data: [{
|
||
value: abilityData.value.map(item => item.value),
|
||
name: '当前能力',
|
||
areaStyle: {
|
||
color: 'rgba(103, 194, 58, 0.3)'
|
||
},
|
||
lineStyle: {
|
||
color: '#67c23a',
|
||
width: 2
|
||
},
|
||
itemStyle: {
|
||
color: '#67c23a'
|
||
}
|
||
}]
|
||
}]
|
||
}
|
||
|
||
radarChart.setOption(option)
|
||
}
|
||
|
||
/**
|
||
* AI 分析智能工牌数据
|
||
*/
|
||
const analyzeSmartBadgeData = async () => {
|
||
// 前置检查:只有学员用户且已绑定手机号才能分析
|
||
if (userInfo.value.role !== 'trainee') {
|
||
ElMessage.warning('该功能仅对学员开放')
|
||
return
|
||
}
|
||
|
||
if (!userInfo.value.phone) {
|
||
ElMessage.warning('请先在个人中心绑定手机号,以便使用智能工牌数据分析功能')
|
||
return
|
||
}
|
||
|
||
analyzing.value = true
|
||
|
||
try {
|
||
// 调用后端API分析智能工牌数据
|
||
const response = await analyzeYanjiBadge()
|
||
|
||
if (response.code === 200 && response.data) {
|
||
const { dimensions, recommended_courses, total_score, conversation_count } = response.data
|
||
|
||
// 更新能力雷达图数据
|
||
abilityData.value = dimensions.map(dim => ({
|
||
name: dim.name,
|
||
value: dim.score,
|
||
max: 100
|
||
}))
|
||
|
||
// 保存AI分析反馈(包含详细的分析意见)
|
||
abilityFeedback.value = dimensions
|
||
|
||
// 重新初始化雷达图
|
||
initRadarChart()
|
||
|
||
// 更新推荐课程列表
|
||
// 从Dify获取推荐后,查询课程详情补充完整信息
|
||
const coursePromises = recommended_courses.map(async (rec) => {
|
||
try {
|
||
// 查询课程详情
|
||
const courseResponse = await getCourseDetail(rec.course_id)
|
||
const courseDetail = courseResponse.data
|
||
|
||
return {
|
||
id: rec.course_id,
|
||
name: rec.course_name,
|
||
description: rec.recommendation_reason, // 使用AI推荐理由作为描述
|
||
duration: courseDetail?.duration_hours || 0,
|
||
difficulty: courseDetail?.difficulty_level || 'intermediate',
|
||
learnerCount: courseDetail?.learner_count || 0,
|
||
priority: rec.priority,
|
||
matchScore: rec.match_score,
|
||
targetWeakPoints: [], // TODO: 从recommendation_reason中提取
|
||
expectedImprovement: 10, // TODO: 从recommendation_reason中提取
|
||
progress: 0,
|
||
examPassed: false
|
||
}
|
||
} catch (error) {
|
||
console.error(`查询课程${rec.course_id}详情失败:`, error)
|
||
// 失败时使用基本信息
|
||
return {
|
||
id: rec.course_id,
|
||
name: rec.course_name,
|
||
description: rec.recommendation_reason,
|
||
duration: 0,
|
||
difficulty: 'intermediate',
|
||
learnerCount: 0,
|
||
priority: rec.priority,
|
||
matchScore: rec.match_score,
|
||
targetWeakPoints: [],
|
||
expectedImprovement: 10,
|
||
progress: 0,
|
||
examPassed: false
|
||
}
|
||
}
|
||
})
|
||
|
||
recommendedCourses.value = await Promise.all(coursePromises)
|
||
|
||
ElMessage.success(
|
||
`智能工牌数据分析完成!分析了${conversation_count}条对话记录,综合评分:${total_score}分`
|
||
)
|
||
} else {
|
||
throw new Error(response.message || '分析失败')
|
||
}
|
||
} catch (error: any) {
|
||
console.error('智能工牌数据分析失败:', error)
|
||
|
||
// 根据错误类型显示不同提示
|
||
if (error.response?.status === 404) {
|
||
ElMessage.warning('暂无智能工牌数据,请先使用智能工牌记录对话')
|
||
} else if (error.response?.status === 400) {
|
||
ElMessage.warning('用户未绑定手机号,无法匹配智能工牌数据')
|
||
} else {
|
||
ElMessage.error('AI分析失败,请稍后重试')
|
||
}
|
||
|
||
// 失败时使用模拟数据作为兜底
|
||
abilityData.value = [
|
||
{ name: '专业知识', value: 85, max: 100 },
|
||
{ name: '沟通技巧', value: 88, max: 100 },
|
||
{ name: '操作技能', value: 82, max: 100 },
|
||
{ name: '客户服务', value: 90, max: 100 },
|
||
{ name: '安全意识', value: 79, max: 100 },
|
||
{ name: '应变能力', value: 76, max: 100 }
|
||
]
|
||
initRadarChart()
|
||
} finally {
|
||
analyzing.value = false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 刷新AI推荐课程
|
||
* 重新调用AI分析接口,获取最新的课程推荐
|
||
*/
|
||
const refreshRecommendations = async () => {
|
||
// 直接调用AI分析智能工牌数据的方法,复用已有逻辑
|
||
await analyzeSmartBadgeData()
|
||
}
|
||
|
||
/**
|
||
* 获取节点图标
|
||
*/
|
||
const getNodeIcon = (status: string) => {
|
||
switch (status) {
|
||
case 'completed':
|
||
return 'CircleCheck'
|
||
case 'current':
|
||
return 'Clock'
|
||
case 'locked':
|
||
return 'Lock'
|
||
default:
|
||
return 'Clock'
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理节点点击
|
||
*/
|
||
const handleNodeClick = (node: any) => {
|
||
if (node.status === 'locked') {
|
||
ElMessage.warning('请先完成前置课程')
|
||
return
|
||
}
|
||
|
||
// 跳转到课程详情页
|
||
if (node.courseId) {
|
||
router.push(`/trainee/course-detail?id=${node.courseId}`)
|
||
} else {
|
||
ElMessage.info(`${node.title}`)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 跳转到个人中心
|
||
*/
|
||
const viewProfile = () => {
|
||
router.push('/user/profile')
|
||
}
|
||
|
||
/**
|
||
* 获取优先级文本
|
||
*/
|
||
const getPriorityText = (priority: string) => {
|
||
const textMap: Record<string, string> = {
|
||
high: '高优先级',
|
||
medium: '中优先级',
|
||
low: '低优先级'
|
||
}
|
||
return textMap[priority] || priority
|
||
}
|
||
|
||
/**
|
||
* 获取难度文本
|
||
*/
|
||
const getDifficultyText = (difficulty: string) => {
|
||
const textMap: Record<string, string> = {
|
||
easy: '简单',
|
||
medium: '中等',
|
||
hard: '困难'
|
||
}
|
||
return textMap[difficulty] || difficulty
|
||
}
|
||
|
||
/**
|
||
* 查看课程详情
|
||
*/
|
||
const viewCourseDetail = (course: any) => {
|
||
ElMessage.info(`查看课程详情:${course.name}`)
|
||
}
|
||
|
||
/**
|
||
* 进入课程
|
||
*/
|
||
const enterCourse = (course: any) => {
|
||
router.push('/trainee/course-detail?id=' + course.id)
|
||
}
|
||
|
||
/**
|
||
* 播放音频
|
||
*/
|
||
const playAudio = (course: any) => {
|
||
router.push('/trainee/audio-player?courseId=' + course.id)
|
||
}
|
||
|
||
/**
|
||
* 与课程对话
|
||
*/
|
||
const chatWithCourse = (course: any) => {
|
||
router.push('/trainee/chat-course?courseId=' + course.id)
|
||
}
|
||
|
||
/**
|
||
* 开始考试
|
||
*/
|
||
const startExam = (course: any) => {
|
||
if (course.progress < 80) {
|
||
ElMessage.warning('请先完成80%的学习进度')
|
||
return
|
||
}
|
||
router.push('/trainee/exam?courseId=' + course.id)
|
||
}
|
||
|
||
/**
|
||
* 开始陪练
|
||
*/
|
||
const startPractice = (course: any) => {
|
||
if (!course.examPassed) {
|
||
ElMessage.warning('请先通过考试')
|
||
return
|
||
}
|
||
router.push('/trainee/ai-practice?courseId=' + course.id)
|
||
}
|
||
|
||
/**
|
||
* 获取平均提升分数
|
||
*/
|
||
const getAverageImprovement = () => {
|
||
if (recommendedCourses.value.length === 0) return 0
|
||
const total = recommendedCourses.value.reduce((sum, course) => sum + course.expectedImprovement, 0)
|
||
return Math.round(total / recommendedCourses.value.length)
|
||
}
|
||
|
||
/**
|
||
* 获取总学时
|
||
*/
|
||
const getTotalHours = () => {
|
||
return recommendedCourses.value.reduce((sum, course) => sum + course.duration, 0)
|
||
}
|
||
|
||
/**
|
||
* 按优先级筛选
|
||
*/
|
||
const filterByPriority = () => {
|
||
// 筛选逻辑在计算属性中处理
|
||
}
|
||
|
||
/**
|
||
* 获取当前用户信息
|
||
*/
|
||
const fetchUserInfo = async () => {
|
||
try {
|
||
const response = await getCurrentUserProfile()
|
||
if (response.code === 200 && response.data) {
|
||
const user = response.data
|
||
userInfo.value = {
|
||
name: user.full_name || user.username || '未命名',
|
||
position: user.position_name || (user.role === 'admin' ? '管理员' : user.role === 'trainer' ? '培训师' : '学员'),
|
||
level: 1,
|
||
exp: 0,
|
||
nextLevelExp: 1000,
|
||
avatar: user.avatar_url || '',
|
||
role: user.role || 'trainee',
|
||
phone: user.phone || ''
|
||
}
|
||
|
||
// 获取等级信息
|
||
try {
|
||
const { getMyLevel } = await import('@/api/level')
|
||
const levelResponse = await getMyLevel()
|
||
if (levelResponse.code === 200 && levelResponse.data) {
|
||
const levelData = levelResponse.data
|
||
userInfo.value.level = levelData.level
|
||
userInfo.value.exp = levelData.total_exp
|
||
userInfo.value.nextLevelExp = levelData.next_level_exp || 1000
|
||
}
|
||
} catch (levelError) {
|
||
console.warn('获取等级信息失败:', levelError)
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('获取用户信息失败:', error)
|
||
ElMessage.warning('获取用户信息失败,使用默认信息')
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理窗口大小变化
|
||
*/
|
||
const handleResize = () => {
|
||
if (radarChart) {
|
||
radarChart.resize()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 组件挂载时初始化
|
||
*/
|
||
onMounted(async () => {
|
||
// 获取用户信息
|
||
await fetchUserInfo()
|
||
|
||
// 加载成长路径数据
|
||
await loadGrowthPath()
|
||
|
||
nextTick(() => {
|
||
initRadarChart()
|
||
})
|
||
|
||
// 只有学员用户且已绑定手机号才自动分析智能工牌数据
|
||
if (userInfo.value.role === 'trainee' && userInfo.value.phone) {
|
||
analyzeSmartBadgeData()
|
||
} else if (!userInfo.value.phone && userInfo.value.role === 'trainee') {
|
||
// 学员未绑定手机号时显示提示
|
||
ElMessage.info('请先在个人中心绑定手机号,以便使用智能工牌数据分析功能')
|
||
}
|
||
|
||
// 响应式处理
|
||
window.addEventListener('resize', handleResize)
|
||
})
|
||
|
||
/**
|
||
* 组件卸载时清理资源
|
||
*/
|
||
onUnmounted(() => {
|
||
// 销毁图表实例
|
||
radarChart?.dispose()
|
||
|
||
// 移除事件监听
|
||
window.removeEventListener('resize', handleResize)
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.growth-path-container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
|
||
.personal-info {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 24px;
|
||
margin-bottom: 24px;
|
||
|
||
.info-left {
|
||
display: flex;
|
||
gap: 24px;
|
||
align-items: center;
|
||
|
||
.info-content {
|
||
.user-name {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.user-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 14px;
|
||
color: #666;
|
||
margin-bottom: 12px;
|
||
|
||
.separator {
|
||
color: #ddd;
|
||
}
|
||
|
||
.position {
|
||
color: #667eea;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.level {
|
||
color: #e6a23c;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.el-progress {
|
||
width: 300px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.info-right {
|
||
.el-button {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
border: none;
|
||
}
|
||
}
|
||
}
|
||
|
||
.main-content {
|
||
display: grid;
|
||
grid-template-columns: 400px 1fr;
|
||
gap: 24px;
|
||
margin-bottom: 24px;
|
||
|
||
.ability-radar {
|
||
height: fit-content;
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20px 24px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
|
||
.card-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
}
|
||
|
||
.radar-chart {
|
||
padding: 24px;
|
||
height: 350px;
|
||
width: 100%;
|
||
}
|
||
|
||
.ability-feedback {
|
||
padding: 24px;
|
||
border-top: 1px solid #f0f0f0;
|
||
|
||
.feedback-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 16px;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
|
||
.el-icon {
|
||
color: #667eea;
|
||
}
|
||
}
|
||
|
||
.feedback-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.feedback-item {
|
||
padding: 16px;
|
||
border-radius: 8px;
|
||
background: #fafafa;
|
||
border-left: 4px solid #94a3b8;
|
||
transition: all 0.3s ease;
|
||
|
||
&:hover {
|
||
background: #f5f5f5;
|
||
transform: translateX(4px);
|
||
}
|
||
|
||
&.weak {
|
||
border-left-color: #ef4444;
|
||
background: #fef2f2;
|
||
|
||
&:hover {
|
||
background: #fee2e2;
|
||
}
|
||
}
|
||
|
||
&.good {
|
||
border-left-color: #f59e0b;
|
||
background: #fffbeb;
|
||
|
||
&:hover {
|
||
background: #fef3c7;
|
||
}
|
||
}
|
||
|
||
&.excellent {
|
||
border-left-color: #10b981;
|
||
background: #f0fdf4;
|
||
|
||
&:hover {
|
||
background: #dcfce7;
|
||
}
|
||
}
|
||
|
||
.feedback-header-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
|
||
.dimension-name {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.dimension-score {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #667eea;
|
||
}
|
||
}
|
||
|
||
.feedback-text {
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
color: #666;
|
||
margin: 0;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// 成长路径卡片样式
|
||
.growth-tree {
|
||
margin-top: 24px;
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20px 24px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
|
||
.card-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.legend {
|
||
display: flex;
|
||
gap: 16px;
|
||
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 12px;
|
||
color: #666;
|
||
|
||
.dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
display: inline-block;
|
||
}
|
||
|
||
&.completed .dot {
|
||
background: #67c23a;
|
||
}
|
||
|
||
&.current .dot {
|
||
background: #409eff;
|
||
}
|
||
|
||
&.locked .dot {
|
||
background: #c0c4cc;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.tree-container {
|
||
padding: 24px;
|
||
|
||
.tree-level {
|
||
margin-bottom: 32px;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.level-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
padding: 14px 20px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
border-radius: 10px;
|
||
box-shadow: 0 4px 10px rgba(102, 126, 234, 0.2);
|
||
|
||
.level-name {
|
||
font-size: 17px;
|
||
font-weight: 600;
|
||
color: #fff;
|
||
}
|
||
|
||
.level-progress {
|
||
font-size: 14px;
|
||
color: rgba(255, 255, 255, 0.9);
|
||
background: rgba(255, 255, 255, 0.2);
|
||
padding: 4px 12px;
|
||
border-radius: 12px;
|
||
}
|
||
}
|
||
|
||
.level-nodes {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||
gap: 20px;
|
||
|
||
.tree-node {
|
||
padding: 24px;
|
||
border: 2px solid #e4e7ed;
|
||
border-radius: 12px;
|
||
background: #fff;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
position: relative;
|
||
overflow: hidden;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 4px;
|
||
height: 100%;
|
||
background: #409eff;
|
||
transform: scaleY(0);
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
&:hover {
|
||
transform: translateY(-4px);
|
||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
|
||
|
||
&::before {
|
||
transform: scaleY(1);
|
||
}
|
||
}
|
||
|
||
&.completed {
|
||
border-color: #67c23a;
|
||
background: linear-gradient(135deg, rgba(103, 194, 58, 0.05) 0%, rgba(103, 194, 58, 0.02) 100%);
|
||
|
||
&::before {
|
||
background: #67c23a;
|
||
}
|
||
}
|
||
|
||
&.current {
|
||
border-color: #409eff;
|
||
background: linear-gradient(135deg, rgba(64, 158, 255, 0.08) 0%, rgba(64, 158, 255, 0.03) 100%);
|
||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
|
||
|
||
&::before {
|
||
background: #409eff;
|
||
transform: scaleY(1);
|
||
}
|
||
}
|
||
|
||
&.locked {
|
||
border-color: #dcdfe6;
|
||
background: #f9fafb;
|
||
cursor: not-allowed;
|
||
opacity: 0.7;
|
||
|
||
&:hover {
|
||
transform: none;
|
||
box-shadow: none;
|
||
}
|
||
|
||
&::before {
|
||
background: #c0c4cc;
|
||
}
|
||
}
|
||
|
||
.node-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 12px;
|
||
background: rgba(64, 158, 255, 0.1);
|
||
color: #409eff;
|
||
margin-bottom: 16px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
&.completed .node-icon {
|
||
background: rgba(103, 194, 58, 0.1);
|
||
color: #67c23a;
|
||
}
|
||
|
||
&.locked .node-icon {
|
||
background: rgba(192, 196, 204, 0.1);
|
||
color: #c0c4cc;
|
||
}
|
||
|
||
&:hover .node-icon {
|
||
transform: scale(1.1) rotate(5deg);
|
||
}
|
||
|
||
.node-content {
|
||
.node-title {
|
||
font-size: 17px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
margin-bottom: 10px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.node-desc {
|
||
font-size: 14px;
|
||
color: #606266;
|
||
line-height: 1.6;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.node-progress {
|
||
margin-top: 16px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid #f0f0f0;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// AI 智能学习助手样式
|
||
.ai-learning-hub-inner {
|
||
margin-bottom: 32px;
|
||
background: linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(248,250,252,0.9) 100%);
|
||
border-radius: 24px;
|
||
padding: 32px;
|
||
box-shadow: 0 20px 40px rgba(0,0,0,0.08);
|
||
border: 1px solid rgba(255,255,255,0.2);
|
||
backdrop-filter: blur(20px);
|
||
position: relative;
|
||
overflow: hidden;
|
||
|
||
&:before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background:
|
||
radial-gradient(circle at 20% 10%, rgba(102,126,234,0.1) 0%, transparent 40%),
|
||
radial-gradient(circle at 80% 90%, rgba(118,75,162,0.08) 0%, transparent 40%);
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
}
|
||
|
||
> * {
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
// 模块头部样式
|
||
.hub-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 32px;
|
||
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
|
||
.ai-avatar {
|
||
position: relative;
|
||
width: 60px;
|
||
height: 60px;
|
||
border-radius: 50%;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
|
||
.avatar-glow {
|
||
position: absolute;
|
||
inset: -4px;
|
||
border-radius: 50%;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
opacity: 0.3;
|
||
filter: blur(8px);
|
||
animation: pulse 2s ease-in-out infinite;
|
||
}
|
||
|
||
.el-icon {
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
}
|
||
|
||
.header-text {
|
||
.hub-title {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
background: linear-gradient(135deg, #2d3748 0%, #667eea 100%);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.hub-subtitle {
|
||
font-size: 14px;
|
||
color: #64748b;
|
||
margin: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
.header-actions {
|
||
.refresh-btn {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
border: none;
|
||
border-radius: 12px;
|
||
padding: 12px 20px;
|
||
font-weight: 600;
|
||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||
|
||
&:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 推荐统计样式
|
||
.recommendation-stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 20px;
|
||
margin-bottom: 32px;
|
||
|
||
.stat-card {
|
||
background: linear-gradient(135deg, rgba(255,255,255,0.9) 0%, rgba(248,250,252,0.8) 100%);
|
||
border-radius: 16px;
|
||
padding: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
border: 1px solid rgba(255,255,255,0.3);
|
||
backdrop-filter: blur(10px);
|
||
transition: all 0.3s ease;
|
||
|
||
&:hover {
|
||
transform: translateY(-4px);
|
||
box-shadow: 0 12px 24px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.stat-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 12px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
font-size: 20px;
|
||
}
|
||
|
||
.stat-content {
|
||
.stat-number {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: #2d3748;
|
||
line-height: 1;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 12px;
|
||
color: #64748b;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 推荐课程区域样式
|
||
.recommendations-section {
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 24px;
|
||
|
||
.section-title {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: #2d3748;
|
||
margin: 0;
|
||
}
|
||
|
||
.filter-tabs {
|
||
.el-radio-group {
|
||
background: rgba(255,255,255,0.8);
|
||
border-radius: 12px;
|
||
padding: 4px;
|
||
backdrop-filter: blur(10px);
|
||
|
||
:deep(.el-radio-button__inner) {
|
||
border: none;
|
||
background: transparent;
|
||
border-radius: 8px;
|
||
padding: 8px 16px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
:deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 课程网格样式
|
||
.course-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||
gap: 24px;
|
||
|
||
.smart-course-card {
|
||
background: linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(248,250,252,0.9) 100%);
|
||
border-radius: 16px;
|
||
padding: 16px;
|
||
border: 1px solid rgba(255,255,255,0.3);
|
||
backdrop-filter: blur(15px);
|
||
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||
cursor: pointer;
|
||
position: relative;
|
||
overflow: hidden;
|
||
animation: slideUp 0.6s ease-out both;
|
||
animation-delay: var(--delay);
|
||
|
||
&:before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: linear-gradient(135deg, rgba(102,126,234,0.05) 0%, rgba(118,75,162,0.03) 100%);
|
||
opacity: 0;
|
||
transition: opacity 0.3s ease;
|
||
}
|
||
|
||
&:hover {
|
||
transform: translateY(-8px) scale(1.02);
|
||
box-shadow: 0 20px 40px rgba(0,0,0,0.15);
|
||
border-color: rgba(102, 126, 234, 0.3);
|
||
|
||
&:before {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
&.featured {
|
||
border: 2px solid rgba(102, 126, 234, 0.3);
|
||
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.15);
|
||
}
|
||
}
|
||
|
||
// 智能课程卡片内部样式
|
||
.smart-course-card {
|
||
// 卡片装饰
|
||
.card-decoration {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 4px;
|
||
z-index: 2;
|
||
|
||
.decoration-line {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
||
border-radius: 20px 20px 0 0;
|
||
}
|
||
|
||
.priority-indicator {
|
||
position: absolute;
|
||
top: -2px;
|
||
right: 20px;
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
font-size: 12px;
|
||
|
||
&.high {
|
||
background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%);
|
||
box-shadow: 0 4px 12px rgba(245, 101, 101, 0.4);
|
||
}
|
||
|
||
&.medium {
|
||
background: linear-gradient(135deg, #ed8936 0%, #dd6b20 100%);
|
||
box-shadow: 0 4px 12px rgba(237, 137, 54, 0.4);
|
||
}
|
||
|
||
&.low {
|
||
background: linear-gradient(135deg, #a0aec0 0%, #718096 100%);
|
||
box-shadow: 0 4px 12px rgba(160, 174, 192, 0.4);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 智能封面
|
||
.smart-cover {
|
||
position: relative;
|
||
height: 80px;
|
||
margin: -16px -16px 16px -16px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 70%, #f093fb 100%);
|
||
overflow: hidden;
|
||
border-radius: 16px 16px 0 0;
|
||
|
||
.cover-gradient {
|
||
position: absolute;
|
||
inset: 0;
|
||
background:
|
||
radial-gradient(circle at 30% 20%, rgba(255,255,255,0.3) 0%, transparent 50%),
|
||
radial-gradient(circle at 80% 80%, rgba(255,255,255,0.2) 0%, transparent 50%);
|
||
}
|
||
|
||
.cover-content {
|
||
position: relative;
|
||
z-index: 1;
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 16px;
|
||
|
||
.course-icon {
|
||
color: rgba(255,255,255,0.9);
|
||
filter: drop-shadow(0 2px 8px rgba(0,0,0,0.2));
|
||
|
||
.el-icon {
|
||
font-size: 28px;
|
||
}
|
||
}
|
||
|
||
.match-score {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 8px;
|
||
|
||
.score-ring {
|
||
position: relative;
|
||
width: 36px;
|
||
height: 36px;
|
||
|
||
.score-circle {
|
||
width: 100%;
|
||
height: 100%;
|
||
|
||
.circle-bg {
|
||
fill: none;
|
||
stroke: rgba(255,255,255,0.3);
|
||
stroke-width: 2;
|
||
}
|
||
|
||
.circle {
|
||
fill: none;
|
||
stroke: #fff;
|
||
stroke-width: 2;
|
||
stroke-linecap: round;
|
||
transform: rotate(-90deg);
|
||
transform-origin: 50% 50%;
|
||
transition: stroke-dasharray 0.6s ease;
|
||
}
|
||
}
|
||
|
||
.score-text {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: white;
|
||
}
|
||
}
|
||
|
||
.score-label {
|
||
font-size: 10px;
|
||
color: rgba(255,255,255,0.8);
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 智能信息区域
|
||
.smart-info {
|
||
.course-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 8px;
|
||
|
||
.course-title {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: #2d3748;
|
||
line-height: 1.4;
|
||
flex: 1;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.priority-tag {
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
padding: 4px 8px;
|
||
border-radius: 12px;
|
||
white-space: nowrap;
|
||
|
||
&.high {
|
||
background: linear-gradient(135deg, rgba(245, 101, 101, 0.15) 0%, rgba(229, 62, 62, 0.1) 100%);
|
||
color: #e53e3e;
|
||
border: 1px solid rgba(245, 101, 101, 0.2);
|
||
}
|
||
|
||
&.medium {
|
||
background: linear-gradient(135deg, rgba(237, 137, 54, 0.15) 0%, rgba(221, 107, 32, 0.1) 100%);
|
||
color: #dd6b20;
|
||
border: 1px solid rgba(237, 137, 54, 0.2);
|
||
}
|
||
|
||
&.low {
|
||
background: linear-gradient(135deg, rgba(160, 174, 192, 0.15) 0%, rgba(113, 128, 150, 0.1) 100%);
|
||
color: #718096;
|
||
border: 1px solid rgba(160, 174, 192, 0.2);
|
||
}
|
||
}
|
||
}
|
||
|
||
.course-description {
|
||
font-size: 13px;
|
||
color: #64748b;
|
||
line-height: 1.5;
|
||
margin-bottom: 12px;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
}
|
||
|
||
// AI分析结果
|
||
.ai-analysis {
|
||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.05) 100%);
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
margin-bottom: 12px;
|
||
border: 1px solid rgba(102, 126, 234, 0.15);
|
||
position: relative;
|
||
|
||
&:before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 3px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
border-radius: 0 2px 2px 0;
|
||
}
|
||
|
||
.analysis-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #667eea;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.analysis-content {
|
||
.weakness-tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
margin-bottom: 10px;
|
||
|
||
.el-tag {
|
||
font-size: 11px;
|
||
border-radius: 6px;
|
||
|
||
.el-icon {
|
||
font-size: 10px;
|
||
margin-right: 4px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.improvement-badge {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 12px;
|
||
color: #10b981;
|
||
font-weight: 600;
|
||
|
||
.el-icon {
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 课程元数据
|
||
.course-metadata {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 12px;
|
||
|
||
.meta-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 12px;
|
||
color: #64748b;
|
||
|
||
.el-icon {
|
||
font-size: 14px;
|
||
color: #94a3b8;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 学习进度
|
||
.progress-section {
|
||
margin-bottom: 12px;
|
||
|
||
.progress-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
|
||
.progress-label {
|
||
font-size: 12px;
|
||
color: #64748b;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.progress-value {
|
||
font-size: 12px;
|
||
color: #667eea;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 智能操作按钮
|
||
.smart-actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
|
||
.primary-action {
|
||
width: 100%;
|
||
height: 44px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
border: none;
|
||
border-radius: 22px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: white;
|
||
transition: all 0.3s ease;
|
||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||
|
||
&:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
||
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||
}
|
||
|
||
.el-icon {
|
||
margin-right: 8px;
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
.action-grid {
|
||
display: grid !important;
|
||
grid-template-columns: repeat(2, 1fr) !important;
|
||
grid-template-rows: repeat(2, 38px) !important;
|
||
gap: 10px !important;
|
||
align-items: stretch !important;
|
||
justify-items: stretch !important;
|
||
width: 100% !important;
|
||
|
||
// 重置所有可能的Element Plus样式
|
||
:deep(.el-button) {
|
||
width: 100% !important;
|
||
height: 38px !important;
|
||
min-height: 38px !important;
|
||
max-height: 38px !important;
|
||
margin: 0 !important;
|
||
padding: 0 16px !important;
|
||
border-radius: 19px !important;
|
||
font-size: 12px !important;
|
||
line-height: 1 !important;
|
||
box-sizing: border-box !important;
|
||
}
|
||
|
||
.action-btn.el-button {
|
||
width: 100% !important;
|
||
height: 38px !important;
|
||
min-height: 38px !important;
|
||
max-height: 38px !important;
|
||
border: 1px solid #e2e8f0 !important;
|
||
border-radius: 19px !important;
|
||
font-size: 12px !important;
|
||
font-weight: 500 !important;
|
||
line-height: 1 !important;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||
background: #ffffff !important;
|
||
color: #64748b !important;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) !important;
|
||
display: flex !important;
|
||
align-items: center !important;
|
||
justify-content: center !important;
|
||
position: relative !important;
|
||
overflow: hidden !important;
|
||
cursor: pointer !important;
|
||
padding: 0 16px !important;
|
||
margin: 0 !important;
|
||
|
||
// 添加涟漪效果
|
||
&:before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
width: 0;
|
||
height: 0;
|
||
border-radius: 50%;
|
||
background: rgba(102, 126, 234, 0.2);
|
||
transform: translate(-50%, -50%);
|
||
transition: all 0.4s ease;
|
||
pointer-events: none;
|
||
}
|
||
|
||
&:active:before {
|
||
width: 100px;
|
||
height: 100px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
&:hover {
|
||
transform: translateY(-2px) scale(1.02) !important;
|
||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15) !important;
|
||
background: #f8fafc !important;
|
||
border-color: #cbd5e1 !important;
|
||
}
|
||
|
||
&:active {
|
||
transform: translateY(0) scale(0.98) !important;
|
||
transition: transform 0.1s ease !important;
|
||
}
|
||
|
||
.el-icon {
|
||
margin-right: 6px !important;
|
||
font-size: 14px !important;
|
||
color: #94a3b8 !important;
|
||
position: relative !important;
|
||
z-index: 2 !important;
|
||
flex-shrink: 0 !important;
|
||
}
|
||
|
||
// 确保文字不换行
|
||
span {
|
||
position: relative !important;
|
||
z-index: 2 !important;
|
||
white-space: nowrap !important;
|
||
overflow: hidden !important;
|
||
text-overflow: ellipsis !important;
|
||
flex: 1 !important;
|
||
text-align: center !important;
|
||
}
|
||
|
||
// 考试按钮特殊样式
|
||
&.exam-btn {
|
||
background: #fffbeb !important;
|
||
color: #92400e !important;
|
||
border: 1px solid #fbbf24 !important;
|
||
height: 38px !important;
|
||
min-height: 38px !important;
|
||
max-height: 38px !important;
|
||
|
||
&:before {
|
||
background: rgba(251, 191, 36, 0.2) !important;
|
||
}
|
||
|
||
&:hover {
|
||
background: #fef3cd !important;
|
||
border-color: #f59e0b !important;
|
||
box-shadow: 0 6px 20px rgba(251, 191, 36, 0.2) !important;
|
||
transform: translateY(-2px) scale(1.02) !important;
|
||
}
|
||
|
||
.el-icon {
|
||
color: #d97706 !important;
|
||
margin-right: 6px !important;
|
||
font-size: 14px !important;
|
||
}
|
||
}
|
||
|
||
// 陪练按钮特殊样式
|
||
&.practice-btn {
|
||
background: #f0fdf4 !important;
|
||
color: #065f46 !important;
|
||
border: 1px solid #34d399 !important;
|
||
height: 38px !important;
|
||
min-height: 38px !important;
|
||
max-height: 38px !important;
|
||
|
||
&:before {
|
||
background: rgba(52, 211, 153, 0.2) !important;
|
||
}
|
||
|
||
&:hover {
|
||
background: #dcfce7 !important;
|
||
border-color: #10b981 !important;
|
||
box-shadow: 0 6px 20px rgba(52, 211, 153, 0.2) !important;
|
||
transform: translateY(-2px) scale(1.02) !important;
|
||
}
|
||
|
||
.el-icon {
|
||
color: #059669 !important;
|
||
margin-right: 6px !important;
|
||
font-size: 14px !important;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 空状态样式
|
||
.empty-recommendations {
|
||
text-align: center;
|
||
padding: 60px 20px;
|
||
|
||
.empty-icon {
|
||
margin-bottom: 24px;
|
||
color: #94a3b8;
|
||
|
||
.rotating {
|
||
animation: rotate 2s linear infinite;
|
||
}
|
||
}
|
||
|
||
.empty-title {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: #475569;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.empty-description {
|
||
color: #64748b;
|
||
margin-bottom: 32px;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 动画定义
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 0.3; }
|
||
50% { opacity: 0.6; }
|
||
}
|
||
|
||
@keyframes rotate {
|
||
from {
|
||
transform: rotate(0deg);
|
||
}
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
@keyframes slideUp {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(30px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 响应式设计
|
||
@media (max-width: 768px) {
|
||
.growth-path-container {
|
||
.personal-info {
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
|
||
.info-left {
|
||
flex-direction: column;
|
||
text-align: center;
|
||
|
||
.info-content {
|
||
.el-progress {
|
||
width: 100%;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.main-content {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.tree-container {
|
||
.level-nodes {
|
||
grid-template-columns: 1fr !important;
|
||
}
|
||
}
|
||
|
||
.ai-learning-hub-inner {
|
||
padding: 20px;
|
||
|
||
.hub-header {
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
text-align: center;
|
||
|
||
.header-left {
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
}
|
||
|
||
.recommendation-stats {
|
||
grid-template-columns: 1fr;
|
||
gap: 16px;
|
||
}
|
||
|
||
.course-grid {
|
||
grid-template-columns: 1fr;
|
||
gap: 20px;
|
||
}
|
||
|
||
.recommendations-section {
|
||
.section-header {
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
text-align: center;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 手机端深度优化
|
||
@media (max-width: 480px) {
|
||
.growth-path-container {
|
||
padding: 12px;
|
||
|
||
.personal-info {
|
||
padding: 16px;
|
||
border-radius: 12px;
|
||
|
||
.info-left {
|
||
.el-avatar {
|
||
width: 64px !important;
|
||
height: 64px !important;
|
||
}
|
||
|
||
.info-content {
|
||
.user-name {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.user-meta {
|
||
font-size: 12px;
|
||
gap: 6px;
|
||
|
||
.separator {
|
||
margin: 0 4px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.info-right {
|
||
.el-button {
|
||
width: 100%;
|
||
}
|
||
}
|
||
}
|
||
|
||
.main-content {
|
||
gap: 16px;
|
||
margin-top: 16px;
|
||
|
||
.card {
|
||
padding: 14px;
|
||
border-radius: 12px;
|
||
|
||
.card-header {
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
|
||
.card-title {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.el-button {
|
||
width: 100%;
|
||
}
|
||
}
|
||
}
|
||
|
||
.ability-radar {
|
||
.radar-chart {
|
||
height: 260px;
|
||
}
|
||
|
||
.ability-feedback {
|
||
.feedback-item {
|
||
padding: 12px;
|
||
|
||
.feedback-header-row {
|
||
.dimension-name {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.dimension-score {
|
||
font-size: 13px;
|
||
}
|
||
}
|
||
|
||
.feedback-text {
|
||
font-size: 12px;
|
||
line-height: 1.6;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.ai-learning-hub-inner {
|
||
padding: 16px;
|
||
border-radius: 12px;
|
||
|
||
.hub-header {
|
||
.ai-avatar {
|
||
width: 48px;
|
||
height: 48px;
|
||
}
|
||
|
||
.header-text {
|
||
.hub-title {
|
||
font-size: 17px;
|
||
}
|
||
|
||
.hub-subtitle {
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
|
||
.header-actions {
|
||
width: 100%;
|
||
|
||
.refresh-btn {
|
||
width: 100%;
|
||
}
|
||
}
|
||
}
|
||
|
||
.recommendation-stats {
|
||
.stat-card {
|
||
padding: 12px;
|
||
|
||
.stat-content {
|
||
.stat-number {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 11px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.recommendations-section {
|
||
.section-header {
|
||
.section-title {
|
||
font-size: 15px;
|
||
}
|
||
|
||
.filter-tabs {
|
||
width: 100%;
|
||
overflow-x: auto;
|
||
|
||
.el-radio-group {
|
||
flex-wrap: nowrap;
|
||
|
||
:deep(.el-radio-button__inner) {
|
||
padding: 6px 12px;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.course-grid {
|
||
gap: 14px;
|
||
|
||
.smart-course-card {
|
||
padding: 14px;
|
||
border-radius: 12px;
|
||
|
||
.card-content {
|
||
.course-header {
|
||
.course-name {
|
||
font-size: 15px;
|
||
}
|
||
}
|
||
|
||
.course-reason {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.course-meta {
|
||
gap: 6px;
|
||
|
||
.meta-tag {
|
||
font-size: 11px;
|
||
padding: 3px 8px;
|
||
}
|
||
}
|
||
|
||
.action-row {
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
|
||
.improvement-badge,
|
||
.el-button {
|
||
width: 100%;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|