lsc преди 1 седмица
родител
ревизия
08ba4f2ea7
променени са 3 файла, в които са добавени 389 реда и са изтрити 23 реда
  1. 40 11
      src/components/WritingBoard.vue
  2. 46 3
      src/views/Screen/WritingBoardTool.vue
  3. 303 9
      src/views/Student/index.vue

+ 40 - 11
src/components/WritingBoard.vue

@@ -6,18 +6,19 @@
       :style="{
         width: canvasWidth + 'px',
         height: canvasHeight + 'px',
+        pointerEvents: readonly ? 'none' : 'auto',
       }"
-      @mousedown="$event => handleMousedown($event)"
-      @mousemove="$event => handleMousemove($event)"
-      @mouseup="handleMouseup()"
-      @touchstart="$event => handleMousedown($event)"
-      @touchmove="$event => handleMousemove($event)"
-      @touchend="handleMouseup(); mouseInCanvas = false"
-      @mouseleave="handleMouseup(); mouseInCanvas = false"
-      @mouseenter="mouseInCanvas = true"
+      @mousedown="$event => !readonly && handleMousedown($event)"
+      @mousemove="$event => !readonly && handleMousemove($event)"
+      @mouseup="!readonly && handleMouseup()"
+      @touchstart="$event => !readonly && handleMousedown($event)"
+      @touchmove="$event => !readonly && handleMousemove($event)"
+      @touchend="!readonly && (handleMouseup(), mouseInCanvas = false)"
+      @mouseleave="!readonly && (handleMouseup(), mouseInCanvas = false)"
+      @mouseenter="!readonly && (mouseInCanvas = true)"
     ></canvas>
 
-    <template v-if="mouseInCanvas">
+    <template v-if="mouseInCanvas && !readonly">
       <div 
         class="eraser"
         :style="{
