Просмотр исходного кода

feat: 更新多语言支持并优化UI组件

refactor(components): 重构Popover和CollapsibleToolbar组件样式
fix(useImport): 修复base64图片处理逻辑
docs(lang): 添加多语言翻译字段
style(components): 调整边框和圆角样式
lsc 3 недель назад
Родитель
Сommit
75aa47b3ee

+ 35 - 35
src/components/CollapsibleToolbar/index2.vue

@@ -11,7 +11,7 @@
               <path id="Vector_2" d="M12.8335 1.83398V7.33398H18.3335" stroke="currentColor" stroke-width="1.83333" />
             </g>
           </svg>
-          <span class="item-label">页面</span>
+          <span class="item-label">{{ lang.ssPage }}</span>
         </div>
         <div class="sidebar-item" :class="{ active: activeSubmenu === 'interactive' }"
           @click="toggleSubmenu('interactive')">
@@ -42,7 +42,7 @@
             <path d="M2 12h20" />
             <path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z" />
           </svg>
-          <span class="item-label">交互网页</span>
+          <span class="item-label">{{ lang.ssInteractiveWebpage }}</span>
         </div>
         <!-- <div class="sidebar-item" @click="handleToolClick('video')">
           <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -68,13 +68,13 @@
                 stroke="currentColor" stroke-width="1.83333" />
             </g>
           </svg>
-          <span class="item-label">多媒体</span>
+          <span class="item-label">{{ lang.ssMultimedia }}</span>
         </div>
       </div>
     </div>
     <div class="submenu" :class="{ visible: activeSubmenu === 'page' }">
       <div class="submenu-title">
-        <div class="title">添加模版页面</div>
+        <div class="title">{{ lang.ssAddTemplatePage }}</div>
         <div class="close-icon" @click="toggleSubmenu('page')">
           <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
             <g id="Component 3">
@@ -94,7 +94,7 @@
               </g>
             </svg>
           </div>
-          <span class="submenu-label">标题页</span>
+          <span class="submenu-label">{{ lang.ssTitlePage }}</span>
         </div>
         <div class="submenu-item">
           <div class="submenu-icon">
@@ -110,7 +110,7 @@
               </g>
             </svg>
           </div>
-          <span class="submenu-label">图片页</span>
+          <span class="submenu-label">{{ lang.ssImagePage }}</span>
         </div>
         <div class="submenu-item">
           <div class="submenu-icon">
@@ -124,7 +124,7 @@
               </g>
             </svg>
           </div>
-          <span class="submenu-label">内容页</span>
+          <span class="submenu-label">{{ lang.ssContentPage }}</span>
         </div>
         <div class="submenu-item">
           <div class="submenu-icon">
@@ -139,7 +139,7 @@
               </g>
             </svg>
           </div>
-          <span class="submenu-label">文图页</span>
+          <span class="submenu-label">{{ lang.ssTextImagePage }}</span>
         </div>
         <div class="submenu-item">
           <div class="submenu-icon">
@@ -154,7 +154,7 @@
               </g>
             </svg>
           </div>
-          <span class="submenu-label">图文页</span>
+          <span class="submenu-label">{{ lang.ssImageTextPage }}</span>
         </div>
       </div>
       <FileInput accept="application/vnd.openxmlformats-officedocument.presentationml.presentation"
@@ -172,13 +172,13 @@
               </g>
             </svg>
           </div>
-          <span class="submenu-label">上传PPT</span>
+          <span class="submenu-label">{{ lang.ssUploadPPT }}</span>
         </div>
       </FileInput>
     </div>
     <div class="submenu" :class="{ visible: activeSubmenu === 'interactive' }">
       <div class="submenu-title">
-        <div class="title">添加互动工具</div>
+        <div class="title">{{ lang.ssAddInteractiveTool }}</div>
         <div class="close-icon" @click="toggleSubmenu('interactive')">
           <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
             <g id="Component 3">
@@ -210,7 +210,7 @@
                 fill="#E5E7EB" />
             </g>
           </svg>
-          <div class="detail">选择工具创建互动画面</div>
+          <div class="detail">{{ lang.ssSelectToolCreateInteractive }}</div>
         </div>
         <img class="submenu-img" v-else-if="hoveredTool === 'qa'" key="qa" :src="toolAnswer" alt="">
         <img class="submenu-img" v-else-if="hoveredTool === 'choice'" key="choice" :src="toolChoice" alt="">
