5 Commits 27830bc4d7 ... 48339fd0c5

Author SHA1 Message Date
  jimmylee 48339fd0c5 feat(speaking): show real total duration in overall report 5 months ago
  jimmylee 900f1ae661 调整宽度, 删除统计数据 5 months ago
  jimmylee a63bc12d04 编辑区缩放 5 months ago
  jimmylee ce59d09987 chore(speaking): rename default AI from Tom to Amy 5 months ago
  jimmylee 34423451c1 feat(speaking): create new slide when adding tool from recommend card 5 months ago

+ 4 - 10
src/views/Editor/EnglishSpeaking/layers/Layer2Speaking.vue

@@ -75,12 +75,11 @@
 
 <script lang="ts" setup>
 import { ref, computed } from 'vue'
-import { storeToRefs } from 'pinia'
 import { lang } from '@/main'
 import type { CreationMode, TopicDiscussionTask } from '@/types/englishSpeaking'
 import { useSpeakingStore } from '@/store/speaking'
-import { useSlidesStore } from '@/store'
 import useCreateElement from '@/hooks/useCreateElement'
+import useSlideHandler from '@/hooks/useSlideHandler'
 import { createSpeakingConfig } from '@/services/speaking'
 import message from '@/utils/message'
 import tasksData from '../data/topicDiscussionTasks.json'
@@ -130,22 +129,17 @@ const filteredTasks = computed(() => {
   return unitTasks.value
 })
 
-// 新交互:点卡片即创建配置 + 插入画布元素;配置页改由"点击画布里的 77 型元素"唤起
+// 新交互:点卡片即新建一页 + 创建配置 + 插入画布元素;配置页改由"点击画布里的 77 型元素"唤起
 const { createFrameElement } = useCreateElement()
