lsc 1 неделя назад
Родитель
Сommit
36989f9627

+ 1 - 0
src/assets/styles/variable.scss

@@ -1,4 +1,5 @@
 $themeColor: #d14424;
+$themeColor2: #ec8c00;
 $themeHoverColor: #de6949;
 $textColor: #41464b;
 $borderColor: #e5e7eb;

+ 2 - 1
src/hooks/useGlobalHotkey.ts

@@ -92,8 +92,9 @@ export default () => {
   }
 
   const move = (key: string) => {
+    console.log('move', key)
     if (activeElementIdList.value.length) moveElement(key)
-    else if (key === KEYS.UP || key === KEYS.DOWN) updateSlideIndex(key)
+    else if (key === KEYS.UP || key === KEYS.DOWN || key === KEYS.LEFT || key === KEYS.RIGHT) updateSlideIndex(key)
   }
 
   const moveSlide = (key: string) => {

+ 44 - 26
src/hooks/useImport.ts

@@ -33,7 +33,7 @@ import type {
 
 const convertFontSizePtToPx = (html: string, ratio: number, autoFit: any) => {
   if (autoFit?.fontScale && autoFit?.type == 'text') {
-    ratio = ratio * autoFit.fontScale / 100 
+    ratio = ratio * autoFit.fontScale / 100
   }
   // return html;
   return html.replace(/\s*([\d.]+)pt/g, (match, p1) => {
@@ -742,16 +742,16 @@ export default () => {
     options?: { tolerance?: number }
   ): Promise<File> => {
     const tolerance = options?.tolerance ?? 15
-  
+
     // 1. 统一输入为 Blob 和 MIME
     const { blob, mime } = await getBlobAndMime(data)
     const format = getFormat(mime, filename)
-  
+
     // 2. 浏览器原生支持的格式直接返回
     if (format === 'browser') {
       return new File([blob], filename, { type: mime })
     }
-  
+
     // 3. 需要转换成 PNG 的格式
     let pngBlob: Blob
     if (format === 'tiff') {
@@ -767,7 +767,7 @@ export default () => {
       // format === 'png' 的情况
       pngBlob = blob
     }
-  
+
     // --- 新增:检测 PNG 是否已经包含透明背景 ---
     let alreadyTransparent = false
     // 无论原始格式是 PNG 还是转换后得到的 PNG,都进行检测
@@ -789,7 +789,7 @@ export default () => {
     finally {
       URL.revokeObjectURL(checkUrl)
     }
-  
+
     let transparentPngBlob: Blob
     transparentPngBlob = pngBlob
     /*
@@ -806,7 +806,7 @@ export default () => {
     const finalFilename = format === 'png' ? filename : filename.replace(/\.[^.]*$/, '') + '.png'
     return new File([transparentPngBlob], finalFilename, { type: 'image/png' })
   }
-  
+
 
   // ================== 辅助函数 ==================
 
@@ -854,21 +854,20 @@ export default () => {
   // 参考示例:https://github.com/wood/rtf.js/blob/master/demo/WMFJS.html
   async function convertMetafileToPng(
     arrayBuffer: ArrayBuffer,
-    RendererClass: any // new (data: ArrayBuffer) => { render(settings: any): SVGElement }
+    RendererClass: any
   ): Promise<Blob> {
     // 1. 创建 Renderer 实例
     const renderer = new RendererClass(arrayBuffer)
 
-    // 2. 先尝试获取图片的真实尺寸(通过临时渲染并解析 SVG 的 viewBox
-    let width = 800, height = 600 // 默认值
+    // 2. 获取真实尺寸(保持原逻辑
+    let width = 800, height = 600
     try {
-      // 使用一个较大的临时尺寸进行第一次渲染,以获取 SVG 的 viewBox
       const tempSettings = {
         width: '100%',
         height: '100%',
         xExt: 1000,
         yExt: 1000,
-        mapMode: 8, // 保持宽高比
+        mapMode: 8,
       }
       const tempSvg = renderer.render(tempSettings)
       const viewBox = tempSvg.getAttribute('viewBox')
@@ -878,9 +877,7 @@ export default () => {
           width = parseFloat(parts[2])
           height = parseFloat(parts[3])
         }
-      }
-      else {
-        // 尝试从 width/height 属性获取
+      } else {
         const svgWidth = tempSvg.getAttribute('width')
         const svgHeight = tempSvg.getAttribute('height')
         if (svgWidth && svgHeight) {
@@ -888,28 +885,46 @@ export default () => {
           height = parseFloat(svgHeight)
         }
       }
-    }
-    catch (e) {
+    } catch (e) {
       console.warn('Failed to get dimensions from SVG, using default', e)
     }
 
-    // 3. 使用实际尺寸重新渲染
+    // 3. 使用实际尺寸渲染最终 SVG
     const settings = {
       width: width + 'px',
       height: height + 'px',
       xExt: width,
       yExt: height,
-      mapMode: 8, // 保持宽高比
+      mapMode: 8,
     }
     const svg = renderer.render(settings)
 
-    // 4. 将 SVG 转为 data URL 并用 Image 加载
+    // 4. 序列化并修补 SVG 字符串
     const serializer = new XMLSerializer()
     let svgString = serializer.serializeToString(svg)
+
+    // ✅ 关键修复:强制设置根元素宽高与 viewBox 一致
+    const parser = new DOMParser()
+    const svgDoc = parser.parseFromString(svgString, 'image/svg+xml')
+    const svgRoot = svgDoc.documentElement
+
     // 确保有命名空间
-    if (!svgString.includes('xmlns="http://www.w3.org/2000/svg"')) {
-      svgString = svgString.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"')
+    if (!svgRoot.hasAttribute('xmlns')) {
+      svgRoot.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
+    }
+
+    // 强制设置宽高为确定的像素值(与 viewBox 的宽高一致)
+    svgRoot.setAttribute('width', width.toString())
+    svgRoot.setAttribute('height', height.toString())
+
+    // 若原本没有 viewBox,根据获取的宽高补上
+    if (!svgRoot.hasAttribute('viewBox')) {
+      svgRoot.setAttribute('viewBox', `0 0 ${width} ${height}`)
     }
+
+    svgString = new XMLSerializer().serializeToString(svgRoot)
+
+    // 5. 后续 Image 加载与 canvas 绘制保持不变
     const blob = new Blob([svgString], { type: 'image/svg+xml' })
     const url = URL.createObjectURL(blob)
     try {
@@ -919,17 +934,20 @@ export default () => {
         image.onerror = reject
         image.src = url
       })
-      // 5. 绘制到 canvas
+
       const canvas = document.createElement('canvas')
       canvas.width = img.width
       canvas.height = img.height
       const ctx = canvas.getContext('2d')!
       ctx.drawImage(img, 0, 0)
+
       return new Promise((resolve, reject) => {
-        canvas.toBlob(blob => (blob ? resolve(blob) : reject(new Error('Metafile to PNG failed'))), 'image/png')
+        canvas.toBlob(
+          (blob) => (blob ? resolve(blob) : reject(new Error('Metafile to PNG failed'))),
+          'image/png'
+        )
       })
-    }
-    finally {
+    } finally {
       URL.revokeObjectURL(url)
     }
   }

+ 3 - 2
src/hooks/useSlideHandler.ts

@@ -46,11 +46,12 @@ export default () => {
    * @param command 移动页面焦点命令:上移、下移
    */
   const updateSlideIndex = (command: string) => {
-    if (command === KEYS.UP && slideIndex.value > 0) {
+    console.log('updateSlideIndex', command)
+    if ((command === KEYS.UP || command === KEYS.LEFT) && slideIndex.value > 0) {
       if (activeElementIdList.value.length) mainStore.setActiveElementIdList([])
       slidesStore.updateSlideIndex(slideIndex.value - 1)
     }
-    else if (command === KEYS.DOWN && slideIndex.value < slides.value.length - 1) {
+    else if ((command === KEYS.DOWN || command === KEYS.RIGHT) && slideIndex.value < slides.value.length - 1) {
       if (activeElementIdList.value.length) mainStore.setActiveElementIdList([])
       slidesStore.updateSlideIndex(slideIndex.value + 1)
     }

+ 11 - 11
src/views/Editor/Thumbnails/index2.vue

@@ -435,26 +435,26 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
 
   .thumbnail {
     border-radius: $borderRadius;
-    outline: 2px solid rgba($color: $themeColor, $alpha: .15);
+    outline: 2px solid rgba($color: $themeColor2, $alpha: .15);
   }
 
   &.active {
     .label {
-      color: $themeColor;
+      color: $themeColor2;
     }
     .thumbnail {
-      outline-color: $themeColor;
+      outline-color: $themeColor2;
     }
   }
   &.selected {
     .thumbnail {
-      outline-color: $themeColor;
+      outline-color: $themeColor2;
     }
     .note-flag {
-      background-color: $themeColor;
+      background-color: $themeColor2;
 
       &::after {
-        border-top-color: $themeColor;
+        border-top-color: $themeColor2;
       }
     }
   }
@@ -467,7 +467,7 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
     left: 8px;
     top: 13px;
     font-size: 8px;
-    background-color: rgba($color: $themeColor, $alpha: .75);
+    background-color: rgba($color: $themeColor2, $alpha: .75);
     color: #fff;
     text-align: center;
     line-height: 12px;
@@ -481,7 +481,7 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
       top: 10px;
       left: 4px;
       border: 4px solid transparent;
-      border-top-color: rgba($color: $themeColor, $alpha: .75);
+      border-top-color: rgba($color: $themeColor2, $alpha: .75);
     }
   }
 }
@@ -519,11 +519,11 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
   color: #555;
 
   &.contextmenu-active {
-    color: $themeColor;
+    color: $themeColor2;
 
     .text::before {
-      border-bottom-color: $themeColor;
-      border-right-color: $themeColor;
+      border-bottom-color: $themeColor2;
+      border-right-color: $themeColor2;
     }
   }
 

+ 5 - 3
src/views/Student/index.vue

@@ -1700,10 +1700,11 @@ const elementDone = async (element: any, slideIndex: number) => {
           isHTML: false,
           url: iframeSrc
         }
-      } 
+      }
+      else {
       // 加载完成但无法获取contentWindow,也要移除iframe
-      document.body.removeChild(tempIframe)
-                    
+        document.body.removeChild(tempIframe)
+      }
     }
     catch (error) {
       console.log(`iframe ${iframeSrc} 无法获取contentWindow,使用HTML方式:`, error)
@@ -2457,6 +2458,7 @@ const getRefreshButtonRight = () => {
 
 // 键盘快捷键
 const handleKeydown = (e: KeyboardEvent) => {
+  console.log('键盘事件:', e.key)
   switch (e.key) {
     case 'ArrowLeft':
       e.preventDefault()

+ 9 - 3
src/views/components/element/FrameElement/BaseFrameElement.vue

@@ -22,7 +22,7 @@
       :style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
     >
       <div class="element-content">
-        <div class="fullscreen-spin mask" v-if="!elementInfo.isDone && !isThumbnail && isVisible">
+        <div class="fullscreen-spin mask" v-if="(!elementInfo.isDone && !isThumbnail && isVisible) || frameloading">
           <div class="spin">
             <div class="spinner"></div>
           </div>
@@ -170,10 +170,12 @@ const getTypeLabel = (type: number): string => {
   return (key ? lang[key] : lang.ssUnknown) as string
 }
 
+const frameloading = ref(false)
+
 // 处理iframe加载完成事件
 const handleIframeLoad = async (event: Event) => {
   const iframe = event.target as HTMLIFrameElement
-
+  frameloading.value = true
   try {
     // 等待iframe完全加载
     await nextTick()
@@ -217,6 +219,7 @@ const handleIframeLoad = async (event: Event) => {
               console.error(`获取 ${jsFile.id} 失败:`, fetchError)
             }
           }
+          frameloading.value = false
 
           // 可选:在iframe中执行一些初始化代码
           try {
@@ -226,16 +229,19 @@ const handleIframeLoad = async (event: Event) => {
               `)
           }
           catch (evalError) {
+            frameloading.value = false
             console.warn('无法在iframe中执行代码:', evalError)
           }
         }
       }
       else {
+        frameloading.value = false
         console.warn('无法访问iframe内容,可能是跨域限制')
       }
     }, 1000)
   }
   catch (error) {
+    frameloading.value = false
     console.error('注入JS到iframe失败:', error)
   }
 }
@@ -278,7 +284,7 @@ const handleIframeLoad = async (event: Event) => {
 .thumbnail-content-inner {
   width: 100%;
   height: 100%;
-  color: #3681fc;
+  color: #ec8c00;
   font-size: 110px;
   font-weight: 600;
   text-align: center;

+ 2 - 2
src/views/lang/hk.json

@@ -49,7 +49,7 @@
   "ssSectName": "輸入節名稱",
   "ssUntitledSec": "無標題節",
   "ssDefSec": "預設節",
-  "ssSlidePage": "頁碼為",
+  "ssSlidePage": "Slide",
   "ssStyle": "樣式",
   "ssSymbol": "符號",
   "ssPosition": "位置",
@@ -683,7 +683,7 @@
   "ssUnknownTool": "未知工具",
   "ssPage": "頁面",
   "ssInteractiveWebpage": "交互網頁",
-  "ssMultimedia": "多媒體",
+  "ssMultimedia": "Media",
   "ssAddTemplatePage": "添加模版頁面",
   "ssTitlePage": "標題頁",
   "ssImagePage": "圖片頁",