OverallReport.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. <template>
  2. <div class="overall-report">
  3. <template v-if="evaluation">
  4. <!-- 完成提示 -->
  5. <div class="completion-header">
  6. <div class="completion-emoji">🎉</div>
  7. <h2 class="completion-title">对话完成!</h2>
  8. <p class="completion-subtitle">
  9. 与 {{ role?.name || 'AI' }} 对话 {{ evaluation.statistics.totalRounds }} 轮 · {{ formatDuration(evaluation.statistics.totalDuration) }}
  10. </p>
  11. </div>
  12. <!-- 综合评分区域 -->
  13. <div v-if="scoreDisplayMode !== 'comment_only'" class="score-section">
  14. <div class="score-header">
  15. <div>
  16. <div class="score-label">综合评分</div>
  17. <!-- 数字模式 -->
  18. <div v-if="scoreDisplayMode === 'numeric'" class="score-display">
  19. <span class="score-number">{{ evaluation.overallScore }}</span>
  20. <span class="score-unit">分</span>
  21. </div>
  22. <!-- 字母模式 -->
  23. <div v-else class="score-display">
  24. <span class="score-letter" :style="{ color: letterColorMap[scoreToLetter(evaluation.overallScore)] }">
  25. {{ scoreToLetter(evaluation.overallScore) }}
  26. </span>
  27. <span class="score-unit">级</span>
  28. </div>
  29. </div>
  30. <div class="score-meta">
  31. <div class="level-badge" :style="{ backgroundColor: levelConfig.bgColor, color: levelConfig.color }">
  32. {{ levelConfig.label }}
  33. </div>
  34. <div class="percentile-text">
  35. 超过 <span class="percentile-value">{{ evaluation.percentile }}%</span> 同学
  36. </div>
  37. </div>
  38. </div>
  39. <!-- 四维能力分析 -->
  40. <div class="ability-section">
  41. <div class="ability-label">能力分析</div>
  42. <div class="ability-bars">
  43. <div v-for="dim in visibleDimensions" :key="dim.key" class="ability-row">
  44. <span class="ability-name">{{ dim.label }}</span>
  45. <div class="ability-track">
  46. <div class="ability-fill" :style="{ width: `${getDimValue(dim.key)}%` }" />
  47. </div>
  48. <span class="ability-value">
  49. {{ scoreDisplayMode === 'letter' ? scoreToLetter(getDimValue(dim.key)) : getDimValue(dim.key) }}
  50. </span>
  51. </div>
  52. </div>
  53. </div>
  54. </div>
  55. <!-- AI 点评 -->
  56. <div class="ai-comment-section">
  57. <div class="ai-comment-inner">
  58. <div class="ai-avatar-circle">{{ role?.avatar || '👦' }}</div>
  59. <div class="ai-comment-body">
  60. <div class="ai-comment-name">{{ role?.name || 'AI' }} 说</div>
  61. <p class="ai-comment-text">{{ evaluation.aiComment }}</p>
  62. </div>
  63. </div>
  64. </div>
  65. <!-- 亮点与建议 -->
  66. <div class="highlights-grid">
  67. <div class="highlight-card">
  68. <div class="highlight-header"><span class="highlight-icon-good">✓</span> 亮点</div>
  69. <div class="highlight-list">
  70. <div v-for="(h, i) in evaluation.highlights.slice(0, 3)" :key="i" class="highlight-item">• {{ h }}</div>
  71. </div>
  72. </div>
  73. <div class="highlight-card">
  74. <div class="highlight-header"><span class="highlight-icon-improve">→</span> 建议</div>
  75. <div class="highlight-list">
  76. <div v-for="(h, i) in evaluation.improvements.slice(0, 3)" :key="i" class="highlight-item">• {{ h }}</div>
  77. </div>
  78. </div>
  79. </div>
  80. </template>
  81. <!-- 无评分时的占位提示 -->
  82. <div v-else class="completion-header">
  83. <div class="completion-emoji">🎉</div>
  84. <h2 class="completion-title">对话完成!</h2>
  85. </div>
  86. <!-- 统计数据 -->
  87. <div class="stats-section">
  88. <!-- <div class="stats-grid" :class="{ 'stats-narrow': scoreDisplayMode === 'comment_only' }">
  89. <template v-if="scoreDisplayMode !== 'comment_only'">
  90. <div class="stat-cell">
  91. <div class="stat-value">
  92. {{ scoreDisplayMode === 'letter' ? scoreToLetter(evaluation.statistics.averageScore) : evaluation.statistics.averageScore }}
  93. </div>
  94. <div class="stat-label">{{ scoreDisplayMode === 'letter' ? '平均等级' : '平均分' }}</div>
  95. </div>
  96. <div class="stat-cell">
  97. <div class="stat-value stat-green">
  98. {{ scoreDisplayMode === 'letter' ? scoreToLetter(evaluation.statistics.highestScore) : evaluation.statistics.highestScore }}
  99. </div>
  100. <div class="stat-label">{{ scoreDisplayMode === 'letter' ? '最高等级' : '最高分' }}</div>
  101. </div>
  102. </template>
  103. <div class="stat-cell">
  104. <div class="stat-value stat-blue">{{ evaluation.statistics.excellentExpressions }}</div>
  105. <div class="stat-label">优秀表达</div>
  106. </div>
  107. <div class="stat-cell">
  108. <div class="stat-value stat-amber">{{ evaluation.statistics.grammarErrors }}</div>
  109. <div class="stat-label">语法错误</div>
  110. </div>
  111. </div> -->
  112. </div>
  113. <!-- 操作按钮 -->
  114. <div class="action-buttons">
  115. <button class="btn-secondary" @click="$emit('restart')">
  116. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  117. <polyline points="23 4 23 10 17 10" /><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
  118. </svg>
  119. 再来一次
  120. </button>
  121. <button class="btn-primary" @click="$emit('complete')">
  122. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  123. <polyline points="20 6 9 17 4 12" />
  124. </svg>
  125. 完成
  126. </button>
  127. </div>
  128. </div>
  129. </template>
  130. <script lang="ts" setup>
  131. import { computed } from 'vue'
  132. import type { OverallEvaluation, ScoreLevel, ScoreDisplayMode, PreviewAIRole } from '@/types/englishSpeaking'
  133. interface Props {
  134. evaluation: OverallEvaluation | null
  135. role: PreviewAIRole | null
  136. scoreDisplayMode?: ScoreDisplayMode
  137. }
  138. const props = withDefaults(defineProps<Props>(), {
  139. scoreDisplayMode: 'numeric',
  140. })
  141. defineEmits<{
  142. restart: []
  143. complete: []
  144. }>()
  145. const scoreLevelConfig: Record<ScoreLevel, { label: string; color: string; bgColor: string }> = {
  146. excellent: { label: '优秀', color: '#22c55e', bgColor: '#dcfce7' },
  147. good: { label: '良好', color: '#3b82f6', bgColor: '#dbeafe' },
  148. fair: { label: '不错', color: '#f59e0b', bgColor: '#fef3c7' },
  149. needsWork: { label: '继续加油', color: '#ef4444', bgColor: '#fee2e2' },
  150. }
  151. const letterColorMap: Record<string, string> = {
  152. A: '#22c55e', B: '#3b82f6', C: '#f59e0b', D: '#f97316', E: '#ef4444',
  153. }
  154. const dimensionConfig = [
  155. { label: '准确度', key: 'grammar' },
  156. { label: '语调', key: 'interaction' },
  157. { label: '重音', key: 'vocabulary' },
  158. { label: '流畅度', key: 'fluency' },
  159. ]
  160. const levelConfig = computed(() => scoreLevelConfig[props.evaluation?.scoreLevel ?? 'good'])
  161. const visibleDimensions = computed(() => dimensionConfig)
  162. function scoreToLetter(score: number): string {
  163. if (score >= 90) return 'A'
  164. if (score >= 80) return 'B'
  165. if (score >= 70) return 'C'
  166. if (score >= 60) return 'D'
  167. return 'E'
  168. }
  169. function formatDuration(seconds: number) {
  170. const mins = Math.floor(seconds / 60)
  171. const secs = seconds % 60
  172. return `${mins}分${secs}秒`
  173. }
  174. function getDimValue(key: string): number {
  175. const dims = props.evaluation?.dimensions as Record<string, number> | undefined
  176. return dims?.[key] || 0
  177. }
  178. </script>
  179. <style lang="scss" scoped>
  180. .overall-report {
  181. width: 100%;
  182. display: flex;
  183. flex-direction: column;
  184. gap: 12px;
  185. }
  186. .completion-header { text-align: center; }
  187. .completion-emoji { font-size: 30px; margin-bottom: 4px; }
  188. .completion-title { font-size: 16px; font-weight: 600; color: #111827; margin: 0; }
  189. .completion-subtitle { font-size: 12px; color: #6b7280; margin: 2px 0 0; }
  190. // 评分区域
  191. .score-section {
  192. background: #fff;
  193. border-radius: 12px;
  194. border: 1px solid #f3f4f6;
  195. overflow: hidden;
  196. }
  197. .score-header {
  198. padding: 12px 16px;
  199. background: linear-gradient(to right, #fff7ed, #f6f8ff);
  200. border-bottom: 1px solid #f3f4f6;
  201. display: flex;
  202. align-items: center;
  203. justify-content: space-between;
  204. }
  205. .score-label { font-size: 12px; color: #6b7280; margin-bottom: 4px; }
  206. .score-display { display: flex; align-items: baseline; gap: 4px; }
  207. .score-number { font-size: 32px; font-weight: 700; color: #f97316; }
  208. .score-letter { font-size: 32px; font-weight: 700; }
  209. .score-unit { font-size: 14px; color: #9ca3af; }
  210. .score-meta { text-align: right; }
  211. .level-badge {
  212. display: inline-block;
  213. padding: 4px 10px;
  214. border-radius: 999px;
  215. font-size: 12px;
  216. font-weight: 500;
  217. }
  218. .percentile-text { font-size: 10px; color: #9ca3af; margin-top: 4px; }
  219. .percentile-value { color: #f97316; font-weight: 500; }
  220. .ability-section { padding: 12px 16px; }
  221. .ability-label { font-size: 12px; color: #6b7280; margin-bottom: 8px; }
  222. .ability-bars { display: flex; flex-direction: column; gap: 10px; }
  223. .ability-row { display: flex; align-items: center; gap: 12px; }
  224. .ability-name { font-size: 12px; color: #4b5563; width: 40px; flex-shrink: 0; }
  225. .ability-track { flex: 1; height: 8px; background: #f3f4f6; border-radius: 999px; overflow: hidden; }
  226. .ability-fill { height: 100%; background: #f97316; border-radius: 999px; transition: width 0.5s; }
  227. .ability-value { font-size: 12px; font-weight: 500; color: #f97316; width: 32px; text-align: right; }
  228. // AI 点评
  229. .ai-comment-section {
  230. background: #fff;
  231. border-radius: 12px;
  232. border: 1px solid #f3f4f6;
  233. padding: 12px;
  234. }
  235. .ai-comment-inner { display: flex; gap: 10px; }
  236. .ai-avatar-circle {
  237. width: 32px; height: 32px;
  238. border-radius: 50%;
  239. background: #fff7ed;
  240. display: flex; align-items: center; justify-content: center;
  241. font-size: 16px;
  242. flex-shrink: 0;
  243. }
  244. .ai-comment-body { flex: 1; min-width: 0; }
  245. .ai-comment-name { font-size: 12px; color: #9ca3af; margin-bottom: 4px; }
  246. .ai-comment-text { font-size: 12px; color: #374151; line-height: 1.6; margin: 0; }
  247. // 亮点与建议
  248. .highlights-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
  249. .highlight-card {
  250. background: #fff;
  251. border-radius: 12px;
  252. border: 1px solid #f3f4f6;
  253. padding: 10px;
  254. }
  255. .highlight-header { font-size: 12px; color: #6b7280; margin-bottom: 6px; display: flex; align-items: center; gap: 4px; }
  256. .highlight-icon-good { color: #22c55e; }
  257. .highlight-icon-improve { color: #f59e0b; }
  258. .highlight-list { display: flex; flex-direction: column; gap: 4px; }
  259. .highlight-item { font-size: 10px; color: #4b5563; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  260. // 统计数据
  261. .stats-section {
  262. background: #fff;
  263. border-radius: 12px;
  264. border: 1px solid #f3f4f6;
  265. padding: 10px;
  266. }
  267. .stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; text-align: center; }
  268. .stats-narrow { grid-template-columns: repeat(2, 1fr); }
  269. .stat-value { font-size: 16px; font-weight: 600; color: #1f2937; }
  270. .stat-green { color: #22c55e; }
  271. .stat-blue { color: #3b82f6; }
  272. .stat-amber { color: #f59e0b; }
  273. .stat-label { font-size: 9px; color: #9ca3af; }
  274. // 按钮
  275. .action-buttons {
  276. display: flex;
  277. align-items: center;
  278. justify-content: center;
  279. gap: 12px;
  280. padding-top: 4px;
  281. }
  282. .btn-secondary {
  283. display: flex;
  284. align-items: center;
  285. gap: 6px;
  286. padding: 8px 16px;
  287. border-radius: 8px;
  288. font-size: 12px;
  289. font-weight: 500;
  290. color: #4b5563;
  291. background: #f3f4f6;
  292. border: none;
  293. cursor: pointer;
  294. &:hover { background: #e5e7eb; }
  295. }
  296. .btn-primary {
  297. display: flex;
  298. align-items: center;
  299. gap: 6px;
  300. padding: 8px 16px;
  301. border-radius: 8px;
  302. font-size: 12px;
  303. font-weight: 500;
  304. color: #fff;
  305. background: #f97316;
  306. border: none;
  307. cursor: pointer;
  308. &:hover { background: #ea580c; }
  309. }
  310. </style>