@@ -77,6 +78,7 @@ const props = withDefaults(defineProps<{
   markSize?: number
   rubberSize?: number
   shapeSize?: number
+  readonly?: boolean
 }>(), {
   color: '#ffcc00',
   model: 'pen',
@@ -86,6 +88,7 @@ const props = withDefaults(defineProps<{
   markSize: 24,
   rubberSize: 80,
   shapeSize: 4,
+  readonly: false,
 })
 
 const emit = defineEmits<{
@@ -343,7 +346,7 @@ const handleMove = (x: number, y: number) => {
     draw(x, y, props.markSize)
     lastPos = { x, y }
   }
-  else if (props.model ==='eraser') {
+  else if (props.model === 'eraser') {
     erase(x, y)
     lastPos = { x, y }
   }
@@ -425,9 +428,35 @@ const setImageDataURL = (imageDataURL: string) => {
     const img = new Image()
     img.src = imageDataURL
     img.onload = () => {
-      ctx!.drawImage(img, 0, 0)
+      // 直接按canvas尺寸绘制,不缩放(因为画图数据本身就是按canvas尺寸保存的)
+      // 如果canvas尺寸不同,需要按比例缩放
+      const canvasWidth = canvasRef.value!.width
+      const canvasHeight = canvasRef.value!.height
+      const imgWidth = img.width
+      const imgHeight = img.height
+      
+      // 如果图片尺寸和canvas尺寸一致,直接绘制
+      if (imgWidth === canvasWidth && imgHeight === canvasHeight) {
+        ctx!.drawImage(img, 0, 0)
+      }
+      else {
+        // 如果尺寸不一致,按比例缩放(保持宽高比)
+        const scaleX = canvasWidth / imgWidth
+        const scaleY = canvasHeight / imgHeight
+        const scale = Math.min(scaleX, scaleY)
+        
+        const scaledWidth = imgWidth * scale
+        const scaledHeight = imgHeight * scale
+        const x = (canvasWidth - scaledWidth) / 2
+        const y = (canvasHeight - scaledHeight) / 2
+        
+        ctx!.drawImage(img, x, y, scaledWidth, scaledHeight)
+      }
       updateCtx()
     }
+    img.onerror = () => {
+      console.warn('📝 画图图片加载失败:', imageDataURL.substring(0, 50) + '...')
+    }
   }
 }
 

+ 46 - 3
src/views/Screen/WritingBoardTool.vue

@@ -1,10 +1,11 @@
 <template>
-  <div class="writing-board-tool">
+  <div class="writing-board-tool" @click.stop>
     <div class="writing-board-wrap"
       :style="{
         width: slideWidth + 'px',
         height: slideHeight + 'px',
       }"
+      @click.stop
     >
       <WritingBoard 
         ref="writingBoardRef" 
@@ -16,11 +17,13 @@
         :rubberSize="rubberSize"
         :shapeSize="shapeSize"
         :shapeType="shapeType"
+        :readonly="readonly"
         @end="hanldeWritingEnd()"
       />
     </div>
 
     <MoveablePanel 
+      v-if="!readonly"
       class="tools-panel" 
       :width="510" 
       :height="50"
@@ -82,7 +85,7 @@
           <div class="btn" v-tooltip="'清除墨迹'" @click="clearCanvas()">
             <IconClear class="icon" />
           </div>
-          <div class="btn" :class="{ 'active': blackboard }" v-tooltip="'黑板'" @click="blackboard = !blackboard">
+          <div class="btn" :class="{ 'active': blackboard }" v-tooltip="'黑板'" @click="handleBlackboardToggle">
             <IconFill class="icon" />
           </div>
           <div class="colors">
@@ -120,18 +123,26 @@ const writingBoardColors = ['#000000', '#ffffff', '#1e497b', '#4e81bb', '#e2534d
 
 type WritingBoardModel = 'pen' | 'mark' | 'eraser' | 'shape'
 
-withDefaults(defineProps<{
+const props = withDefaults(defineProps<{
   slideWidth: number
   slideHeight: number
   left?: number
   top?: number
+  readonly?: boolean
+  syncDataURL?: string | null
+  syncBlackboard?: boolean | null
 }>(), {
   left: -5,
   top: -5,
+  readonly: false,
+  syncDataURL: null,
+  syncBlackboard: null,
 })
 
 const emit = defineEmits<{
   (event: 'close'): void
+  (event: 'drawing-end', dataURL: string): void
+  (event: 'blackboard-change', blackboard: boolean): void
 }>()
 
 const { currentSlide } = storeToRefs(useSlidesStore())
@@ -164,21 +175,50 @@ const changeColor = (color: string) => {
   writingBoardColor.value = color
 }
 
+// 处理小黑板切换
+const handleBlackboardToggle = () => {
+  if (props.readonly) return // 只读模式下不允许切换
+  blackboard.value = !blackboard.value
+  // 通知父组件小黑板状态变化
+  emit('blackboard-change', blackboard.value)
+}
+
 // 关闭写字板
 const closeWritingBoard = () => {
+  // 只读模式下不允许关闭
+  if (props.readonly) {
+    console.log('📝 只读模式下不允许关闭画图工具')
+    return
+  }
   emit('close')
 }
 
 // 打开画笔工具或切换页面时,将数据库中存储的墨迹绘制到画布上
 watch(currentSlide, () => {
+  if (props.readonly) return // 只读模式下不从数据库加载
   db.writingBoardImgs.where('id').equals(currentSlide.value.id).toArray().then(ret => {
     const currentImg = ret[0]
     writingBoardRef.value!.setImageDataURL(currentImg?.dataURL || '')
   })
 }, { immediate: true })
 
+// 监听同步数据(学生端接收老师端的画图数据)
+watch(() => props.syncDataURL, (newDataURL) => {
+  if (props.readonly && newDataURL && writingBoardRef.value) {
+    writingBoardRef.value.setImageDataURL(newDataURL)
+  }
+}, { immediate: true })
+
+// 监听同步小黑板状态(学生端接收老师端的小黑板状态)
+watch(() => props.syncBlackboard, (newBlackboard) => {
+  if (props.readonly && newBlackboard !== null) {
+    blackboard.value = newBlackboard
+  }
+}, { immediate: true })
+
 // 每次绘制完成后将绘制完的图片更新到数据库
 const hanldeWritingEnd = () => {
+  if (props.readonly) return // 只读模式下不保存
   const dataURL = writingBoardRef.value!.getImageDataURL()
   if (!dataURL) return
 
@@ -187,6 +227,9 @@ const hanldeWritingEnd = () => {
     if (currentImg) db.writingBoardImgs.update(currentImg, { dataURL })
     else db.writingBoardImgs.add({ id: currentSlide.value.id, dataURL })
   })
+  
+  // 触发绘制结束事件,用于同步
+  emit('drawing-end', dataURL)
 }
 </script>
 

+ 303 - 9
src/views/Student/index.vue

@@ -144,8 +144,17 @@
         <SlideThumbnails v-if="slideThumbnailModelVisible" :turnSlideToIndex="goToSlide"
           @close="slideThumbnailModelVisible = false" />
 
-        <WritingBoardTool :slideWidth="slideWidth" :slideHeight="slideHeight" v-if="writingBoardToolVisible"
-          @close="writingBoardToolVisible = false" />
+        <WritingBoardTool 
+          :slideWidth="slideWidth" 
+          :slideHeight="slideHeight" 
+          v-if="writingBoardToolVisible || (props.type == '2' && isFollowModeActive && writingBoardSyncDataURL && writingBoardSyncDataURL.trim() !== '')"
+          :readonly="props.type == '2'"
+          :syncDataURL="props.type == '2' ? writingBoardSyncDataURL : null"
+          :syncBlackboard="props.type == '2' ? writingBoardSyncBlackboard : null"
+          @close="handleWritingBoardClose"
+          @drawing-end="handleDrawingEnd"
+          @blackboard-change="handleBlackboardChange"
+        />
 
         <CountdownTimer 
           v-if="timerlVisible" 
@@ -261,7 +270,7 @@
             <div class="homework-empty" v-else>
               暂无作业提交
             </div>
-          </div>--!>
+          </div>-->
           
        
           <!--<div v-if="unsubmittedStudents && unsubmittedStudents.length > 0" class="homework-title" style="margin-top: 20px;">未提交</div>
@@ -428,12 +437,12 @@ let lastLayout: { w: number; h: number } | null = null
 // 学生端覆盖层矩形(固定定位)
 const laserOverlayRect = ref<{ left: number; top: number; width: number; height: number }>({ left: 0, top: 0, width: 0, height: 0 })
 const laserOverlayStyle = computed(() => ({
-  position: 'fixed',
+  position: 'fixed' as const,
   left: laserOverlayRect.value.left + 'px',
   top: laserOverlayRect.value.top + 'px',
   width: laserOverlayRect.value.width + 'px',
   height: laserOverlayRect.value.height + 'px',
-  pointerEvents: 'auto',
+  pointerEvents: 'auto' as const,
   zIndex: 1000
 }))
 const refreshLaserOverlayRect = () => {
@@ -536,7 +545,11 @@ const docSocket = ref<Y.Doc | null>(null)
 const yMessage = ref<any | null>(null)
 const yTimerState = ref<any | null>(null)
 const yLaserState = ref<any | null>(null)
+const yWritingBoardState = ref<any | null>(null)
 const providerSocket = ref<WebsocketProvider | null>(null)
+// 学生端画图同步数据
+const writingBoardSyncDataURL = ref<string | null>(null)
+const writingBoardSyncBlackboard = ref<boolean | null>(null)
 const mId = ref<string | null>(null)
 
 // WebSocket重连相关变量
@@ -817,6 +830,47 @@ const nextSlide = () => {
 
 
 
+// 监听幻灯片切换,清除不匹配的画图数据
+watch(() => slideIndex.value, () => {
+  if (props.type == '2' && yWritingBoardState.value && currentSlide.value) {
+    const snap = yWritingBoardState.value.toJSON()
+    console.log('📝 幻灯片切换,检查画图数据:', { snap, currentSlideId: currentSlide.value.id })
+    if (snap && snap.slideId === currentSlide.value.id && snap.dataURL) {
+      // 当前幻灯片有画图数据,显示
+      writingBoardSyncDataURL.value = snap.dataURL
+      writingBoardSyncBlackboard.value = snap.blackboard !== undefined ? snap.blackboard : null
+      console.log('📝 当前幻灯片有画图数据,显示画图工具,小黑板状态:', writingBoardSyncBlackboard.value)
+    }
+    else {
+      // 当前幻灯片没有画图数据,隐藏
+      writingBoardSyncDataURL.value = null
+      writingBoardSyncBlackboard.value = null
+      console.log('📝 当前幻灯片没有画图数据,隐藏画图工具')
+    }
+  }
+})
+
+// 监听 currentSlide 变化,确保刷新后能获取到画图状态
+watch(() => currentSlide.value?.id, (newSlideId, oldSlideId) => {
+  // 只在学生端且跟随模式下检查
+  if (props.type == '2' && isFollowModeActive.value && yWritingBoardState.value && newSlideId) {
+    const snap = yWritingBoardState.value.toJSON()
+    console.log('📝 currentSlide变化,检查画图数据:', { snap, newSlideId, oldSlideId })
+    if (snap && snap.slideId === newSlideId && snap.dataURL) {
+      // 当前幻灯片有画图数据,显示
+      writingBoardSyncDataURL.value = snap.dataURL
+      writingBoardSyncBlackboard.value = snap.blackboard !== undefined ? snap.blackboard : null
+      console.log('📝 currentSlide变化后找到画图数据,显示画图工具,小黑板状态:', writingBoardSyncBlackboard.value)
+    }
+    else if (snap && snap.slideId !== newSlideId) {
+      // 当前幻灯片没有画图数据,隐藏
+      writingBoardSyncDataURL.value = null
+      writingBoardSyncBlackboard.value = null
+      console.log('📝 currentSlide变化后没有匹配的画图数据,隐藏画图工具')
+    }
+  }
+}, { immediate: true })
+
 // 监听slideIndex变化,调用getWork
 watch(() => slideIndex.value, (newIndex, oldIndex) => {
   console.log('slideIndex变化,调用getWork', { newIndex, oldIndex })
@@ -922,6 +976,113 @@ const getWorkId = () => {
   }
 }
 
+// 处理画图关闭事件
+const handleWritingBoardClose = () => {
+  // 学生端只读模式下,不应该响应关闭事件(因为关闭按钮已隐藏)
+  // 只有老师端可以关闭
+  if (props.type == '2') {
+    console.log('📝 学生端收到关闭事件,但只读模式下不应该关闭,忽略')
+    return
+  }
+  writingBoardToolVisible.value = false
+  // 老师端关闭时,清空共享状态并通知学生端
+  if (props.type == '1' && isFollowModeActive.value && isCreator.value) {
+    clearWritingBoardState()
+  }
+}
+
+// 清空画图共享状态(仅创建人)
+const clearWritingBoardState = () => {
+  try {
+    if (props.type == '1' && isCreator.value && yWritingBoardState.value) {
+      docSocket.value?.transact(() => {
+        yWritingBoardState.value.clear()
+      })
+      sendMessage({ 
+        type: 'writing_board_close', 
+        courseid: props.courseid 
+      })
+    }
+  }
+  catch (e) {
+    console.warn('清空画图状态失败', e)
+  }
+}
+
+// 处理小黑板状态变化(老师端)
+const handleBlackboardChange = (blackboard: boolean) => {
+  if (props.type == '1' && isFollowModeActive.value && isCreator.value) {
+    // 同步到共享 Map
+    if (yWritingBoardState.value) {
+      docSocket.value?.transact(() => {
+        yWritingBoardState.value.set('blackboard', blackboard)
+      })
+    }
+    // 广播消息
+    sendMessage({ 
+      type: 'writing_board_blackboard', 
+      blackboard: blackboard,
+      courseid: props.courseid 
+    })
+  }
+}
+
+// 处理画图结束事件(老师端)
+const handleDrawingEnd = (dataURL: string) => {
+  if (props.type == '1' && isFollowModeActive.value && isCreator.value) {
+    // 同步到共享 Map
+    if (yWritingBoardState.value) {
+      docSocket.value?.transact(() => {
+        yWritingBoardState.value.set('slideId', currentSlide.value.id)
+        yWritingBoardState.value.set('dataURL', dataURL)
+        // 保持小黑板状态
+        const currentBlackboard = yWritingBoardState.value.get('blackboard')
+        if (currentBlackboard !== undefined) {
+          yWritingBoardState.value.set('blackboard', currentBlackboard)
+        }
+      })
+    }
+    // 广播消息(包含当前小黑板状态)
+    const currentBlackboard = yWritingBoardState.value?.get('blackboard') || false
+    sendMessage({ 
+      type: 'writing_board_update', 
+      slideId: currentSlide.value.id,
+      dataURL: dataURL,
+      blackboard: currentBlackboard,
+      courseid: props.courseid 
+    })
+  }
+}
+
+// 应用画图共享状态(任意端)
+const applyWritingBoardStateSnapshot = (snap: any) => {
+  console.log('📝 应用画图状态快照:', snap, '当前幻灯片ID:', currentSlide.value?.id, '跟随模式:', isFollowModeActive.value, '用户类型:', props.type)
+  if (!snap || !snap.dataURL || typeof snap.dataURL !== 'string' || snap.dataURL.trim() === '') {
+    writingBoardSyncDataURL.value = null
+    writingBoardSyncBlackboard.value = null
+    console.log('📝 画图状态为空,隐藏画图工具')
+    return
+  }
+  const slideId = snap.slideId
+  const dataURL = snap.dataURL
+  const blackboardState = snap.blackboard !== undefined ? snap.blackboard : null
+  // 只有当前幻灯片匹配时才显示
+  if (slideId && currentSlide.value && slideId === currentSlide.value.id) {
+    writingBoardSyncDataURL.value = dataURL
+    writingBoardSyncBlackboard.value = blackboardState
+    console.log('📝 画图数据匹配,显示画图工具,数据长度:', dataURL.length, '小黑板状态:', blackboardState, '显示条件:', {
+      type: props.type,
+      isFollowModeActive: isFollowModeActive.value,
+      hasData: !!writingBoardSyncDataURL.value
+    })
+  }
+  else {
+    writingBoardSyncDataURL.value = null
+    writingBoardSyncBlackboard.value = null
+    console.log('📝 画图数据不匹配,隐藏画图工具', { slideId, currentSlideId: currentSlide.value?.id })
+  }
+}
+
 // 切换激光笔模式
 const toggleLaserPen = () => {
   laserPen.value = !laserPen.value
@@ -2214,6 +2375,43 @@ const messageInit = () => {
       applyLaserStateSnapshot(s)
     })
   }
+  // 初始化画图状态 Map 并监听
+  if (docSocket.value && !yWritingBoardState.value) {
+    yWritingBoardState.value = docSocket.value.getMap('writingBoardState')
+    const wsnap = yWritingBoardState.value.toJSON()
+    console.log('📝 初始化画图状态Map,快照:', wsnap, '当前幻灯片:', currentSlide.value?.id)
+    // 延迟应用,确保 currentSlide 已初始化
+    nextTick(() => {
+      // 如果 currentSlide 还没准备好,再等一帧
+      if (currentSlide.value && currentSlide.value.id) {
+        applyWritingBoardStateSnapshot(wsnap)
+      }
+      else {
+        // 如果还没准备好,等待 currentSlide 变化(最多等待3秒)
+        let timeoutId: any = null
+        const unwatch = watch(() => currentSlide.value?.id, (slideId) => {
+          if (slideId) {
+            applyWritingBoardStateSnapshot(wsnap)
+            unwatch()
+            if (timeoutId) clearTimeout(timeoutId)
+          }
+        }, { immediate: true })
+        // 3秒后如果还没准备好,强制应用一次
+        timeoutId = setTimeout(() => {
+          if (currentSlide.value && currentSlide.value.id) {
+            applyWritingBoardStateSnapshot(wsnap)
+          }
+          unwatch()
+        }, 3000)
+      }
+    })
+    yWritingBoardState.value.observe(() => {
+      const s = yWritingBoardState.value.toJSON()
+      if (currentSlide.value && currentSlide.value.id) {
+        applyWritingBoardStateSnapshot(s)
+      }
+    })
+  }
 }
 
 /**
@@ -2298,6 +2496,65 @@ const getMessages = (msgObj: any) => {
     }
   }
 
+  // 画图:老师广播的画图数据
+  if (props.type == '2' && msgObj.type === 'writing_board_update' && msgObj.courseid === props.courseid) {
+    console.log('📝 学生端收到画图更新消息:', { slideId: msgObj.slideId, currentSlideId: currentSlide.value?.id, hasData: !!msgObj.dataURL })
+    if (currentSlide.value && msgObj.slideId === currentSlide.value.id) {
+      writingBoardSyncDataURL.value = msgObj.dataURL || null
+      // 如果消息中包含小黑板状态,也更新
+      if (msgObj.blackboard !== undefined) {
+        writingBoardSyncBlackboard.value = msgObj.blackboard
+      }
+      console.log('📝 画图数据匹配当前幻灯片,显示画图工具')
+      // 同步到共享 Map
+      if (yWritingBoardState.value) {
+        docSocket.value?.transact(() => {
+          yWritingBoardState.value.set('slideId', msgObj.slideId)
+          yWritingBoardState.value.set('dataURL', msgObj.dataURL)
+          if (msgObj.blackboard !== undefined) {
+            yWritingBoardState.value.set('blackboard', msgObj.blackboard)
+          }
+        })
+      }
+    }
+    else {
+      // 不是当前幻灯片,但也要更新到 Map(供后续切换时使用)
+      if (yWritingBoardState.value) {
+        docSocket.value?.transact(() => {
+          yWritingBoardState.value.set('slideId', msgObj.slideId)
+          yWritingBoardState.value.set('dataURL', msgObj.dataURL)
+          if (msgObj.blackboard !== undefined) {
+            yWritingBoardState.value.set('blackboard', msgObj.blackboard)
+          }
+        })
+      }
+      console.log('📝 画图数据不匹配当前幻灯片,已保存到Map供后续使用')
+    }
+  }
+
+  // 画图:老师关闭画图工具
+  if (props.type == '2' && msgObj.type === 'writing_board_close' && msgObj.courseid === props.courseid) {
+    writingBoardSyncDataURL.value = null
+    writingBoardSyncBlackboard.value = null
+    // 清空共享 Map
+    if (yWritingBoardState.value) {
+      docSocket.value?.transact(() => {
+        yWritingBoardState.value.clear()
+      })
+    }
+  }
+
+  // 画图:老师切换小黑板状态
+  if (props.type == '2' && msgObj.type === 'writing_board_blackboard' && msgObj.courseid === props.courseid) {
+    writingBoardSyncBlackboard.value = msgObj.blackboard || false
+    // 同步到共享 Map
+    if (yWritingBoardState.value) {
+      docSocket.value?.transact(() => {
+        yWritingBoardState.value.set('blackboard', msgObj.blackboard)
+      })
+    }
+  }
+
 }
 
 
@@ -2312,12 +2569,13 @@ const openChoiceQuestionDetail = (index:number) => {
 }
 
 const handlePageUnload = () => {
-  if (isCreator.value && timerIndicator.value.visible) {
+  if (isCreator.value && timerIndicator.value.visible && props.type === '1') {
     sendMessage({ type: 'timer_stop', courseid: props.courseid })
   }
-  // 创建老师刷新/关闭页面时,清空激光笔共享状态
-  if (isCreator.value) {
+  // 创建老师刷新/关闭页面时,清空激光笔和画图共享状态
+  if (isCreator.value && props.type === '1') {
     clearLaserState()
+    clearWritingBoardState()
   }
 }
 
@@ -2406,8 +2664,9 @@ onMounted(() => {
   // visibilitychange 事件(适用于 iframe 嵌套场景,当外层页面返回时触发)
   const handleVisibilityChange = () => {
     if (document.hidden && isCreator.value) {
-      // 页面被隐藏时,清空激光笔状态
+      // 页面被隐藏时,清空激光笔和画图状态
       clearLaserState()
+      clearWritingBoardState()
       if (timerIndicator.value.visible) {
         sendMessage({ type: 'timer_stop', courseid: props.courseid })
       }
@@ -2531,6 +2790,41 @@ const createWebSocketConnection = () => {
             const s = yLaserState.value.toJSON()
             applyLaserStateSnapshot(s)
           })
+          // 画图 map
+          yWritingBoardState.value = docSocket.value.getMap('writingBoardState')
+          const ws = yWritingBoardState.value.toJSON()
+          console.log('📝 WebSocket连接成功,读取画图状态:', ws, '当前幻灯片:', currentSlide.value?.id)
+          // 延迟应用,确保 currentSlide 已初始化
+          nextTick(() => {
+            // 如果 currentSlide 还没准备好,再等一帧
+            if (currentSlide.value && currentSlide.value.id) {
+              applyWritingBoardStateSnapshot(ws)
+            }
+            else {
+              // 如果还没准备好,等待 currentSlide 变化(最多等待3秒)
+              let timeoutId: any = null
+              const unwatch = watch(() => currentSlide.value?.id, (slideId) => {
+                if (slideId) {
+                  applyWritingBoardStateSnapshot(ws)
+                  unwatch()
+                  if (timeoutId) clearTimeout(timeoutId)
+                }
+              }, { immediate: true })
+              // 3秒后如果还没准备好,强制应用一次
+              timeoutId = setTimeout(() => {
+                if (currentSlide.value && currentSlide.value.id) {
+                  applyWritingBoardStateSnapshot(ws)
+                }
+                unwatch()
+              }, 3000)
+            }
+          })
+          yWritingBoardState.value.observe(() => {
+            const s = yWritingBoardState.value.toJSON()
+            if (currentSlide.value && currentSlide.value.id) {
+              applyWritingBoardStateSnapshot(s)
+            }
+          })
         }
       }
       else if (event.status === 'disconnected') {