@@ -235,7 +235,7 @@
     </div>
     <div class="submenu" :class="{ visible: activeSubmenu === 'aiapp' }">
       <div class="submenu-title">
-        <div class="title">添加AI应用</div>
+        <div class="title">{{ lang.ssAddAIApp }}</div>
         <div class="close-icon" @click="toggleSubmenu('aiapp')">
           <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
             <g id="Component 3">
@@ -266,7 +266,7 @@
               </g>
             </svg>
           </div>
-          <span class="submenu-label">应用中心</span>
+          <span class="submenu-label">{{ lang.ssAppCenter }}</span>
         </div>
         <div class="submenu-item">
           <div class="submenu-icon">
@@ -280,13 +280,13 @@
               </g>
             </svg>
           </div>
-          <span class="submenu-label">创建应用</span>
+          <span class="submenu-label">{{ lang.ssCreateApp }}</span>
         </div>
       </div>
     </div>
     <div class="submenu" :class="{ visible: activeSubmenu === 'h5page' }">
       <div class="submenu-title">
-        <div class="title">添加交互网页</div>
+        <div class="title">{{ lang.ssAddInteractiveWebpage }}</div>
         <div class="close-icon" @click="toggleSubmenu('h5page')">
           <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
             <g id="Component 3">
@@ -319,7 +319,7 @@
               </g>
             </svg>
           </div>
-          <span class="submenu-label">网页中心</span>
+          <span class="submenu-label">{{ lang.ssWebpageCenter }}</span>
         </div>
         <div class="submenu-item">
           <div class="submenu-icon">
@@ -333,7 +333,7 @@
               </g>
             </svg>
           </div>
-          <span class="submenu-label">上传网页</span>
+          <span class="submenu-label">{{ lang.ssUploadWebpage }}</span>
         </div>
         <div class="submenu-item">
           <div class="submenu-icon">
@@ -350,13 +350,13 @@
               </g>
             </svg>
           </div>
-          <span class="submenu-label">爬取网页</span>
+          <span class="submenu-label">{{ lang.ssCrawlWebpage }}</span>
         </div>
       </div>
     </div>
     <div class="submenu" :class="{ visible: activeSubmenu === 'multimedia' }">
       <div class="submenu-title">
-        <div class="title">添加多媒体</div>
+        <div class="title">{{ lang.ssAddMultimedia }}</div>
         <div class="close-icon" @click="toggleSubmenu('multimedia')">
           <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
             <g id="Component 3">
@@ -368,7 +368,7 @@
         </div>
       </div>
       <div class="submenu-item-box2">
-        <div class="submenu-item">
+        <div class="submenu-item"  @click="handleToolClick('video')">
           <div class="submenu-icon">
             <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
               <g id="Component 1">
@@ -379,9 +379,9 @@
               </g>
             </svg>
           </div>
-          <span class="submenu-label">视频</span>
+          <span class="submenu-label">{{ lang.ssVideo }}</span>
         </div>
-        <div class="submenu-item">
+        <!-- <div class="submenu-item">
           <div class="submenu-icon">
             <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
               <g id="Component 1">
@@ -395,7 +395,7 @@
               </g>
             </svg>
           </div>
-          <span class="submenu-label">音频</span>
+          <span class="submenu-label">{{ lang.ssAudio }}</span>
         </div>
         <div class="submenu-item">
           <div class="submenu-icon">
@@ -410,7 +410,7 @@
               </g>
             </svg>
           </div>
-          <span class="submenu-label">文档</span>
+          <span class="submenu-label">{{ lang.ssDocument }}</span>
         </div>
         <div class="submenu-item">
           <div class="submenu-icon">
@@ -427,8 +427,8 @@
               </g>
             </svg>
           </div>
-          <span class="submenu-label">文档集</span>
-        </div>
+          <span class="submenu-label">{{ lang.ssDocumentSet }}</span>
+        </div> -->
       </div>
     </div>
 
@@ -444,11 +444,11 @@
           </svg>
 
         </div>
