SpeakingPanel.vue 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. <!--
  2. SpeakingPanel — 英语口语练习的主面板容器组件
  3. 职责:
  4. 1. 管理三层页面导航状态(layer1 → layer2 → config)
  5. 2. 渲染面包屑导航,支持任意层级回退
  6. 3. 根据 pageMode 切换展示不同的子页面组件
  7. 页面流转:
  8. layer1 (首页:选课本/年级/单元) → layer2 (口语练习列表) → config (话题讨论配置)
  9. -->
  10. <template>
  11. <div class="speaking-panel">
  12. <!-- 面包屑导航:根据当前 pageMode 展示不同层级的路径 -->
  13. <div class="panel-header">
  14. <div class="breadcrumb">
  15. <!-- layer1: 仅显示标题,无回退链接 -->
  16. <template v-if="pageMode === 'layer1'">
  17. <span class="breadcrumb-title">{{ lang.ssEnglishSubject }}</span>
  18. </template>
  19. <template v-else>
  20. <button class="breadcrumb-link" @click="goHome">{{ lang.ssEnglishSubject }}</button>
  21. <svg class="breadcrumb-arrow" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  22. <path d="M9 18l6-6-6-6" />
  23. </svg>
  24. <!-- config 层:显示完整三级面包屑 英语 > 口语 > 话题讨论 -->
  25. <template v-if="pageMode === 'config'">
  26. <button class="breadcrumb-link" @click="goLayer2">{{ lang.ssSpeaking }}</button>
  27. <svg class="breadcrumb-arrow" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  28. <path d="M9 18l6-6-6-6" />
  29. </svg>
  30. <span class="breadcrumb-current">{{ lang.ssTopicDiscussion }}</span>
  31. </template>
  32. <!-- layer2:显示两级面包屑 英语 > 口语 -->
  33. <template v-else>
  34. <span class="breadcrumb-current">{{ lang.ssSpeaking }}</span>
  35. </template>
  36. </template>
  37. </div>
  38. </div>
  39. <!-- 内容区域:根据 pageMode 切换子页面 -->
  40. <div class="panel-body">
  41. <Layer1Home
  42. v-if="pageMode === 'layer1'"
  43. :selectedTextbook="selectedTextbook"
  44. :selectedGrade="selectedGrade"
  45. :selectedUnit="selectedUnit"
  46. @update:selectedTextbook="selectedTextbook = $event"
  47. @update:selectedGrade="selectedGrade = $event"
  48. @update:selectedUnit="selectedUnit = $event"
  49. @selectExerciseType="handleSelectExerciseType"
  50. />
  51. <Layer2Speaking
  52. v-else-if="pageMode === 'layer2'"
  53. :unitId="selectedUnit"
  54. @openConfig="handleOpenConfig"
  55. />
  56. <TopicDiscussionConfig
  57. v-else-if="pageMode === 'config'"
  58. @back="goLayer2"
  59. />
  60. </div>
  61. </div>
  62. </template>
  63. <script lang="ts" setup>
  64. import { ref, computed, watch } from 'vue'
  65. import { lang } from '@/main'
  66. import type { PageMode, ExerciseType } from '@/types/englishSpeaking'
  67. import { useSpeakingStore } from '@/store/speaking'
  68. import Layer1Home from './layers/Layer1Home.vue'
  69. import Layer2Speaking from './layers/Layer2Speaking.vue'
  70. import TopicDiscussionConfig from './configs/TopicDiscussionConfig.vue'
  71. /** 当前页面层级:layer1(首页) → layer2(口语列表) → config(配置页) */
  72. const pageMode = ref<PageMode>('layer1')
  73. const speakingStore = useSpeakingStore()
  74. // 课本筛选条件的默认值(上海英语·五年级上·Unit2),供开发/演示用
  75. const selectedTextbook = ref('shep')
  76. const selectedUnit = ref('SHEP-5-上-Unit2')
  77. // selectedGrade 持久化到 store(用于发送到后端 createSession 渲染 system_prompt)
  78. const selectedGrade = computed({
  79. get: () => speakingStore.config.grade,
  80. set: (val: string) => speakingStore.updateGrade(val),
  81. })
  82. /** Layer1 选择练习类型后的回调,目前仅 speaking 类型会触发页面跳转 */
  83. const handleSelectExerciseType = (type: ExerciseType) => {
  84. if (type === 'speaking') {
  85. pageMode.value = 'layer2'
  86. }
  87. }
  88. /** Layer2 点击某个口语话题后,进入配置页 */
  89. const handleOpenConfig = () => {
  90. pageMode.value = 'config'
  91. }
  92. // ---- 面包屑导航回退 ----
  93. const goHome = () => {
  94. pageMode.value = 'layer1'
  95. }
  96. const goLayer2 = () => {
  97. pageMode.value = 'layer2'
  98. }
  99. // 点击画布上的 77 型 frame 会递增 openConfigSignal → 强制切到配置页
  100. watch(() => speakingStore.openConfigSignal, (val, old) => {
  101. if (val !== old) pageMode.value = 'config'
  102. })
  103. </script>
  104. <style lang="scss" scoped>
  105. .speaking-panel {
  106. display: flex;
  107. flex-direction: column;
  108. height: 100%;
  109. background: #fff;
  110. }
  111. .panel-header {
  112. padding: 12px 16px;
  113. border-bottom: 1px solid rgba(0, 0, 0, 0.06);
  114. flex-shrink: 0;
  115. }
  116. .breadcrumb {
  117. display: flex;
  118. align-items: center;
  119. gap: 4px;
  120. font-size: 13px;
  121. }
  122. .breadcrumb-title {
  123. font-size: 15px;
  124. font-weight: 600;
  125. color: #111;
  126. }
  127. .breadcrumb-link {
  128. background: none;
  129. border: none;
  130. padding: 0;
  131. cursor: pointer;
  132. color: #9ca3af;
  133. font-size: 13px;
  134. transition: color 0.2s;
  135. &:hover {
  136. color: #6b7280;
  137. }
  138. }
  139. .breadcrumb-arrow {
  140. color: #d1d5db;
  141. flex-shrink: 0;
  142. }
  143. .breadcrumb-current {
  144. font-weight: 600;
  145. color: #111;
  146. }
  147. .panel-body {
  148. flex: 1;
  149. overflow-y: auto;
  150. }
  151. </style>