| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336 |
- <template>
- <div class="overall-report">
- <template v-if="evaluation">
- <!-- 完成提示 -->
- <div class="completion-header">
- <div class="completion-emoji">🎉</div>
- <h2 class="completion-title">对话完成!</h2>
- <p class="completion-subtitle">
- 与 {{ role?.name || 'AI' }} 对话 {{ evaluation.statistics.totalRounds }} 轮 · {{ formatDuration(evaluation.statistics.totalDuration) }}
- </p>
- </div>
- <!-- 综合评分区域 -->
- <div v-if="scoreDisplayMode !== 'comment_only'" class="score-section">
- <div class="score-header">
- <div>
- <div class="score-label">综合评分</div>
- <!-- 数字模式 -->
- <div v-if="scoreDisplayMode === 'numeric'" class="score-display">
- <span class="score-number">{{ evaluation.overallScore }}</span>
- <span class="score-unit">分</span>
- </div>
- <!-- 字母模式 -->
- <div v-else class="score-display">
- <span class="score-letter" :style="{ color: letterColorMap[scoreToLetter(evaluation.overallScore)] }">
- {{ scoreToLetter(evaluation.overallScore) }}
- </span>
- <span class="score-unit">级</span>
- </div>
- </div>
- <div class="score-meta">
- <div class="level-badge" :style="{ backgroundColor: levelConfig.bgColor, color: levelConfig.color }">
- {{ levelConfig.label }}
- </div>
- <div class="percentile-text">
- 超过 <span class="percentile-value">{{ evaluation.percentile }}%</span> 同学
- </div>
- </div>
- </div>
- <!-- 四维能力分析 -->
- <div class="ability-section">
- <div class="ability-label">能力分析</div>
- <div class="ability-bars">
- <div v-for="dim in visibleDimensions" :key="dim.key" class="ability-row">
- <span class="ability-name">{{ dim.label }}</span>
- <div class="ability-track">
- <div class="ability-fill" :style="{ width: `${getDimValue(dim.key)}%` }" />
- </div>
- <span class="ability-value">
- {{ scoreDisplayMode === 'letter' ? scoreToLetter(getDimValue(dim.key)) : getDimValue(dim.key) }}
- </span>
- </div>
- </div>
- </div>
- </div>
- <!-- AI 点评 -->
- <div class="ai-comment-section">
- <div class="ai-comment-inner">
- <div class="ai-avatar-circle">{{ role?.avatar || '👦' }}</div>
- <div class="ai-comment-body">
- <div class="ai-comment-name">{{ role?.name || 'AI' }} 说</div>
- <p class="ai-comment-text">{{ evaluation.aiComment }}</p>
- </div>
- </div>
- </div>
- <!-- 亮点与建议 -->
- <div class="highlights-grid">
- <div class="highlight-card">
- <div class="highlight-header"><span class="highlight-icon-good">✓</span> 亮点</div>
- <div class="highlight-list">
- <div v-for="(h, i) in evaluation.highlights.slice(0, 3)" :key="i" class="highlight-item">• {{ h }}</div>
- </div>
- </div>
- <div class="highlight-card">
- <div class="highlight-header"><span class="highlight-icon-improve">→</span> 建议</div>
- <div class="highlight-list">
- <div v-for="(h, i) in evaluation.improvements.slice(0, 3)" :key="i" class="highlight-item">• {{ h }}</div>
- </div>
- </div>
- </div>
- </template>
- <!-- 无评分时的占位提示 -->
- <div v-else class="completion-header">
- <div class="completion-emoji">🎉</div>
- <h2 class="completion-title">对话完成!</h2>
- </div>
- <!-- 统计数据 -->
- <div class="stats-section">
- <!-- <div class="stats-grid" :class="{ 'stats-narrow': scoreDisplayMode === 'comment_only' }">
- <template v-if="scoreDisplayMode !== 'comment_only'">
- <div class="stat-cell">
- <div class="stat-value">
- {{ scoreDisplayMode === 'letter' ? scoreToLetter(evaluation.statistics.averageScore) : evaluation.statistics.averageScore }}
- </div>
- <div class="stat-label">{{ scoreDisplayMode === 'letter' ? '平均等级' : '平均分' }}</div>
- </div>
- <div class="stat-cell">
- <div class="stat-value stat-green">
- {{ scoreDisplayMode === 'letter' ? scoreToLetter(evaluation.statistics.highestScore) : evaluation.statistics.highestScore }}
- </div>
- <div class="stat-label">{{ scoreDisplayMode === 'letter' ? '最高等级' : '最高分' }}</div>
- </div>
- </template>
- <div class="stat-cell">
- <div class="stat-value stat-blue">{{ evaluation.statistics.excellentExpressions }}</div>
- <div class="stat-label">优秀表达</div>
- </div>
- <div class="stat-cell">
- <div class="stat-value stat-amber">{{ evaluation.statistics.grammarErrors }}</div>
- <div class="stat-label">语法错误</div>
- </div>
- </div> -->
- </div>
- <!-- 操作按钮 -->
- <div class="action-buttons">
- <button class="btn-secondary" @click="$emit('restart')">
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
- <polyline points="23 4 23 10 17 10" /><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
- </svg>
- 再来一次
- </button>
- <button class="btn-primary" @click="$emit('complete')">
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
- <polyline points="20 6 9 17 4 12" />
- </svg>
- 完成
- </button>
- </div>
- </div>
- </template>
- <script lang="ts" setup>
- import { computed } from 'vue'
- import type { OverallEvaluation, ScoreLevel, ScoreDisplayMode, PreviewAIRole } from '@/types/englishSpeaking'
- interface Props {
- evaluation: OverallEvaluation | null
- role: PreviewAIRole | null
- scoreDisplayMode?: ScoreDisplayMode
- }
- const props = withDefaults(defineProps<Props>(), {
- scoreDisplayMode: 'numeric',
- })
- defineEmits<{
- restart: []
- complete: []
- }>()
- const scoreLevelConfig: Record<ScoreLevel, { label: string; color: string; bgColor: string }> = {
- excellent: { label: '优秀', color: '#22c55e', bgColor: '#dcfce7' },
- good: { label: '良好', color: '#3b82f6', bgColor: '#dbeafe' },
- fair: { label: '不错', color: '#f59e0b', bgColor: '#fef3c7' },
- needsWork: { label: '继续加油', color: '#ef4444', bgColor: '#fee2e2' },
- }
- const letterColorMap: Record<string, string> = {
- A: '#22c55e', B: '#3b82f6', C: '#f59e0b', D: '#f97316', E: '#ef4444',
- }
- const dimensionConfig = [
- { label: '准确度', key: 'grammar' },
- { label: '语调', key: 'interaction' },
- { label: '重音', key: 'vocabulary' },
- { label: '流畅度', key: 'fluency' },
- ]
- const levelConfig = computed(() => scoreLevelConfig[props.evaluation?.scoreLevel ?? 'good'])
- const visibleDimensions = computed(() => dimensionConfig)
- function scoreToLetter(score: number): string {
- if (score >= 90) return 'A'
- if (score >= 80) return 'B'
- if (score >= 70) return 'C'
- if (score >= 60) return 'D'
- return 'E'
- }
- function formatDuration(seconds: number) {
- const mins = Math.floor(seconds / 60)
- const secs = seconds % 60
- return `${mins}分${secs}秒`
- }
- function getDimValue(key: string): number {
- const dims = props.evaluation?.dimensions as Record<string, number> | undefined
- return dims?.[key] || 0
- }
- </script>
- <style lang="scss" scoped>
- .overall-report {
- width: 100%;
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
- .completion-header { text-align: center; }
- .completion-emoji { font-size: 30px; margin-bottom: 4px; }
- .completion-title { font-size: 16px; font-weight: 600; color: #111827; margin: 0; }
- .completion-subtitle { font-size: 12px; color: #6b7280; margin: 2px 0 0; }
- // 评分区域
- .score-section {
- background: #fff;
- border-radius: 12px;
- border: 1px solid #f3f4f6;
- overflow: hidden;
- }
- .score-header {
- padding: 12px 16px;
- background: linear-gradient(to right, #fff7ed, #f6f8ff);
- border-bottom: 1px solid #f3f4f6;
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .score-label { font-size: 12px; color: #6b7280; margin-bottom: 4px; }
- .score-display { display: flex; align-items: baseline; gap: 4px; }
- .score-number { font-size: 32px; font-weight: 700; color: #f97316; }
- .score-letter { font-size: 32px; font-weight: 700; }
- .score-unit { font-size: 14px; color: #9ca3af; }
- .score-meta { text-align: right; }
- .level-badge {
- display: inline-block;
- padding: 4px 10px;
- border-radius: 999px;
- font-size: 12px;
- font-weight: 500;
- }
- .percentile-text { font-size: 10px; color: #9ca3af; margin-top: 4px; }
- .percentile-value { color: #f97316; font-weight: 500; }
- .ability-section { padding: 12px 16px; }
- .ability-label { font-size: 12px; color: #6b7280; margin-bottom: 8px; }
- .ability-bars { display: flex; flex-direction: column; gap: 10px; }
- .ability-row { display: flex; align-items: center; gap: 12px; }
- .ability-name { font-size: 12px; color: #4b5563; width: 40px; flex-shrink: 0; }
- .ability-track { flex: 1; height: 8px; background: #f3f4f6; border-radius: 999px; overflow: hidden; }
- .ability-fill { height: 100%; background: #f97316; border-radius: 999px; transition: width 0.5s; }
- .ability-value { font-size: 12px; font-weight: 500; color: #f97316; width: 32px; text-align: right; }
- // AI 点评
- .ai-comment-section {
- background: #fff;
- border-radius: 12px;
- border: 1px solid #f3f4f6;
- padding: 12px;
- }
- .ai-comment-inner { display: flex; gap: 10px; }
- .ai-avatar-circle {
- width: 32px; height: 32px;
- border-radius: 50%;
- background: #fff7ed;
- display: flex; align-items: center; justify-content: center;
- font-size: 16px;
- flex-shrink: 0;
- }
- .ai-comment-body { flex: 1; min-width: 0; }
- .ai-comment-name { font-size: 12px; color: #9ca3af; margin-bottom: 4px; }
- .ai-comment-text { font-size: 12px; color: #374151; line-height: 1.6; margin: 0; }
- // 亮点与建议
- .highlights-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
- .highlight-card {
- background: #fff;
- border-radius: 12px;
- border: 1px solid #f3f4f6;
- padding: 10px;
- }
- .highlight-header { font-size: 12px; color: #6b7280; margin-bottom: 6px; display: flex; align-items: center; gap: 4px; }
- .highlight-icon-good { color: #22c55e; }
- .highlight-icon-improve { color: #f59e0b; }
- .highlight-list { display: flex; flex-direction: column; gap: 4px; }
- .highlight-item { font-size: 10px; color: #4b5563; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
- // 统计数据
- .stats-section {
- background: #fff;
- border-radius: 12px;
- border: 1px solid #f3f4f6;
- padding: 10px;
- }
- .stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; text-align: center; }
- .stats-narrow { grid-template-columns: repeat(2, 1fr); }
- .stat-value { font-size: 16px; font-weight: 600; color: #1f2937; }
- .stat-green { color: #22c55e; }
- .stat-blue { color: #3b82f6; }
- .stat-amber { color: #f59e0b; }
- .stat-label { font-size: 9px; color: #9ca3af; }
- // 按钮
- .action-buttons {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 12px;
- padding-top: 4px;
- }
- .btn-secondary {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 8px 16px;
- border-radius: 8px;
- font-size: 12px;
- font-weight: 500;
- color: #4b5563;
- background: #f3f4f6;
- border: none;
- cursor: pointer;
- &:hover { background: #e5e7eb; }
- }
- .btn-primary {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 8px 16px;
- border-radius: 8px;
- font-size: 12px;
- font-weight: 500;
- color: #fff;
- background: #f97316;
- border: none;
- cursor: pointer;
- &:hover { background: #ea580c; }
- }
- </style>
|