ZhangQiPro/src/views/ReportAnalysis.vue
sladro ce76c1a37a refactor: 完善CSS变量系统并优化报告功能
- 新增紫色和状态评分颜色CSS变量到variables.css
- 修复ReportCenter评分颜色硬编码问题,改用CSS变量
- 修复ReportAnalysis中紫色硬编码,使用CSS变量
- 优化Dashboard边框样式和Report页面布局
- 更新mockData中学生姓名为更真实的中文姓名

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-15 16:20:14 +08:00

915 lines
22 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="analysis-page">
<!-- 返回按钮和标题 -->
<header class="page-header">
<div class="header-left">
<el-button icon="ArrowLeft" @click="goBack" plain>
返回报告
</el-button>
<div class="page-title-section">
<h1 class="page-title">能力分析</h1>
<p class="page-subtitle">{{ currentStudent?.name }} 的多维度能力分析与建议</p>
</div>
</div>
<div class="page-actions">
<el-button type="primary" icon="Download" @click="exportAnalysis">
导出分析报告
</el-button>
</div>
</header>
<!-- 能力雷达图 -->
<BaseCard class="radar-card">
<div class="card-header">
<h3 class="card-title">
<el-icon><PieChart /></el-icon>
六维能力雷达图
</h3>
<div class="score-summary">
<span class="summary-label">综合能力指数</span>
<span class="summary-score">{{ overallAbilityScore }}</span>
</div>
</div>
<div class="radar-container">
<div ref="radarChartRef" class="chart"></div>
<div class="ability-legend">
<div
v-for="(ability, index) in abilityDimensions"
:key="ability.name"
class="legend-item"
>
<div class="legend-color" :style="{ backgroundColor: ability.color }"></div>
<div class="legend-content">
<span class="legend-name">{{ ability.name }}</span>
<span class="legend-score">{{ ability.score }}</span>
</div>
</div>
</div>
</div>
</BaseCard>
<!-- 能力分析详情 -->
<div class="analysis-grid">
<BaseCard
v-for="ability in abilityDimensions"
:key="ability.name"
class="ability-card"
>
<div class="ability-header">
<div class="ability-icon" :style="{ backgroundColor: ability.color }">
<el-icon size="24">
<Cpu v-if="ability.name === '技术能力'" />
<ChatLineRound v-else-if="ability.name === '沟通协调'" />
<UserFilled v-else-if="ability.name === '团队协作'" />
<Tools v-else-if="ability.name === '问题解决'" />
<Reading v-else-if="ability.name === '学习能力'" />
<MagicStick v-else-if="ability.name === '创新思维'" />
<Star v-else />
</el-icon>
</div>
<div class="ability-info">
<h4 class="ability-name">{{ ability.name }}</h4>
<div class="ability-score-bar">
<div class="score-track">
<div
class="score-fill"
:style="{
width: `${ability.score}%`,
backgroundColor: ability.color
}"
></div>
</div>
<span class="score-text">{{ ability.score }}/100</span>
</div>
</div>
</div>
<div class="ability-details">
<div class="ability-level">
<span class="level-label">当前水平:</span>
<el-tag
:type="getAbilityLevel(ability.score).type"
class="level-tag"
>
{{ getAbilityLevel(ability.score).text }}
</el-tag>
</div>
<p class="ability-desc">{{ ability.description }}</p>
<div class="ability-suggestion">
<h5 class="suggestion-title">改进建议:</h5>
<ul class="suggestion-list">
<li v-for="suggestion in ability.suggestions" :key="suggestion">
{{ suggestion }}
</li>
</ul>
</div>
</div>
</BaseCard>
</div>
<!-- 优势与劣势分析 -->
<div class="strength-weakness-grid">
<BaseCard class="strength-card">
<div class="analysis-header">
<h3 class="analysis-title">
<el-icon><TrophyBase /></el-icon>
优势能力
</h3>
</div>
<div class="strength-list">
<div
v-for="strength in strengths"
:key="strength.name"
class="strength-item"
>
<div class="strength-icon">
<el-icon><Check /></el-icon>
</div>
<div class="strength-content">
<h4 class="strength-name">{{ strength.name }}</h4>
<p class="strength-desc">{{ strength.description }}</p>
</div>
<div class="strength-score">{{ strength.score }}</div>
</div>
</div>
</BaseCard>
<BaseCard class="weakness-card">
<div class="analysis-header">
<h3 class="analysis-title">
<el-icon><Warning /></el-icon>
待提升能力
</h3>
</div>
<div class="weakness-list">
<div
v-for="weakness in weaknesses"
:key="weakness.name"
class="weakness-item"
>
<div class="weakness-icon">
<el-icon><ArrowUp /></el-icon>
</div>
<div class="weakness-content">
<h4 class="weakness-name">{{ weakness.name }}</h4>
<p class="weakness-desc">{{ weakness.description }}</p>
</div>
<div class="weakness-score">{{ weakness.score }}</div>
</div>
</div>
</BaseCard>
</div>
<!-- 发展建议 -->
<BaseCard class="recommendation-card">
<div class="recommendation-header">
<h3 class="recommendation-title">
<el-icon><Lightbulb /></el-icon>
个性化发展建议
</h3>
<p class="recommendation-subtitle">基于当前能力水平的针对性提升方案</p>
</div>
<div class="recommendation-content">
<div class="recommendation-section">
<h4 class="section-title">短期目标1-3个月</h4>
<ul class="recommendation-list">
<li v-for="goal in shortTermGoals" :key="goal">{{ goal }}</li>
</ul>
</div>
<div class="recommendation-section">
<h4 class="section-title">中期目标3-6个月</h4>
<ul class="recommendation-list">
<li v-for="goal in mediumTermGoals" :key="goal">{{ goal }}</li>
</ul>
</div>
<div class="recommendation-section">
<h4 class="section-title">长期目标6个月以上</h4>
<ul class="recommendation-list">
<li v-for="goal in longTermGoals" :key="goal">{{ goal }}</li>
</ul>
</div>
</div>
</BaseCard>
</div>
</template>
<script>
import { ref, computed, onMounted, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import * as echarts from 'echarts'
import BaseCard from '@/components/BaseCard.vue'
import { mockStudents, mockPortraitData, mockReportData } from '@/utils/mockData'
// 获取CSS变量值的工具函数
const getCSSVariable = (variable) => {
return getComputedStyle(document.documentElement).getPropertyValue(variable).trim()
}
const ABILITY_COLORS = [
getCSSVariable('--primary'),
getCSSVariable('--secondary'),
getCSSVariable('--success'),
getCSSVariable('--warning'),
getCSSVariable('--danger'),
getCSSVariable('--purple')
]
export default {
name: 'ReportAnalysis',
components: {
BaseCard
},
setup() {
const route = useRoute()
const router = useRouter()
const studentId = ref(null)
const currentStudent = ref(null)
const radarChartRef = ref(null)
let radarChart = null
const abilityDimensions = ref([
{
name: '技术能力',
score: 85,
color: ABILITY_COLORS[0],
description: '编程技能、工具使用、技术理解等方面表现优秀,具备扎实的专业基础。',
suggestions: [
'深入学习前沿技术框架',
'参与开源项目贡献代码',
'加强算法和数据结构学习'
]
},
{
name: '沟通协调',
score: 78,
color: ABILITY_COLORS[1],
description: '能够清晰表达想法,与团队成员保持良好沟通,具备一定的协调能力。',
suggestions: [
'多参与团队讨论和技术分享',
'练习公开演讲和汇报技巧',
'学习跨部门沟通方法'
]
},
{
name: '团队协作',
score: 82,
color: ABILITY_COLORS[2],
description: '团队合作意识强,能够有效配合完成项目任务,具备良好的集体荣誉感。',
suggestions: [
'主动承担团队责任',
'学习项目管理方法',
'培养领导力和影响力'
]
},
{
name: '问题解决',
score: 73,
color: ABILITY_COLORS[3],
description: '面对问题时能够积极思考解决方案,具备一定的分析和判断能力。',
suggestions: [
'加强逻辑思维训练',
'多参与复杂项目挑战',
'学习系统化问题分析方法'
]
},
{
name: '学习能力',
score: 88,
color: ABILITY_COLORS[4],
description: '学习新知识和技能的能力突出,能够快速适应新的工作环境和要求。',
suggestions: [
'建立系统的知识管理体系',
'培养深度学习习惯',
'加强实践与理论结合'
]
},
{
name: '创新思维',
score: 75,
color: ABILITY_COLORS[5],
description: '具备一定的创新意识和思维能力,能够提出新的想法和解决方案。',
suggestions: [
'多接触跨领域知识',
'参与创新项目和竞赛',
'培养批判性思维能力'
]
}
])
const overallAbilityScore = computed(() => {
const total = abilityDimensions.value.reduce((sum, ability) => sum + ability.score, 0)
return Math.round(total / abilityDimensions.value.length)
})
const strengths = computed(() => {
return abilityDimensions.value
.filter(ability => ability.score >= 80)
.sort((a, b) => b.score - a.score)
.map(ability => ({
name: ability.name,
score: ability.score,
description: `${ability.name}表现突出,在同龄人中处于领先水平`
}))
})
const weaknesses = computed(() => {
return abilityDimensions.value
.filter(ability => ability.score < 80)
.sort((a, b) => a.score - b.score)
.map(ability => ({
name: ability.name,
score: ability.score,
description: `${ability.name}有较大提升空间,建议重点关注`
}))
})
const shortTermGoals = ref([])
const mediumTermGoals = ref([])
const longTermGoals = ref([])
const getAbilityLevel = (score) => {
if (score >= 90) return { text: '优秀', type: 'success' }
if (score >= 80) return { text: '良好', type: 'primary' }
if (score >= 70) return { text: '中等', type: 'warning' }
return { text: '待提升', type: 'danger' }
}
const initRadarChart = () => {
if (!radarChartRef.value) return
radarChart = echarts.init(radarChartRef.value)
const radarData = abilityDimensions.value.map(ability => ({
name: ability.name,
max: 100
}))
const seriesData = abilityDimensions.value.map(ability => ability.score)
const option = {
tooltip: {
trigger: 'item',
formatter: (params) => {
return `${params.name}: ${params.value}`
}
},
radar: {
indicator: radarData,
center: ['50%', '50%'],
radius: '70%',
startAngle: 90,
splitNumber: 4,
shape: 'polygon',
axisName: {
color: getCSSVariable('--gray'),
fontSize: 12
},
splitLine: {
lineStyle: {
color: getCSSVariable('--border-light')
}
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(59, 130, 246, 0.05)', 'rgba(59, 130, 246, 0.02)']
}
},
axisLine: {
lineStyle: {
color: getCSSVariable('--border-light')
}
}
},
series: [
{
type: 'radar',
data: [
{
value: seriesData,
name: '能力评分',
areaStyle: {
color: new echarts.graphic.RadialGradient(0.5, 0.5, 0.5, [
{ offset: 0, color: 'rgba(30, 58, 138, 0.3)' },
{ offset: 1, color: 'rgba(30, 58, 138, 0.05)' }
])
},
lineStyle: {
color: getCSSVariable('--primary'),
width: 2
},
itemStyle: {
color: getCSSVariable('--primary'),
borderColor: getCSSVariable('--white'),
borderWidth: 2
}
}
]
}
]
}
radarChart.setOption(option)
}
const goBack = () => {
router.push(`/home/report/${studentId.value}`)
}
const exportAnalysis = () => {
ElMessage.success('分析报告导出功能开发中...')
}
onMounted(() => {
studentId.value = parseInt(route.params.studentId)
currentStudent.value = mockStudents.find(s => s.id === studentId.value)
if (!currentStudent.value) {
ElMessage.error('未找到学生信息')
router.push('/home')
return
}
// 使用Mock数据更新能力维度分数
const portraitData = mockPortraitData[studentId.value]
if (portraitData && portraitData.abilities) {
abilityDimensions.value.forEach((ability, index) => {
if (portraitData.abilities[index]) {
ability.score = portraitData.abilities[index]
}
})
}
// 加载发展建议数据
const suggestionData = mockReportData.developmentSuggestions[studentId.value]
if (suggestionData) {
shortTermGoals.value = suggestionData.shortTerm || []
mediumTermGoals.value = suggestionData.mediumTerm || []
longTermGoals.value = suggestionData.longTerm || []
}
nextTick(() => {
initRadarChart()
window.addEventListener('resize', () => {
radarChart?.resize()
})
})
})
return {
currentStudent,
abilityDimensions,
overallAbilityScore,
strengths,
weaknesses,
shortTermGoals,
mediumTermGoals,
longTermGoals,
radarChartRef,
getAbilityLevel,
goBack,
exportAnalysis
}
}
}
</script>
<style scoped>
.analysis-page {
flex: 1;
display: flex;
flex-direction: column;
overflow-x: hidden;
background: var(--bg-secondary);
gap: var(--spacing-lg);
padding: 0 calc(var(--spacing-xxl) * 1.2) var(--spacing-xxl);
}
/* 页面头部 */
.page-header {
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(20px);
border-bottom: 1px solid var(--border);
padding: var(--spacing-lg) calc(var(--spacing-xxl) * 1.2);
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: var(--shadow-sm);
margin: 0 calc(-1 * var(--spacing-xxl) * 1.2) var(--spacing-lg);
position: sticky;
top: 0;
z-index: 40;
}
.header-left {
display: flex;
align-items: center;
gap: var(--spacing-lg);
}
.page-title-section {
flex: 1;
}
.page-title {
font-size: calc(var(--font-size-xxl) * 1.3);
font-weight: 700;
color: var(--text-primary);
margin: 0;
background: var(--gradient-brand);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.page-subtitle {
font-size: var(--font-size-md);
color: var(--text-secondary);
margin: var(--spacing-xs) 0 0 0;
}
.page-actions {
display: flex;
gap: var(--spacing-md);
}
/* 雷达图卡片 */
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-lg);
}
.card-title {
display: flex;
align-items: center;
gap: var(--spacing-sm);
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.score-summary {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.summary-label {
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.summary-score {
font-size: var(--font-size-xl);
font-weight: 700;
color: var(--primary);
}
.radar-container {
display: grid;
grid-template-columns: 2fr 1fr;
gap: var(--spacing-xl);
height: 400px;
}
.chart {
width: 100%;
height: 100%;
}
.ability-legend {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
justify-content: center;
}
.legend-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 50%;
flex-shrink: 0;
}
.legend-content {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.legend-name {
color: var(--text-primary);
font-size: var(--font-size-sm);
}
.legend-score {
font-weight: 600;
color: var(--primary);
font-size: var(--font-size-sm);
}
/* 能力分析网格 */
.analysis-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: var(--spacing-lg);
margin-bottom: var(--spacing-lg);
}
.ability-card {
padding: var(--spacing-lg);
}
.ability-header {
display: flex;
align-items: flex-start;
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
.ability-icon {
width: 48px;
height: 48px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
color: var(--white);
flex-shrink: 0;
}
.ability-info {
flex: 1;
}
.ability-name {
font-size: var(--font-size-md);
font-weight: 600;
color: var(--text-primary);
margin: 0 0 var(--spacing-sm) 0;
}
.ability-score-bar {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.score-track {
flex: 1;
height: 8px;
background: var(--bg-secondary);
border-radius: var(--radius-lg);
overflow: hidden;
}
.score-fill {
height: 100%;
border-radius: var(--radius-lg);
transition: var(--transition);
}
.score-text {
font-size: var(--font-size-sm);
font-weight: 600;
color: var(--text-secondary);
min-width: 50px;
text-align: right;
}
.ability-details {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.ability-level {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.level-label {
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.level-tag {
font-size: var(--font-size-xs);
}
.ability-desc {
color: var(--text-secondary);
font-size: var(--font-size-sm);
line-height: 1.5;
margin: 0;
}
.suggestion-title {
color: var(--text-primary);
font-size: var(--font-size-sm);
font-weight: 600;
margin: 0 0 var(--spacing-xs) 0;
}
.suggestion-list {
margin: 0;
padding-left: var(--spacing-lg);
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.suggestion-list li {
margin-bottom: var(--spacing-xs);
line-height: 1.4;
}
/* 优势劣势分析 */
.strength-weakness-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-lg);
margin-bottom: var(--spacing-lg);
}
.analysis-header {
margin-bottom: var(--spacing-lg);
}
.analysis-title {
display: flex;
align-items: center;
gap: var(--spacing-sm);
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.strength-list,
.weakness-list {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.strength-item,
.weakness-item {
display: flex;
align-items: flex-start;
gap: var(--spacing-md);
padding: var(--spacing-md);
background: var(--bg-secondary);
border-radius: var(--radius-md);
}
.strength-icon,
.weakness-icon {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: var(--white);
flex-shrink: 0;
}
.strength-icon {
background: var(--gradient-card-success);
}
.weakness-icon {
background: var(--gradient-card-warning);
}
.strength-content,
.weakness-content {
flex: 1;
}
.strength-name,
.weakness-name {
font-size: var(--font-size-sm);
font-weight: 600;
color: var(--text-primary);
margin: 0 0 var(--spacing-xs) 0;
}
.strength-desc,
.weakness-desc {
color: var(--text-secondary);
font-size: var(--font-size-xs);
margin: 0;
}
.strength-score,
.weakness-score {
font-weight: 700;
color: var(--primary);
font-size: var(--font-size-md);
}
/* 发展建议 */
.recommendation-header {
margin-bottom: var(--spacing-xl);
}
.recommendation-title {
display: flex;
align-items: center;
gap: var(--spacing-sm);
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
margin: 0 0 var(--spacing-sm) 0;
}
.recommendation-subtitle {
color: var(--text-secondary);
margin: 0;
}
.recommendation-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: var(--spacing-xl);
}
.recommendation-section {
padding: var(--spacing-lg);
background: var(--bg-secondary);
border-radius: var(--radius-lg);
border-left: 4px solid var(--primary);
}
.section-title {
color: var(--text-primary);
font-size: var(--font-size-md);
font-weight: 600;
margin: 0 0 var(--spacing-md) 0;
}
.recommendation-list {
margin: 0;
padding-left: var(--spacing-lg);
color: var(--text-secondary);
}
.recommendation-list li {
margin-bottom: var(--spacing-sm);
line-height: 1.5;
}
/* 响应式调整 */
@media (max-width: 1200px) {
.radar-container {
grid-template-columns: 1fr;
height: auto;
}
.chart {
height: 300px;
}
.strength-weakness-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.page-header {
flex-direction: column;
gap: var(--spacing-md);
align-items: stretch;
}
.header-left {
flex-direction: column;
gap: var(--spacing-sm);
align-items: flex-start;
}
.analysis-grid {
grid-template-columns: 1fr;
}
.recommendation-content {
grid-template-columns: 1fr;
}
}
</style>