-const { currentSlide } = storeToRefs(useSlidesStore())
+const { createSlide } = useSlideHandler()
 const inserting = ref(false)
 
 async function insertSpeakingToolToCanvas(source: 'select' | 'manual') {
   if (inserting.value) return
-  // 防御:当前 slide 已有 frame(不管类型)则阻止(createFrameElement 内部也会挡)
-  const hasFrame = currentSlide.value?.elements?.some((el: any) => el.type === 'frame')
-  if (hasFrame) {
-    message.error(lang.ssSlideLearn as string)
-    return
-  }
   inserting.value = true
   try {
     const { id } = await createSpeakingConfig(speakingStore.config)
+    createSlide()
     createFrameElement(id, 77)
     // 创建后立即唤起左侧配置面板(与点击画布 77 型 frame 同款信号),使用户可直接编辑
     speakingStore.openConfigPanel()

+ 1 - 2
src/views/Editor/EnglishSpeaking/preview/DetailedReport.vue

@@ -170,8 +170,7 @@ function getScoreClass(score: number) {
 
 <style lang="scss" scoped>
 .detailed-report {
-  max-width: 448px;
-  margin: 0 auto;
+  width: 100%;
   display: flex;
   flex-direction: column;
   gap: 12px;

+ 1 - 1
src/views/Editor/EnglishSpeaking/preview/DialogueChatView.vue

@@ -564,7 +564,7 @@ interface Props {
 const props = withDefaults(defineProps<Props>(), {
   topic: '',
   keywords: () => ['animal', 'zoo', 'cute', 'favorite'],
-  aiName: 'Tom',
+  aiName: 'Amy',
   aiAvatar: '😊',
   totalRounds: 3,
   showEnglishText: true,

+ 3 - 4
src/views/Editor/EnglishSpeaking/preview/OverallReport.vue

@@ -83,7 +83,7 @@
 
     <!-- 统计数据 -->
     <div class="stats-section">
-      <div class="stats-grid" :class="{ 'stats-narrow': scoreDisplayMode === 'comment_only' }">
+      <!-- <div class="stats-grid" :class="{ 'stats-narrow': scoreDisplayMode === 'comment_only' }">
         <template v-if="scoreDisplayMode !== 'comment_only'">
           <div class="stat-cell">
             <div class="stat-value">
@@ -106,7 +106,7 @@
           <div class="stat-value stat-amber">{{ evaluation.statistics.grammarErrors }}</div>
           <div class="stat-label">语法错误</div>
         </div>
-      </div>
+      </div> -->
     </div>
 
     <!-- 操作按钮 -->
@@ -188,8 +188,7 @@ function getDimValue(key: string): number {
 
 <style lang="scss" scoped>
 .overall-report {
-  max-width: 448px;
-  margin: 0 auto;
+  width: 100%;
   display: flex;
   flex-direction: column;
   gap: 12px;

+ 54 - 2
src/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue

@@ -1,5 +1,6 @@
 <template>
-  <div class="topic-discussion-preview">
+  <div class="topic-discussion-fit-wrapper" ref="fitWrapperRef">
+  <div class="topic-discussion-preview" :style="fitStyle">
     <!-- Ready 阶段:极简首页(参照 enspeak 布局) -->
     <div v-if="dialogueState === 'checking-history'" class="ready-stage">
       <div class="ready-header">
@@ -78,6 +79,7 @@
     </div>
 
   </div>
+  </div>
 </template>
 
 <script lang="ts" setup>
@@ -108,6 +110,40 @@ const props = withDefaults(defineProps<Props>(), {
 
 const speakingStore = useSpeakingStore()
 
+// 让组件按 DESIGN_WIDTH 设计宽度等比放大到任意槽位尺寸:
+// 槽位(type 77 的 frame 默认为整张 slide ~1000×562)若直接装下按手机尺寸设计的 chat
+// 会显得字小、留白大、布局像桌面端。这里用 ResizeObserver + transform: scale 让内部
+// 始终按 DESIGN_WIDTH px 布局,再视觉缩放到槽位实际宽度。
+// 调小 → 内部元素更大;调大 → 内部元素更小(接近槽位实际像素)。
+const DESIGN_WIDTH = 1000
+const fitWrapperRef = ref<HTMLDivElement>()
+const fitScale = ref(1)
+const fitInnerHeight = ref(0)
+
+const fitStyle = computed(() => {
+  if (fitInnerHeight.value === 0) {
+    return { width: '100%', height: '100%' }
+  }
+  return {
+    width: DESIGN_WIDTH + 'px',
+    height: fitInnerHeight.value + 'px',
+    transform: `scale(${fitScale.value})`,
+    transformOrigin: 'top left',
+  }
+})
+
+let fitResizeObserver: ResizeObserver | null = null
+function updateFit() {
+  const el = fitWrapperRef.value
+  if (!el) return
+  const w = el.clientWidth
+  const h = el.clientHeight
+  if (w === 0 || h === 0) return
+  const scale = w / DESIGN_WIDTH
+  fitScale.value = scale
+  fitInnerHeight.value = h / scale
+}
+
 const dialogueState = ref<PreviewDialogueState>('ready')
 const sessionCreating = ref(false)
 const sessionError = ref<string | null>(null)
@@ -130,7 +166,7 @@ const runtimeUserId = computed(() => runtimeParams.value.userId || '')
 
 const mockRole: PreviewAIRole = {
   id: 'tom',
-  name: 'Tom',
+  name: 'Amy',
   avatar: '😊',
   identity: 'Friendly Teacher',
   personality: 'Patient and encouraging',
@@ -452,14 +488,30 @@ watch(() => props.configId, (id) => {
 onMounted(() => {
   speakingStore.setPreviewState(dialogueState.value)
   loadConfigFromBackend(props.configId)
+  if (fitWrapperRef.value) {
+    fitResizeObserver = new ResizeObserver(updateFit)
+    fitResizeObserver.observe(fitWrapperRef.value)
+    updateFit()
+  }
 })
 onUnmounted(() => {
   nextHistoryLoadToken()
   speakingStore.setPreviewState('ready')
+  fitResizeObserver?.disconnect()
+  fitResizeObserver = null
 })
 </script>
 
 <style lang="scss" scoped>
+// 外层 fit-wrapper 始终撑满槽位(type 77 的 frame 默认 ~1000×562 设计 CSS-px)。
+// ResizeObserver 观察它的尺寸,再用 transform: scale() 把内层按 DESIGN_WIDTH 设计尺寸放大填满。
+.topic-discussion-fit-wrapper {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  position: relative;
+}
+
 .topic-discussion-preview {
   width: 100%;
   height: 100%;

+ 2 - 1
src/views/Editor/EnglishSpeaking/services/llmService.ts

@@ -100,6 +100,7 @@ interface BackendReportResponse {
   rounds: BackendRound[]
   overall: BackendOverall | null
   summary: string | null
+  totalDurationSeconds: number | null
 }
 
 interface BackendOverall {
@@ -189,7 +190,7 @@ function adaptReport(raw: BackendReportResponse): DialogueReport {
         highestRound: highest?.round ?? 0,
         grammarErrors: 0,
         excellentExpressions: 0,
-        totalDuration: 0,
+        totalDuration: raw.totalDurationSeconds ?? 0,
       },
       sentenceEvaluations,
     },