-        <h3>{{ exporting ? '解析中...' : '导出完成' }}</h3>
-        <p v-if="exporting">正在解析 {{ currentFileName }}</p>
-        <p v-if="!exporting">解析完成,已生成课件</p>
+        <h3>{{ exporting ? lang.ssParsing : lang.ssExportCompleted }}</h3>
+        <p v-if="exporting">{{ lang.ssParsingFile }}{{ currentFileName }}</p>
+        <p v-if="!exporting">{{ lang.ssParsingCompleted }}</p>
         <button class="close-btn2" @click="handleParsingClose">
-          {{ exporting ? '关闭' : '完成' }}
+          {{ exporting ? lang.ssClose : lang.ssComplete }}
         </button>
       </div>
     </div>
@@ -658,11 +658,11 @@ const handleFileUpload = async (files: FileList) => {
   }
   catch (error) {
     if (error instanceof DOMException && error.name === 'AbortError') {
-      console.log('文件解析已取消')
+      console.log(lang.ssFileParseCancelled)
     }
     else {
-      console.error('文件解析失败:', error)
-      message.error('文件解析失败,请重试')
+      console.error(lang.ssFileParseFailed, error)
+      message.error(lang.ssFileParseFailedRetry)
     }
   }
 }
@@ -672,7 +672,7 @@ const handleParsingClose = () => {
     parsingAbortController.value.abort()
     exporting.value = false
     parsingAbortController.value = null
-    // message.info('解析已取消')
+    // message.info(lang.ssParseCancelled)
   }
   else if (!exporting.value) {
     emit('close')

+ 2 - 1
src/components/CreateCourseDialog.vue

@@ -144,7 +144,8 @@ const handleFileUpload = async (files: FileList) => {
     }
     else {
       console.error('文件解析失败:', error)
-      message.error('文件解析失败,请重试')
+      // message.error('文件解析失败,请重试')
+      message.error(lang.ssFileParseFailedRetry)
     }
   }
 }

+ 3 - 2
src/components/Popover.vue

