TopicDiscussionPreview.vue 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. <template>
  2. <div class="topic-discussion-preview">
  3. <!-- Ready 阶段:极简首页(参照 enspeak 布局) -->
  4. <div v-if="dialogueState === 'ready'" class="ready-stage">
  5. <div class="ready-header">
  6. <h1 class="ready-title">
  7. <span class="ready-title-icon">💬</span>
  8. Topic Discussion
  9. </h1>
  10. <p class="ready-subtitle">话题讨论 · 与 AI 伙伴练习英语对话</p>
  11. </div>
  12. <div class="ready-body">
  13. <span class="topic-emoji">🐼</span>
  14. <h3 class="topic-name">{{ topic }}</h3>
  15. </div>
  16. <div class="ready-footer">
  17. <button class="start-btn" @click="startDialogue">
  18. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
  19. stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  20. <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
  21. <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
  22. <line x1="12" y1="19" x2="12" y2="23" />
  23. <line x1="8" y1="23" x2="16" y2="23" />
  24. </svg>
  25. 开始对话
  26. </button>
  27. </div>
  28. </div>
  29. <!-- Chatting 阶段:直接渲染 DialogueChatView -->
  30. <DialogueChatView
  31. v-else-if="dialogueState === 'chatting'"
  32. :topic="topic"
  33. :keywords="keywords"
  34. :ai-name="mockRole.name"
  35. :ai-avatar="mockRole.avatar"
  36. :total-rounds="totalRounds"
  37. :mode="mode"
  38. @complete="handleDialogueComplete"
  39. />
  40. <!-- Completed 阶段:直接渲染报告 -->
  41. <div v-else class="report-stage">
  42. <div class="report-scroll">
  43. <OverallReport
  44. :evaluation="mockEvaluation"
  45. :role="mockRole"
  46. :scoreDisplayMode="'numeric'"
  47. @restart="resetPreview"
  48. @complete="resetPreview"
  49. />
  50. <div class="report-divider" />
  51. <DetailedReport :sentenceEvaluations="mockEvaluation.sentenceEvaluations" />
  52. </div>
  53. </div>
  54. </div>
  55. </template>
  56. <script lang="ts" setup>
  57. import { ref, watch, onMounted, onUnmounted } from 'vue'
  58. import type { OverallEvaluation, PreviewAIRole, PreviewDialogueState } from '@/types/englishSpeaking'
  59. import { useSpeakingStore } from '@/store/speaking'
  60. import DialogueChatView from './DialogueChatView.vue'
  61. import OverallReport from './OverallReport.vue'
  62. import DetailedReport from './DetailedReport.vue'
  63. interface Props {
  64. mode?: 'preview' | 'real'
  65. topic?: string
  66. keywords?: string[]
  67. totalRounds?: number
  68. }
  69. const props = withDefaults(defineProps<Props>(), {
  70. mode: 'real',
  71. topic: '我最喜欢的动物',
  72. keywords: () => ['favorite', 'adorable', 'bamboo', 'habitat', 'wildlife', 'endangered'],
  73. totalRounds: 3,
  74. })
  75. const dialogueState = ref<PreviewDialogueState>('ready')
  76. const mockRole: PreviewAIRole = {
  77. id: 'tom',
  78. name: 'Tom',
  79. avatar: '😊',
  80. identity: 'Friendly Teacher',
  81. personality: 'Patient and encouraging',
  82. speakingStyle: 'casual',
  83. speed: 'normal',
  84. isCustom: false,
  85. isRecommended: true,
  86. }
  87. const mockEvaluation: OverallEvaluation = {
  88. overallScore: 85,
  89. scoreLevel: 'good',
  90. percentile: 78,
  91. dimensions: {
  92. fluency: 82,
  93. interaction: 88,
  94. vocabulary: 76,
  95. grammar: 90,
  96. },
  97. aiComment: 'Great job! You showed good understanding of the topic. Your pronunciation was clear and your responses were relevant. Keep practicing to improve your fluency and try using more advanced vocabulary!',
  98. highlights: [
  99. 'Clear pronunciation of key words',
  100. 'Good use of complete sentences',
  101. 'Natural conversation flow',
  102. ],
  103. improvements: [
  104. 'Try using more descriptive adjectives',
  105. 'Practice linking words for smoother speech',
  106. 'Expand vocabulary range for the topic',
  107. ],
  108. nextChallenge: {
  109. difficulty: 'Medium',
  110. unlockedTopic: 'My Dream Job',
  111. suggestedMode: 'Free Talk',
  112. },
  113. statistics: {
  114. totalRounds: 3,
  115. averageScore: 83,
  116. highestScore: 92,
  117. highestRound: 2,
  118. grammarErrors: 2,
  119. excellentExpressions: 4,
  120. totalDuration: 180,
  121. },
  122. sentenceEvaluations: [
  123. {
  124. id: 'se-1', round: 1, role: 'ai',
  125. content: "Hi! What's your favorite animal?",
  126. audioDuration: 3,
  127. },
  128. {
  129. id: 'se-2', round: 1, role: 'student',
  130. content: 'I like pandas. They are very cute!',
  131. audioDuration: 4, score: 88,
  132. pronunciation: { accuracy: 90, intonation: 85, stress: 82, fluency: 88 },
  133. feedback: {
  134. highlights: ['Good pronunciation of "pandas"', 'Natural intonation'],
  135. corrections: [],
  136. suggestions: ['Try: "I really love pandas because they\'re incredibly cute!"'],
  137. },
  138. },
  139. {
  140. id: 'se-3', round: 2, role: 'ai',
  141. content: 'Pandas are adorable! Have you seen them at the zoo?',
  142. audioDuration: 4,
  143. },
  144. {
  145. id: 'se-4', round: 2, role: 'student',
  146. content: 'Yes, I went to the zoo last month.',
  147. audioDuration: 3, score: 92,
  148. pronunciation: { accuracy: 95, intonation: 90, stress: 88, fluency: 92 },
  149. feedback: {
  150. highlights: ['Excellent fluency', 'Clear past tense usage'],
  151. corrections: [],
  152. suggestions: ['Add more details: "I visited the Beijing Zoo last month and saw giant pandas there."'],
  153. },
  154. },
  155. {
  156. id: 'se-5', round: 3, role: 'ai',
  157. content: "That's great! What do pandas like to eat?",
  158. audioDuration: 3,
  159. },
  160. {
  161. id: 'se-6', round: 3, role: 'student',
  162. content: 'They like to eat bamboo.',
  163. audioDuration: 3, score: 78,
  164. pronunciation: { accuracy: 80, intonation: 75, stress: 76, fluency: 82 },
  165. feedback: {
  166. highlights: ['Correct answer'],
  167. corrections: [{ original: 'like to eat', corrected: 'mainly feed on', explanation: '"feed on" is more natural for describing animal diets' }],
  168. suggestions: ['Expand: "Pandas mainly feed on bamboo, but they also eat fruits and vegetables."'],
  169. },
  170. },
  171. ],
  172. }
  173. function startDialogue() {
  174. dialogueState.value = 'chatting'
  175. }
  176. function handleDialogueComplete() {
  177. dialogueState.value = 'completed'
  178. }
  179. function resetPreview() {
  180. dialogueState.value = 'ready'
  181. }
  182. // ── Sync with speakingStore (让 CanvasTool 可以驱动"重置预览") ──
  183. const speakingStore = useSpeakingStore()
  184. watch(dialogueState, (s) => { speakingStore.setPreviewState(s) }, { immediate: true })
  185. watch(
  186. () => speakingStore.resetSignal,
  187. (val, old) => {
  188. if (val !== old) resetPreview()
  189. },
  190. )
  191. onMounted(() => { speakingStore.setPreviewState(dialogueState.value) })
  192. onUnmounted(() => { speakingStore.setPreviewState('ready') })
  193. </script>
  194. <style lang="scss" scoped>
  195. .topic-discussion-preview {
  196. width: 100%;
  197. height: 100%;
  198. display: flex;
  199. flex-direction: column;
  200. background: #fff;
  201. position: relative;
  202. overflow: hidden;
  203. }
  204. // ─── Ready 阶段 ───
  205. .ready-stage {
  206. flex: 1;
  207. display: flex;
  208. flex-direction: column;
  209. background: #fff;
  210. }
  211. .ready-header {
  212. padding: 20px 24px 12px;
  213. text-align: center;
  214. border-bottom: 1px solid #f9fafb;
  215. }
  216. .ready-title {
  217. font-size: 20px;
  218. font-weight: 600;
  219. color: #111827;
  220. margin: 0;
  221. display: inline-flex;
  222. align-items: center;
  223. justify-content: center;
  224. gap: 8px;
  225. }
  226. .ready-title-icon { font-size: 22px; }
  227. .ready-subtitle {
  228. font-size: 13px;
  229. color: #9ca3af;
  230. margin: 4px 0 0;
  231. }
  232. .ready-body {
  233. flex: 1;
  234. display: flex;
  235. flex-direction: column;
  236. align-items: center;
  237. justify-content: center;
  238. gap: 16px;
  239. padding: 24px;
  240. }
  241. .topic-emoji { font-size: 56px; line-height: 1; }
  242. .topic-name {
  243. font-size: 18px;
  244. font-weight: 600;
  245. color: #1f2937;
  246. margin: 0;
  247. }
  248. .ready-footer {
  249. padding: 16px 24px 20px;
  250. border-top: 1px solid #f9fafb;
  251. background: #fafbfc;
  252. display: flex;
  253. justify-content: center;
  254. }
  255. .start-btn {
  256. display: inline-flex;
  257. align-items: center;
  258. gap: 8px;
  259. padding: 9px 28px;
  260. border-radius: 999px;
  261. background: #f97316;
  262. color: #fff;
  263. border: none;
  264. font-size: 14px;
  265. font-weight: 500;
  266. cursor: pointer;
  267. box-shadow: 0 4px 12px rgba(249, 115, 22, 0.25);
  268. transition: background 0.2s, transform 0.15s;
  269. &:hover { background: #ea580c; }
  270. &:active { transform: scale(0.97); }
  271. }
  272. // ─── Completed 阶段 ───
  273. .report-stage {
  274. flex: 1;
  275. display: flex;
  276. flex-direction: column;
  277. overflow: hidden;
  278. background: #fff;
  279. }
  280. .report-scroll {
  281. flex: 1;
  282. overflow-y: auto;
  283. padding: 16px;
  284. display: flex;
  285. flex-direction: column;
  286. gap: 24px;
  287. }
  288. .report-divider {
  289. height: 1px;
  290. background: #e5e7eb;
  291. margin: 0;
  292. }
  293. </style>