Browse Source

下载离线包优化

dsy 4 days ago
parent
commit
057fa7b768

+ 61 - 1
main.js

@@ -168,7 +168,67 @@ ipcMain.handle('compress-with-7z', async (evt, opts) => {
     if (!fs.existsSync(s)) throw new Error(`source not found: ${s}`);
   }
 
-  const sevenPath = sevenBin.path7za; // 7za 可执行路径
+  // 7za 可执行路径,优先使用 seven-bin 提供的路径;若被打包进 app.asar,则尝试 app.asar.unpacked 路径
+  let sevenPath = sevenBin.path7za;
+
+  try {
+    // 如果 sevenPath 在 app.asar 内,映射到 app.asar.unpacked 的对应相对路径
+    const asarToken = `${path.sep}app.asar${path.sep}`;
+    if (typeof sevenPath === 'string' && sevenPath.indexOf(asarToken) !== -1) {
+      const rel = sevenPath.split(asarToken)[1]; // 例如: node_modules/7zip-bin/win/x64/7za.exe
+      const unpackedCandidate = path.join(process.resourcesPath, 'app.asar.unpacked', rel);
+      if (fs.existsSync(unpackedCandidate)) {
+        sevenPath = unpackedCandidate;
+      }
+    }
+
+    // 若上面没有找到,尝试常见的 unpacked 路径(兼容不同包结构与架构)
+    if (!fs.existsSync(sevenPath)) {
+      const candidates = [
+        path.join(
+          process.resourcesPath,
+          'app.asar.unpacked',
+          'node_modules',
+          '7zip-bin',
+          'win',
+          'x64',
+          path.basename(sevenPath),
+        ),
+        path.join(
+          process.resourcesPath,
+          'app.asar.unpacked',
+          'node_modules',
+          '7zip-bin',
+          'win',
+          'x86',
+          path.basename(sevenPath),
+        ),
+        path.join(
+          process.resourcesPath,
+          'app.asar.unpacked',
+          'node_modules',
+          '7zip-bin',
+          'bin',
+          path.basename(sevenPath),
+        ),
+      ];
+      for (const c of candidates) {
+        if (fs.existsSync(c)) {
+          sevenPath = c;
+          break;
+        }
+      }
+    }
+  } catch (err) {
+    console.error('Error checking 7za path:', err);
+  }
+
+  if (!fs.existsSync(sevenPath)) {
+    throw new Error(
+      `7za executable not found: ${sevenPath}. If running from a packaged app, add "asarUnpack": ["node_modules/7zip-bin/**"] to build config so 7za is unpacked.`,
+    );
+  }
+
   const args = ['a', `-t${format}`, `-mx=${level}`, dest]; // 基本参数
   if (opts.password) {
     args.push(`-p${opts.password}`);

+ 3 - 0
package.json

@@ -85,6 +85,9 @@
     "appId": "com.gcls.page.textbook",
     "productName": "智慧梧桐数字教材编辑器",
     "copyright": "Copyright © 2025 ${author}",
+    "asarUnpack": [
+      "node_modules/7zip-bin/**"
+    ],
     "directories": {
       "output": "out"
     },

+ 5 - 0
src/styles/common.scss

@@ -30,6 +30,11 @@
   &:hover {
     color: $main-hover-color;
   }
+
+  &.disabled {
+    color: #ccc;
+    cursor: text;
+  }
 }
 
 .pointer {

+ 1 - 1
src/views/book/courseware/create/components/FullTextSettings.vue

@@ -82,7 +82,7 @@ export default {
     },
     settings: {
       type: Object,
-      required: true,
+      default: () => unified_attrib,
     },
     bookId: {
       type: String,

+ 0 - 1
src/views/book/courseware/preview/components/article/components/AudioRed.vue

@@ -111,7 +111,6 @@ export default {
 };
 </script>
 <style lang="scss" scoped>
-//@import url(); 引入公共css类
 .content-voices {
   display: flex;
   align-items: center;

+ 0 - 1
src/views/book/courseware/preview/components/article/components/Freewrite.vue

@@ -233,7 +233,6 @@ export default {
 };
 </script>
 <style lang="scss" scoped>
-//@import url(); 引入公共css类
 .freewrite {
   position: relative;
   width: 100%;

+ 74 - 2
src/views/personal_workbench/edit_task/edit/index.vue

@@ -31,8 +31,11 @@
               <el-option v-for="item in langList" :key="item.type" :label="item.name" :value="item.type" />
             </el-select>
           </span>
-
-          <span v-if="!type" class="link" @click="useTemplate">使用模板</span>
+          <template v-if="!type">
+            <span class="link" @click="copyFormat">复制格式</span>
+            <span :class="['link', { disabled: !format.isCopy }]" @click="pasteFormat">粘贴格式</span>
+            <span class="link" @click="useTemplate">使用模板</span>
+          </template>
           <span class="link" @click="showSetBackground">背景图</span>
           <span class="link" @click="saveCoursewareContent('quit')">退出编辑</span>
           <span class="link" @click="saveCoursewareContent">保存</span>
@@ -53,6 +56,7 @@ import CreatePage from '@/views/book/courseware/create/index.vue';
 import MenuPage from '@/views/personal_workbench/common/menu.vue';
 import UseTemplate from './UseTemplate.vue';
 
+import tinymce from 'tinymce';
 import * as OpenCC from 'opencc-js';
 import { GetBookCoursewareInfo, GetMyBookCoursewareTaskList } from '@/api/project';
 import { GetLanguageTypeList } from '@/api/book';
@@ -87,6 +91,15 @@ export default {
       chinese: 'zh-Hans',
       visibleTemplate: false,
       temporaryCoursewareID: '',
+      format: {
+        isCopy: false, // 是否已复制格式
+        font_size: 12, // 字体大小
+        font_family: 'Arial', // 字体
+        weight: 400, // 字体粗细
+        color: '#000000', // 文字颜色
+        text_decoration: 'none', // 装饰线样式
+        font_style: 'normal', // 字体样式
+      },
     };
   },
   created() {
@@ -105,6 +118,65 @@ export default {
     });
   },
   methods: {
+    // 复制格式
+    copyFormat() {
+      const selection = tinymce.activeEditor.selection.getNode();
+      let content = tinymce.activeEditor.selection.getContent({ format: 'text' });
+      if (!content || content.trim() === '') {
+        this.$message.warning('请先选中需要复制格式的文本');
+        return;
+      }
+      // 向下找找到第一个字的父节点,获取样式
+      let node = selection;
+
+      // 如果选中了文本节点,使用它的父元素;如果选中了元素,查找第一个非空文本子节点并使用它的父元素,
+      // 以保证获取到应用样式的元素而不是直接的文本节点,避免丢失样式
+      if (node && node.nodeType === 3) {
+        node = node.parentElement || node.parentNode;
+      } else if (node && node.nodeType === 1) {
+        const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, {
+          acceptNode(n) {
+            return n.nodeValue && n.nodeValue.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
+          },
+        });
+        const textNode = walker.nextNode();
+        if (textNode && textNode.parentElement) {
+          node = textNode.parentElement;
+        }
+      }
+      const style = window.getComputedStyle(node);
+
+      // 将字体大小转为 pt
+      const pxSize = style.fontSize ? parseFloat(style.fontSize) : 12;
+      // px -> pt (1pt = 1.3333px => pt = px * 0.75)
+      this.format.font_size = Math.round(pxSize * 0.75);
+      this.format.font_family = style.fontFamily || 'Arial';
+      this.format.weight = style.fontWeight ? parseInt(style.fontWeight) : 400;
+      this.format.color = style.color || '#000000';
+      this.format.text_decoration = style.textDecoration || 'none';
+      this.format.font_style = style.fontStyle || 'normal';
+      this.format.isCopy = true;
+    },
+    // 粘贴格式
+    pasteFormat() {
+      if (!this.format.isCopy) {
+        return;
+      }
+      const activeEditor = tinymce.activeEditor;
+      // 格式刷到tinymce 选中的文字上
+      activeEditor.formatter.register('customFormat', {
+        inline: 'span',
+        styles: {
+          'font-size': `${this.format.font_size}pt`,
+          'font-family': this.format.font_family,
+          'font-weight': this.format.weight,
+          color: this.format.color,
+          'text-decoration': this.format.text_decoration,
+          'font-style': this.format.font_style,
+        },
+      });
+      activeEditor.formatter.apply('customFormat');
+    },
     /**
      * 得到教材课件信息
      */

+ 3 - 1
src/views/project_manage/org/book/index.vue

@@ -165,7 +165,6 @@ export default {
           (newVal) => {
             if (newVal >= 100) {
               this.downloadWatcher();
-              this.downloadWatcher = null;
               this.startCompress();
             } else {
               // 显示下载进度,预留 10% 的压缩进度
@@ -200,6 +199,9 @@ export default {
      * 开始压缩
      */
     async startCompress() {
+      if (this.downloadWatcher) {
+        this.downloadWatcher = null;
+      }
       const sources = [`${this.tempDir}\\resource`, `${this.tempDir}\\courseware`]; // 要压缩的文件或文件夹路径数组
       this.file_info_list.forEach(({ dir_name, file_name }) => {
         const dirPath = dir_name.length > 0 ? `${this.tempDir}\\${dir_name}` : this.tempDir;