@@ -96,9 +96,10 @@ onMounted(() => {
 .popover-content {
   background-color: #fff;
   padding: 10px;
-  border: 1px solid $borderColor;
+  // border: 1px solid $borderColor;
   box-shadow: $boxShadow;
-  border-radius: $borderRadius;
+  border-radius: 10px;
+  // border-radius: $borderRadius;
   font-size: 13px;
 }
 </style>

+ 6 - 4
src/components/PopoverMenuItem.vue

@@ -16,20 +16,22 @@ const emit = defineEmits<{
 }>()
 </script>
 
-<style lang="scss" scoped>
+<style lang="scss" sco1ped>
 .popover-menu-item {
   min-width: 80px;
-  padding: 6px 10px;
-  border-radius: $borderRadius;
+  padding: 10px 10px;
+  // border-radius: $borderRadius;
+  border-radius: 5px;
   font-size: 13px;
   cursor: pointer;
+  transition: all 0.3s ease;
 
   &.center {
     text-align: center;
   }
 
   &:hover {
-    background-color: #f1f1f1;
+    background-color: #f3f4f6;
   }
   & + .popover-menu-item {
     margin-top: 2px;

+ 13 - 12
src/hooks/useImport.ts

@@ -528,17 +528,17 @@ export default () => {
         const response = await fetch(input)
         const blob = await response.blob()
         return { blob, mime: blob.type }
-      } else {
-        // 纯 base64 字符串 → 按原逻辑默认当作 PNG
-        const binary = atob(input)
-        const bytes = new Uint8Array(binary.length)
-        for (let i = 0; i < binary.length; i++) {
-          bytes[i] = binary.charCodeAt(i)
-        }
-        // 默认 MIME 为 image/png(与原函数行为一致)
-        const blob = new Blob([bytes], { type: 'image/png' })
-        return { blob, mime: 'image/png' }
+      } 
+      // 纯 base64 字符串 → 按原逻辑默认当作 PNG
+      const binary = atob(input)
+      const bytes = new Uint8Array(binary.length)
+      for (let i = 0; i < binary.length; i++) {
+        bytes[i] = binary.charCodeAt(i)
       }
+      // 默认 MIME 为 image/png(与原函数行为一致)
+      const blob = new Blob([bytes], { type: 'image/png' })
+      return { blob, mime: 'image/png' }
+      
     }
   
     // 获取统一的 blob 和实际 MIME 类型
@@ -552,7 +552,7 @@ export default () => {
     // ----- PNG 格式:执行白色变透明处理 -----
     // 1. 创建对象 URL 用于加载图片
     const imageUrl = URL.createObjectURL(blob)
-    let needRevoke = true
+    const needRevoke = true
   
     // 2. 加载图像
     const img = await new Promise<HTMLImageElement>((resolve, reject) => {
@@ -590,7 +590,8 @@ export default () => {
       }
   
       ctx.putImageData(imageData, 0, 0)
-    } finally {
+    }
+    finally {
       if (needRevoke) {
         URL.revokeObjectURL(imageUrl)
       }

+ 1 - 1
src/views/Editor/CanvasTool/ShapePool.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="shape-pool">
     <div class="category" v-for="item in SHAPE_LIST" :key="item.type">
-      <div class="category-name">{{item.type}}</div>
+      <!-- <div class="category-name">{{item.type}}</div> -->
       <div class="shape-list">
         <ShapeItemThumbnail 
           class="shape-item"

+ 394 - 0
src/views/Editor/CanvasTool/index2.vue

@@ -0,0 +1,394 @@
+<template>
+  <div class="canvas-tool">
+    <div class="left-handler">
+      <Popover trigger="click" v-model:value="textTypeSelectVisible"
+        style="height: 100%;display: flex;align-items: center;" :offset="10">
+        <template #content>
+          <PopoverMenuItem center @click="() => { drawText(); textTypeSelectVisible = false }">
+            <IconTextRotationNone /> {{ lang.ssTextHorizontal }}
+          </PopoverMenuItem>
+          <PopoverMenuItem center @click="() => { drawText(true); textTypeSelectVisible = false }">
+            <IconTextRotationDown /> {{ lang.ssTextVertical }}
+          </PopoverMenuItem>
+        </template>
+        <div class="handler-item">
+          <IconFontSize class="icon" :class="{ 'active': creatingElement?.type === 'text' }" @click="drawText()" />
+          <span>{{ lang.ssText }}</span>
+        </div>
+      </Popover>
+      <Popover trigger="click" style="height: 100%;display: flex;align-items: center;"
+        v-model:value="picturePoolVisible" :offset="10">
+        <template #content>
+          <FileInput @change="files => insertImageElement(files)">
+            <div class="popover-item">
+              <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
+                <polyline points="17 8 12 3 7 8" />
+                <line x1="12" y1="3" x2="12" y2="15" />
+              </svg>
+              <span>{{ lang.ssUploadFromLocal }}</span>
+            </div>
+          </FileInput>
+          <!-- <div class="popover-item">
+            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+              stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+              <circle cx="11" cy="11" r="8" />
+              <path d="M21 21l-4.35-4.35" />
+            </svg>
+            <span>{{ lang.ssSearchFromWeb }}</span>
+          </div>
+          <div class="popover-item">
+            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+              stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+              <path d="M12 2L2 7l10 5 10-5-10-5z" />
+              <path d="M2 17l10 5 10-5" />
+              <path d="M2 12l10 5 10-5" />
+            </svg>
+            <span>{{ lang.ssGenerateFromAI }}</span>
+          </div> -->
+        </template>
+        <div class="handler-item">
+          <IconPicture class="icon" v-tooltip="lang.ssInsertImage" />
+          <span>{{ lang.ssImage }}</span>
+        </div>
+        <!-- <FileInput @change="files => insertImageElement(files)">
+          </FileInput> -->
+      </Popover>
+      <Popover trigger="click" style="height: 100%;display: flex;align-items: center;" v-model:value="shapePoolVisible"
+        :offset="10">
+        <template #content>
+          <ShapePool @select="shape => drawShape(shape)" />
+        </template>
+        <div class="handler-item">
+          <IconGraphicDesign class="icon"
+            :class="{ 'active': creatingCustomShape || creatingElement?.type === 'shape' }" />
+          <span>{{ lang.ssShape }}</span>
+        </div>
+      </Popover>
+    </div>
+
+
+    <Modal v-model:visible="latexEditorVisible" :width="880">
+      <LaTeXEditor @close="latexEditorVisible = false"
+        @update="data => { createLatexElement(data); latexEditorVisible = false }" />
+    </Modal>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed } from 'vue'
+import { storeToRefs } from 'pinia'
+import { useMainStore, useSnapshotStore, useSlidesStore } from '@/store'
+import { getImageDataURL } from '@/utils/image'
+import useImport from '@/hooks/useImport'
+import type { ShapePoolItem } from '@/configs/shapes'
+import type { LinePoolItem } from '@/configs/lines'
+import useScaleCanvas from '@/hooks/useScaleCanvas'
+import useHistorySnapshot from '@/hooks/useHistorySnapshot'
+import useCreateElement from '@/hooks/useCreateElement'
+import { lang } from '@/main'
+
+import ShapePool from './ShapePool.vue'
+import LinePool from './LinePool.vue'
+import ChartPool from './ChartPool.vue'
+import TableGenerator from './TableGenerator.vue'
+import MediaInput from './MediaInput.vue'
+import WebpageInput from './WebpageInput.vue'
+import LaTeXEditor from '@/components/LaTeXEditor/index.vue'
+import FileInput from '@/components/FileInput.vue'
+import Modal from '@/components/Modal.vue'
+import Divider from '@/components/Divider.vue'
+import Popover from '@/components/Popover.vue'
+import PopoverMenuItem from '@/components/PopoverMenuItem.vue'
+
+const mainStore = useMainStore()
+const slidesStore = useSlidesStore()
+const { creatingElement, creatingCustomShape, showSelectPanel, showSearchPanel, showNotesPanel } = storeToRefs(mainStore)
+const { canUndo, canRedo } = storeToRefs(useSnapshotStore())
+const { currentSlide } = storeToRefs(slidesStore)
+
+const getInitialViewMode = () => {
+  const urlParams = new URLSearchParams(window.location.search)
+  const modeFromUrl = urlParams.get('mode')
+  if (modeFromUrl === 'editor2') {
+    return 'editor2'
+  }
+  const modeFromStorage = localStorage.getItem('viewMode')
+  if (modeFromStorage) {
+    return modeFromStorage
+  }
+  return 'editor'
+}
+
+const viewMode = computed(() => getInitialViewMode())
+
+const hasInteractiveTool = computed(() => {
+  const elements = currentSlide.value?.elements || []
+  return elements.some((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15))
+})
+
+const editTool = () => {
+  const elements = currentSlide.value?.elements || []
+  const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15))
+  if (frameElement) {
+    const url = frameElement.url || ''
+
+    interface ParentWindowWithToolList extends Window {
+      toolBtn?: (action: number, id: string) => void;
+    }
+    const parentWindow = window.parent as ParentWindowWithToolList
+    parentWindow?.toolBtn?.(0, url)
+  }
+}
+
+const { redo, undo } = useHistorySnapshot()
+
+const {
+  scaleCanvas,
+  setCanvasScalePercentage,
+  resetCanvas,
+  canvasScalePercentage,
+} = useScaleCanvas()
+
+const canvasScalePresetList = [200, 150, 125, 100, 75, 50]
+const canvasScaleVisible = ref(false)
+
+const applyCanvasPresetScale = (value: number) => {
+  setCanvasScalePercentage(value)
+  canvasScaleVisible.value = false
+}
+
+const {
+  createImageElement,
+  createChartElement,
+  createTableElement,
+  createLatexElement,
+  createVideoElement,
+  createAudioElement,
+  createFrameElement,
+} = useCreateElement()
+
+const { uploadFileToS3 } = useImport()
+const insertImageElement = async (files: FileList) => {
+  const imageFile = files[0]
+  if (!imageFile) return
+  const url = await uploadFileToS3(imageFile)
+  console.log(url)
+  
+  // getImageDataURL(imageFile).then(dataURL => createImageElement(dataURL))
+  createImageElement(url)
+  picturePoolVisible.value = false
+}
+
+const shapePoolVisible = ref(false)
+const picturePoolVisible = ref(false)
+const linePoolVisible = ref(false)
+const chartPoolVisible = ref(false)
+const tableGeneratorVisible = ref(false)
+const mediaInputVisible = ref(false)
+const webpageInputVisible = ref(false)
+
+// 预设的网页列表
+const webpageList = ref<Array<{ type: number, title: string, url: string }>>([])
+
+// 点击插入学习内容时获取数据
+const handleInsertLearningContent = () => {
+  try {
+    // 获取父窗口的工具列表,如果没有则使用空数组
+    // 定义父窗口的类型,添加pptToolList属性
+    interface ParentWindowWithToolList extends Window {
+      pptToolList?: Array<{ tool?: number; title?: string; url?: string }>
+    }
+    const parentWindow = window.parent as ParentWindowWithToolList
+    const pptToolList = parentWindow?.pptToolList || []
+    // 转换父窗口的工具列表格式
+    webpageList.value = pptToolList.map((item: any) => ({
+      type: item.tool || 0,
+      title: item.title || lang.ssUnknownTool,
+      url: item.url || '#'
+    })).filter((item: any) => item.url !== '#') // 过滤掉无效的URL
+
+    console.log('学习内容列表加载完成:', webpageList.value.length, '个项目')
+
+    // 显示弹窗
+    webpageInputVisible.value = true
+
+  }
+  catch (error) {
+    console.error('加载学习内容失败:', error)
+
+    // 发生错误时使用空数组
+    webpageList.value = []
+
+    // 显示弹窗
+    webpageInputVisible.value = true
+  }
+}
+const latexEditorVisible = ref(false)
+const textTypeSelectVisible = ref(false)
+const shapeMenuVisible = ref(false)
+const moreVisible = ref(false)
+
+// 绘制文字范围
+const drawText = (vertical = false) => {
+  mainStore.setCreatingElement({
+    type: 'text',
+    vertical,
+  })
+}
+
+// 绘制形状范围
+const drawShape = (shape: ShapePoolItem) => {
+  mainStore.setCreatingElement({
+    type: 'shape',
+    data: shape,
+  })
+  shapePoolVisible.value = false
+}
+// 绘制自定义任意多边形
+const drawCustomShape = () => {
+  mainStore.setCreatingCustomShapeState(true)
+  shapePoolVisible.value = false
+}
+
+// 绘制线条路径
+const drawLine = (line: LinePoolItem) => {
+  mainStore.setCreatingElement({
+    type: 'line',
+    data: line,
+  })
+  linePoolVisible.value = false
+}
+
+
+
+// 打开选择面板
+const toggleSelectPanel = () => {
+  mainStore.setSelectPanelState(!showSelectPanel.value)
+}
+
+// 打开搜索替换面板
+const toggleSraechPanel = () => {
+  mainStore.setSearchPanelState(!showSearchPanel.value)
+}
+
+// 打开批注面板
+const toggleNotesPanel = () => {
+  mainStore.setNotesPanelState(!showNotesPanel.value)
+}
+</script>
+
+<style lang="scss" scoped>
+.canvas-tool {
+  position: relative;
+  border-bottom: 1px solid $borderColor;
+  background-color: #fff;
+  display: flex;
+  padding: 0 10px;
+  font-size: 13px;
+  user-select: none;
+}
+
+.left-handler {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+
+  .handler-item {
+    // width: 32px;
+    background-color: #f9fafb;
+    border-radius: 5px;
+    transition: all 0.3s ease;
+
+    &:not(.group-btn):hover {
+      background-color: #f1f1f1;
+    }
+
+    &.active {
+      color: $themeColor;
+    }
+
+    .icon {
+      margin-right: 5px;
+    }
+  }
+}
+
+.handler-item {
+  height: 35px;
+  font-size: 14px;
+  // margin: 0 2px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: $borderRadius;
+  overflow: hidden;
+  cursor: pointer;
+
+  &.disable {
+    opacity: .5;
+  }
+}
+
+.left-handler,
+.right-handler {
+  .handler-item {
+    padding: 0 15px;
+
+    &.active,
+    &:not(.disable):hover {
+      background-color: #f1f1f1;
+    }
+  }
+}
+
+.right-handler {
+  display: flex;
+  align-items: center;
+
+  .text {
+    display: inline-block;
+    width: 40px;
+    text-align: center;
+    cursor: pointer;
+  }
+
+  .viewport-size {
+    font-size: 13px;
+  }
+}
+
+.edit-tool-btn {
+  color: #285cf5;
+  cursor: pointer;
+}
+
+.popover-item {
+  min-width: 80px;
+  padding: 10px 10px;
+  // border-radius: $borderRadius;
+  border-radius: 5px;
+  font-size: 13px;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  display: flex;
+  align-items: center;
+
+  svg {
+    margin-right: 5px;
+    width: 1em;
+    height: 1em;
+  }
+  &.center {
+    text-align: center;
+  }
+
+  &:hover {
+    background-color: #f3f4f6;
+  }
+  & + .popover-menu-item {
+    margin-top: 2px;
+  }
+}
+</style>

+ 5 - 5
src/views/Editor/index3.vue

@@ -145,7 +145,7 @@
       <CollapsibleToolbar class="layout-sidebar" @toggle="handleToolbarToggle" />
       <div class="layout-content-center">
         <CanvasTool class="center-top" />
-        <Canvas class="center-body" :style="{ height: `calc(100% - ${remarkHeight + 40}px  - 120px)` }"
+        <Canvas class="center-body" :style="{ height: `calc(100% - ${remarkHeight + 60}px  - 120px)` }"
           :courseid="props.courseid" @course-loaded="handleCourseLoaded"  ref="canvas"/>
         <!-- <Remark
           class="center-bottom" 
@@ -173,10 +173,10 @@
     <AIPPTDialog />
   </Modal>
 
-  <Modal class="createCourseDialog" :visible="showCreateCourseDialog" :closeOnClickMask="false" :closeOnEsc="false"
+  <!-- <Modal class="createCourseDialog" :visible="showCreateCourseDialog" :closeOnClickMask="false" :closeOnEsc="false"
     :closeButton="false" @closed="closeCreateCourseDialog()">
     <CreateCourseDialog @close="closeCreateCourseDialog" @select="handleCreateCourseSelect" />
-  </Modal>
+  </Modal> -->
 </template>
 
 <script lang="ts" setup>
@@ -188,7 +188,7 @@ import usePasteEvent from '@/hooks/usePasteEvent'
 
 import EditorHeader from './EditorHeader/index.vue'
 import Canvas from './Canvas/index.vue'
-import CanvasTool from './CanvasTool/index.vue'
+import CanvasTool from './CanvasTool/index2.vue'
 import Thumbnails from './Thumbnails/index2.vue'
 import Toolbar from './Toolbar/index.vue'
 import Remark from './Remark/index.vue'
@@ -411,7 +411,7 @@ usePasteEvent()
 }
 
 .layout-content-center .center-top {
-  height: 40px;
+  height: 60px;
 }
 
 .layout-content-right {

+ 31 - 1
src/views/lang/cn.json

@@ -677,5 +677,35 @@
   "ssHtml2canvasFailed": "html2canvas失败,尝试html-to-image:",
   "ssHtmlToImageFailed": "to-image也失败了,使用canvas绘制方案:",
   "ssScreenshotSubmitFailed": "截图提交失败:",
-  "ssFailedGetIframeBodyElement": "获取iframe内部body元素失败,无法截图"
+  "ssFailedGetIframeBodyElement": "获取iframe内部body元素失败,无法截图",
+  "ssUploadFromLocal": "自本地上传",
+  "ssSearchFromWeb": "自网页搜索",
+  "ssGenerateFromAI": "自AI生成",
+  "ssUnknownTool": "未知工具",
+  "ssPage": "页面",
+  "ssInteractiveWebpage": "交互网页",
+  "ssMultimedia": "多媒体",
+  "ssAddTemplatePage": "添加模版页面",
+  "ssTitlePage": "标题页",
+  "ssImagePage": "图片页",
+  "ssContentPage": "内容页",
+  "ssTextImagePage": "文图页",
+  "ssImageTextPage": "图文页",
+  "ssUploadPPT": "上传PPT",
+  "ssAddInteractiveTool": "添加互动工具",
+  "ssSelectToolCreateInteractive": "选择工具创建互动画面",
+  "ssAddAIApp": "添加AI应用",
+  "ssAppCenter": "应用中心",
+  "ssCreateApp": "创建应用",
+  "ssAddInteractiveWebpage": "添加交互网页",
+  "ssWebpageCenter": "网页中心",
+  "ssUploadWebpage": "上传网页",
+  "ssCrawlWebpage": "爬取网页",
+  "ssAddMultimedia": "添加多媒体",
+  "ssDocument": "文档",
+  "ssDocumentSet": "文档集",
+  "ssFileParseCancelled": "文件解析已取消",
+  "ssFileParseFailed": "文件解析失败",
+  "ssFileParseFailedRetry": "文件解析失败,请重试",
+  "ssParseCancelled": "解析已取消"
 }

+ 31 - 1
src/views/lang/en.json

@@ -677,5 +677,35 @@
   "ssHtml2canvasFailed": "html2canvas failed, trying html-to-image:",
   "ssHtmlToImageFailed": "html-to-image also failed, using canvas drawing scheme:",
   "ssScreenshotSubmitFailed": "Screenshot submission failed:",
-  "ssFailedGetIframeBodyElement": "Failed to get iframe internal body element, cannot screenshot"
+  "ssFailedGetIframeBodyElement": "Failed to get iframe internal body element, cannot screenshot",
+  "ssUploadFromLocal": "Upload from Local",
+  "ssSearchFromWeb": "Search from Web",
+  "ssGenerateFromAI": "Generate from AI",
+  "ssUnknownTool": "Unknown Tool",
+  "ssPage": "Page",
+  "ssInteractiveWebpage": "Interactive Webpage",
+  "ssMultimedia": "Multimedia",
+  "ssAddTemplatePage": "Add Template Page",
+  "ssTitlePage": "Title Page",
+  "ssImagePage": "Image Page",
+  "ssContentPage": "Content Page",
+  "ssTextImagePage": "Text Image Page",
+  "ssImageTextPage": "Image Text Page",
+  "ssUploadPPT": "Upload PPT",
+  "ssAddInteractiveTool": "Add Interactive Tool",
+  "ssSelectToolCreateInteractive": "Select tool to create interactive screen",
+  "ssAddAIApp": "Add AI Application",
+  "ssAppCenter": "App Center",
+  "ssCreateApp": "Create App",
+  "ssAddInteractiveWebpage": "Add Interactive Webpage",
+  "ssWebpageCenter": "Webpage Center",
+  "ssUploadWebpage": "Upload Webpage",
+  "ssCrawlWebpage": "Crawl Webpage",
+  "ssAddMultimedia": "Add Multimedia",
+  "ssDocument": "Document",
+  "ssDocumentSet": "Document Set",
+  "ssFileParseCancelled": "File parsing cancelled",
+  "ssFileParseFailed": "File parsing failed",
+  "ssFileParseFailedRetry": "File parsing failed, please retry",
+  "ssParseCancelled": "Parsing cancelled"
 }

+ 31 - 1
src/views/lang/hk.json

@@ -677,5 +677,35 @@
   "ssHtml2canvasFailed": "html2canvas失敗,嘗試html-to-image:",
   "ssHtmlToImageFailed": "to-image也失敗了,使用canvas繪製方案:",
   "ssScreenshotSubmitFailed": "截圖提交失敗:",
-  "ssFailedGetIframeBodyElement": "獲取iframe內部body元素失敗,無法截圖"
+  "ssFailedGetIframeBodyElement": "獲取iframe內部body元素失敗,無法截圖",
+  "ssUploadFromLocal": "自本地上傳",
+  "ssSearchFromWeb": "自網頁搜索",
+  "ssGenerateFromAI": "自AI生成",
+  "ssUnknownTool": "未知工具",
+  "ssPage": "頁面",
+  "ssInteractiveWebpage": "交互網頁",
+  "ssMultimedia": "多媒體",
+  "ssAddTemplatePage": "添加模版頁面",
+  "ssTitlePage": "標題頁",
+  "ssImagePage": "圖片頁",
+  "ssContentPage": "內容頁",
+  "ssTextImagePage": "文圖頁",
+  "ssImageTextPage": "圖文頁",
+  "ssUploadPPT": "上傳PPT",
+  "ssAddInteractiveTool": "添加互動工具",
+  "ssSelectToolCreateInteractive": "選擇工具創建互動畫面",
+  "ssAddAIApp": "添加AI應用",
+  "ssAppCenter": "應用中心",
+  "ssCreateApp": "創建應用",
+  "ssAddInteractiveWebpage": "添加交互網頁",
+  "ssWebpageCenter": "網頁中心",
+  "ssUploadWebpage": "上傳網頁",
+  "ssCrawlWebpage": "爬取網頁",
+  "ssAddMultimedia": "添加多媒體",
+  "ssDocument": "文檔",
+  "ssDocumentSet": "文檔集",
+  "ssFileParseCancelled": "文件解析已取消",
+  "ssFileParseFailed": "文件解析失敗",
+  "ssFileParseFailedRetry": "文件解析失敗,請重試",
+  "ssParseCancelled": "解析已取消"
 }