| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172 |
- <!--
- SpeakingPanel — 英语口语练习的主面板容器组件
- 职责:
- 1. 管理三层页面导航状态(layer1 → layer2 → config)
- 2. 渲染面包屑导航,支持任意层级回退
- 3. 根据 pageMode 切换展示不同的子页面组件
- 页面流转:
- layer1 (首页:选课本/年级/单元) → layer2 (口语练习列表) → config (话题讨论配置)
- -->
- <template>
- <div class="speaking-panel">
- <!-- 面包屑导航:根据当前 pageMode 展示不同层级的路径 -->
- <div class="panel-header">
- <div class="breadcrumb">
- <!-- layer1: 仅显示标题,无回退链接 -->
- <template v-if="pageMode === 'layer1'">
- <span class="breadcrumb-title">{{ lang.ssEnglishSubject }}</span>
- </template>
- <template v-else>
- <button class="breadcrumb-link" @click="goHome">{{ lang.ssEnglishSubject }}</button>
- <svg class="breadcrumb-arrow" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <path d="M9 18l6-6-6-6" />
- </svg>
- <!-- config 层:显示完整三级面包屑 英语 > 口语 > 话题讨论 -->
- <template v-if="pageMode === 'config'">
- <button class="breadcrumb-link" @click="goLayer2">{{ lang.ssSpeaking }}</button>
- <svg class="breadcrumb-arrow" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <path d="M9 18l6-6-6-6" />
- </svg>
- <span class="breadcrumb-current">{{ lang.ssTopicDiscussion }}</span>
- </template>
- <!-- layer2:显示两级面包屑 英语 > 口语 -->
- <template v-else>
- <span class="breadcrumb-current">{{ lang.ssSpeaking }}</span>
- </template>
- </template>
- </div>
- </div>
- <!-- 内容区域:根据 pageMode 切换子页面 -->
- <div class="panel-body">
- <Layer1Home
- v-if="pageMode === 'layer1'"
- :selectedTextbook="selectedTextbook"
- :selectedGrade="selectedGrade"
- :selectedUnit="selectedUnit"
- @update:selectedTextbook="selectedTextbook = $event"
- @update:selectedGrade="selectedGrade = $event"
- @update:selectedUnit="selectedUnit = $event"
- @selectExerciseType="handleSelectExerciseType"
- />
- <Layer2Speaking
- v-else-if="pageMode === 'layer2'"
- :unitId="selectedUnit"
- @openConfig="handleOpenConfig"
- />
- <TopicDiscussionConfig
- v-else-if="pageMode === 'config'"
- @back="goLayer2"
- />
- </div>
- </div>
- </template>
- <script lang="ts" setup>
- import { ref, computed, watch } from 'vue'
- import { lang } from '@/main'
- import type { PageMode, ExerciseType } from '@/types/englishSpeaking'
- import { useSpeakingStore } from '@/store/speaking'
- import Layer1Home from './layers/Layer1Home.vue'
- import Layer2Speaking from './layers/Layer2Speaking.vue'
- import TopicDiscussionConfig from './configs/TopicDiscussionConfig.vue'
- /** 当前页面层级:layer1(首页) → layer2(口语列表) → config(配置页) */
- const pageMode = ref<PageMode>('layer1')
- const speakingStore = useSpeakingStore()
- // 课本筛选条件的默认值(上海英语·五年级上·Unit2),供开发/演示用
- const selectedTextbook = ref('shep')
- const selectedUnit = ref('SHEP-5-上-Unit2')
- // selectedGrade 持久化到 store(用于发送到后端 createSession 渲染 system_prompt)
- const selectedGrade = computed({
- get: () => speakingStore.config.grade,
- set: (val: string) => speakingStore.updateGrade(val),
- })
- /** Layer1 选择练习类型后的回调,目前仅 speaking 类型会触发页面跳转 */
- const handleSelectExerciseType = (type: ExerciseType) => {
- if (type === 'speaking') {
- pageMode.value = 'layer2'
- }
- }
- /** Layer2 点击某个口语话题后,进入配置页 */
- const handleOpenConfig = () => {
- pageMode.value = 'config'
- }
- // ---- 面包屑导航回退 ----
- const goHome = () => {
- pageMode.value = 'layer1'
- }
- const goLayer2 = () => {
- pageMode.value = 'layer2'
- }
- // 点击画布上的 77 型 frame 会递增 openConfigSignal → 强制切到配置页
- watch(() => speakingStore.openConfigSignal, (val, old) => {
- if (val !== old) pageMode.value = 'config'
- })
- </script>
- <style lang="scss" scoped>
- .speaking-panel {
- display: flex;
- flex-direction: column;
- height: 100%;
- background: #fff;
- }
- .panel-header {
- padding: 12px 16px;
- border-bottom: 1px solid rgba(0, 0, 0, 0.06);
- flex-shrink: 0;
- }
- .breadcrumb {
- display: flex;
- align-items: center;
- gap: 4px;
- font-size: 13px;
- }
- .breadcrumb-title {
- font-size: 15px;
- font-weight: 600;
- color: #111;
- }
- .breadcrumb-link {
- background: none;
- border: none;
- padding: 0;
- cursor: pointer;
- color: #9ca3af;
- font-size: 13px;
- transition: color 0.2s;
- &:hover {
- color: #6b7280;
- }
- }
- .breadcrumb-arrow {
- color: #d1d5db;
- flex-shrink: 0;
- }
- .breadcrumb-current {
- font-weight: 600;
- color: #111;
- }
- .panel-body {
- flex: 1;
- overflow-y: auto;
- }
- </style>
|