Переглянути джерело

图片预览监听元素变化

dusenyao 11 місяців тому
батько
коміт
f6e857b119

+ 77 - 73
package-lock.json

@@ -12,7 +12,7 @@
         "@tinymce/tinymce-vue": "^3.2.8",
         "axios": "^1.6.8",
         "core-js": "^3.37.0",
-        "dompurify": "^3.1.0",
+        "dompurify": "^3.1.2",
         "element-ui": "^2.15.14",
         "js-cookie": "^3.0.5",
         "md5": "^2.3.0",
@@ -24,8 +24,8 @@
         "vuex": "^3.6.2"
       },
       "devDependencies": {
-        "@babel/core": "^7.24.4",
-        "@babel/eslint-parser": "^7.24.1",
+        "@babel/core": "^7.24.5",
+        "@babel/eslint-parser": "^7.24.5",
         "@electron/fuses": "^1.8.0",
         "@rushstack/eslint-patch": "^1.10.2",
         "@types/md5": "^2.3.5",
@@ -35,7 +35,7 @@
         "@vue/eslint-config-prettier": "^9.0.0",
         "@vue/preload-webpack-plugin": "^2.0.0",
         "compression-webpack-plugin": "^6.1.2",
-        "electron": "^29.3.1",
+        "electron": "^29.3.2",
         "electron-builder": "^24.13.3",
         "eslint": "^8.57.0",
         "eslint-plugin-prettier": "^5.1.3",
@@ -44,7 +44,7 @@
         "patch-package": "^8.0.0",
         "postcss-html": "^1.6.0",
         "prettier": "^3.2.5",
-        "sass": "^1.75.0",
+        "sass": "^1.76.0",
         "sass-loader": "^14.2.1",
         "stylelint": "^15.11.0",
         "stylelint-config-recess-order": "^4.6.0",
@@ -117,21 +117,21 @@
       }
     },
     "node_modules/@babel/core": {
-      "version": "7.24.4",
-      "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.24.4.tgz",
-      "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.24.5.tgz",
+      "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==",
       "dev": true,
       "dependencies": {
         "@ampproject/remapping": "^2.2.0",
         "@babel/code-frame": "^7.24.2",
-        "@babel/generator": "^7.24.4",
+        "@babel/generator": "^7.24.5",
         "@babel/helper-compilation-targets": "^7.23.6",
-        "@babel/helper-module-transforms": "^7.23.3",
-        "@babel/helpers": "^7.24.4",
-        "@babel/parser": "^7.24.4",
+        "@babel/helper-module-transforms": "^7.24.5",
+        "@babel/helpers": "^7.24.5",
+        "@babel/parser": "^7.24.5",
         "@babel/template": "^7.24.0",
-        "@babel/traverse": "^7.24.1",
-        "@babel/types": "^7.24.0",
+        "@babel/traverse": "^7.24.5",
+        "@babel/types": "^7.24.5",
         "convert-source-map": "^2.0.0",
         "debug": "^4.1.0",
         "gensync": "^1.0.0-beta.2",
@@ -147,9 +147,9 @@
       }
     },
     "node_modules/@babel/eslint-parser": {
-      "version": "7.24.1",
-      "resolved": "https://registry.npmmirror.com/@babel/eslint-parser/-/eslint-parser-7.24.1.tgz",
-      "integrity": "sha512-d5guuzMlPeDfZIbpQ8+g1NaCNuAGBBGNECh0HVqz1sjOeVLh2CEaifuOysCH18URW6R7pqXINvf5PaR/dC6jLQ==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmmirror.com/@babel/eslint-parser/-/eslint-parser-7.24.5.tgz",
+      "integrity": "sha512-gsUcqS/fPlgAw1kOtpss7uhY6E9SFFANQ6EFX5GTvzUwaV0+sGaZWk6xq22MOdeT9wfxyokW3ceCUvOiRtZciQ==",
       "dev": true,
       "dependencies": {
         "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1",
@@ -161,16 +161,16 @@
       },
       "peerDependencies": {
         "@babel/core": "^7.11.0",
-        "eslint": "^7.5.0 || ^8.0.0"
+        "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0"
       }
     },
     "node_modules/@babel/generator": {
-      "version": "7.24.4",
-      "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.24.4.tgz",
-      "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.24.5.tgz",
+      "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==",
       "dev": true,
       "dependencies": {
-        "@babel/types": "^7.24.0",
+        "@babel/types": "^7.24.5",
         "@jridgewell/gen-mapping": "^0.3.5",
         "@jridgewell/trace-mapping": "^0.3.25",
         "jsesc": "^2.5.1"
@@ -322,28 +322,28 @@
       }
     },
     "node_modules/@babel/helper-module-imports": {
-      "version": "7.22.15",
-      "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz",
-      "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==",
+      "version": "7.24.3",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz",
+      "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==",
       "dev": true,
       "dependencies": {
-        "@babel/types": "^7.22.15"
+        "@babel/types": "^7.24.0"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-module-transforms": {
-      "version": "7.23.3",
-      "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz",
-      "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz",
+      "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==",
       "dev": true,
       "dependencies": {
         "@babel/helper-environment-visitor": "^7.22.20",
-        "@babel/helper-module-imports": "^7.22.15",
-        "@babel/helper-simple-access": "^7.22.5",
-        "@babel/helper-split-export-declaration": "^7.22.6",
-        "@babel/helper-validator-identifier": "^7.22.20"
+        "@babel/helper-module-imports": "^7.24.3",
+        "@babel/helper-simple-access": "^7.24.5",
+        "@babel/helper-split-export-declaration": "^7.24.5",
+        "@babel/helper-validator-identifier": "^7.24.5"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -408,12 +408,12 @@
       }
     },
     "node_modules/@babel/helper-simple-access": {
-      "version": "7.22.5",
-      "resolved": "https://registry.npmmirror.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz",
-      "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz",
+      "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==",
       "dev": true,
       "dependencies": {
-        "@babel/types": "^7.22.5"
+        "@babel/types": "^7.24.5"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -432,30 +432,30 @@
       }
     },
     "node_modules/@babel/helper-split-export-declaration": {
-      "version": "7.22.6",
-      "resolved": "https://registry.npmmirror.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
-      "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz",
+      "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==",
       "dev": true,
       "dependencies": {
-        "@babel/types": "^7.22.5"
+        "@babel/types": "^7.24.5"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-string-parser": {
-      "version": "7.23.4",
-      "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
-      "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz",
+      "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==",
       "dev": true,
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-validator-identifier": {
-      "version": "7.22.20",
-      "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
-      "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz",
+      "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==",
       "dev": true,
       "engines": {
         "node": ">=6.9.0"
@@ -485,14 +485,14 @@
       }
     },
     "node_modules/@babel/helpers": {
-      "version": "7.24.4",
-      "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.24.4.tgz",
-      "integrity": "sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.24.5.tgz",
+      "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==",
       "dev": true,
       "dependencies": {
         "@babel/template": "^7.24.0",
-        "@babel/traverse": "^7.24.1",
-        "@babel/types": "^7.24.0"
+        "@babel/traverse": "^7.24.5",
+        "@babel/types": "^7.24.5"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -514,9 +514,9 @@
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.24.4",
-      "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.24.4.tgz",
-      "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.24.5.tgz",
+      "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==",
       "bin": {
         "parser": "bin/babel-parser.js"
       },
@@ -1816,19 +1816,19 @@
       }
     },
     "node_modules/@babel/traverse": {
-      "version": "7.24.1",
-      "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.24.1.tgz",
-      "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.24.5.tgz",
+      "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==",
       "dev": true,
       "dependencies": {
-        "@babel/code-frame": "^7.24.1",
-        "@babel/generator": "^7.24.1",
+        "@babel/code-frame": "^7.24.2",
+        "@babel/generator": "^7.24.5",
         "@babel/helper-environment-visitor": "^7.22.20",
         "@babel/helper-function-name": "^7.23.0",
         "@babel/helper-hoist-variables": "^7.22.5",
-        "@babel/helper-split-export-declaration": "^7.22.6",
-        "@babel/parser": "^7.24.1",
-        "@babel/types": "^7.24.0",
+        "@babel/helper-split-export-declaration": "^7.24.5",
+        "@babel/parser": "^7.24.5",
+        "@babel/types": "^7.24.5",
         "debug": "^4.3.1",
         "globals": "^11.1.0"
       },
@@ -1837,13 +1837,13 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.24.0.tgz",
-      "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.24.5.tgz",
+      "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==",
       "dev": true,
       "dependencies": {
-        "@babel/helper-string-parser": "^7.23.4",
-        "@babel/helper-validator-identifier": "^7.22.20",
+        "@babel/helper-string-parser": "^7.24.1",
+        "@babel/helper-validator-identifier": "^7.24.5",
         "to-fast-properties": "^2.0.0"
       },
       "engines": {
@@ -7144,9 +7144,9 @@
       }
     },
     "node_modules/dompurify": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.1.0.tgz",
-      "integrity": "sha512-yoU4rhgPKCo+p5UrWWWNKiIq+ToGqmVVhk0PmMYBK4kRsR3/qhemNFL8f6CFmBd4gMwm3F4T7HBoydP5uY07fA=="
+      "version": "3.1.2",
+      "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.1.2.tgz",
+      "integrity": "sha512-hLGGBI1tw5N8qTELr3blKjAML/LY4ANxksbS612UiJyDfyf/2D092Pvm+S7pmeTGJRqvlJkFzBoHBQKgQlOQVg=="
     },
     "node_modules/domready": {
       "version": "1.0.8",
@@ -7233,7 +7233,9 @@
       }
     },
     "node_modules/electron": {
-      "version": "29.3.1",
+      "version": "29.3.2",
+      "resolved": "https://registry.npmmirror.com/electron/-/electron-29.3.2.tgz",
+      "integrity": "sha512-pRWsaFtbd78kEHR0lg5aMhZDY6b5JykIEhZRtyTCaJpGFObB2NFIJDzR7mTia0cZp3pPq60n/SS4gRxvSM5WFQ==",
       "dev": true,
       "hasInstallScript": true,
       "dependencies": {
@@ -14317,7 +14319,9 @@
       }
     },
     "node_modules/sass": {
-      "version": "1.75.0",
+      "version": "1.76.0",
+      "resolved": "https://registry.npmmirror.com/sass/-/sass-1.76.0.tgz",
+      "integrity": "sha512-nc3LeqvF2FNW5xGF1zxZifdW3ffIz5aBb7I7tSvOoNu7z1RQ6pFt9MBuiPtjgaI62YWrM/txjWlOCFiGtf2xpw==",
       "dev": true,
       "dependencies": {
         "chokidar": ">=3.0.0 <4.0.0",

+ 5 - 5
package.json

@@ -18,7 +18,7 @@
     "@tinymce/tinymce-vue": "^3.2.8",
     "axios": "^1.6.8",
     "core-js": "^3.37.0",
-    "dompurify": "^3.1.0",
+    "dompurify": "^3.1.2",
     "element-ui": "^2.15.14",
     "js-cookie": "^3.0.5",
     "md5": "^2.3.0",
@@ -30,8 +30,8 @@
     "vuex": "^3.6.2"
   },
   "devDependencies": {
-    "@babel/core": "^7.24.4",
-    "@babel/eslint-parser": "^7.24.1",
+    "@babel/core": "^7.24.5",
+    "@babel/eslint-parser": "^7.24.5",
     "@electron/fuses": "^1.8.0",
     "@rushstack/eslint-patch": "^1.10.2",
     "@types/md5": "^2.3.5",
@@ -41,7 +41,7 @@
     "@vue/eslint-config-prettier": "^9.0.0",
     "@vue/preload-webpack-plugin": "^2.0.0",
     "compression-webpack-plugin": "^6.1.2",
-    "electron": "^29.3.1",
+    "electron": "^29.3.2",
     "electron-builder": "^24.13.3",
     "eslint": "^8.57.0",
     "eslint-plugin-prettier": "^5.1.3",
@@ -50,7 +50,7 @@
     "patch-package": "^8.0.0",
     "postcss-html": "^1.6.0",
     "prettier": "^3.2.5",
-    "sass": "^1.75.0",
+    "sass": "^1.76.0",
     "sass-loader": "^14.2.1",
     "stylelint": "^15.11.0",
     "stylelint-config-recess-order": "^4.6.0",

+ 40 - 7
src/views/book/courseware/create/components/CreateCanvas.vue

@@ -291,21 +291,32 @@ export default {
      * @param {number} offsetY y 轴偏移量
      * @param {string} id 组件 id
      */
-    computedMoveData({ i, j, k, type, offsetX, offsetY, id }) {
+    computedMoveData({ i, j, k, type, offsetX, offsetY, id, min_width, min_height, row_width }) {
       const row = this.data.row_list[i];
       const col = row.col_list[j];
       const grid = col.grid_list[k];
 
+      // 上下移动
       if (['top', 'bottom'].includes(type)) {
+        // 高度为 auto 时
         if (grid.height === 'auto') {
           const gridHeight = document.querySelector(`.${id}`).offsetHeight;
+          const height = gridHeight + offsetY;
+          if (height < min_height) {
+            return;
+          }
           grid.height = `${gridHeight + offsetY}px`;
           col.grid_template_rows = `0 ${col.grid_list.map(({ height }) => height).join(' 16px ')} 0`;
           return;
         }
+        // 高度为数字时
         const height = Number(grid.height.replace('px', ''));
-        if (height + offsetY >= 50) {
-          grid.height = `${height + offsetY}px`;
+        const _h = height + offsetY;
+        if (_h < min_height) {
+          return;
+        }
+        if (_h >= 50) {
+          grid.height = `${_h}px`;
           col.grid_template_rows = `0 ${col.grid_list.map(({ height }) => height).join(' 16px ')} 0`;
         }
       }
@@ -314,6 +325,7 @@ export default {
       // 一行中有多个格子
       if (gridList.length > 1) {
         let find = gridList.findIndex((item) => item.id === id);
+        // 移动类型为 left 且不是第一个格子
         if (type === 'left' && find > 0) {
           const prevGrid = gridList[find - 1];
           const prevWidth = Number(prevGrid.width.replace('fr', ''));
@@ -325,10 +337,15 @@ export default {
           // 计算拖动的距离与总宽度的比例
           const ratio = (offsetX / document.querySelector('.row').offsetWidth) * 100;
 
+          const _w = width - ratio;
+          if ((_w / 100) * row_width < min_width) {
+            return;
+          }
           col.grid_list[k - 1].width = `${prevWidth + ratio}fr`;
-          grid.width = `${width - ratio}fr`;
+          grid.width = `${_w}fr`;
         }
 
+        // 移动类型为 right 且不是最后一个格子
         if (type === 'right' && find < gridList.length - 1) {
           let nextGrid = gridList[find + 1];
           const nextWidth = Number(nextGrid.width.replace('fr', ''));
@@ -338,13 +355,16 @@ export default {
             return;
           }
           const ratio = (offsetX / document.querySelector('.row').offsetWidth) * 100;
-
+          const _w = width + ratio;
+          if ((_w / 100) * row_width < min_width) {
+            return;
+          }
           col.grid_list[k + 1].width = `${nextWidth - ratio}fr`;
           grid.width = `${width + ratio}fr`;
         }
         return;
       }
-
+      // 移动类型为 left 且不是第一个格子
       if (type === 'left' && j > 0) {
         const prevGrid = row.width_list[j - 1];
         const prevWidth = Number(prevGrid.replace('fr', ''));
@@ -356,10 +376,15 @@ export default {
         // 计算拖动的距离与总宽度的比例
         const ratio = (offsetX / document.querySelector('.row').offsetWidth) * 100;
 
+        const _w = width - ratio;
+        if ((_w / 100) * row_width < min_width) {
+          return;
+        }
         row.width_list[j - 1] = `${prevWidth + ratio}fr`;
         row.width_list[j] = `${width - ratio}fr`;
       }
 
+      // 移动类型为 right 且不是最后一个格子
       if (type === 'right' && j < row.col_list.length - 1) {
         let nextGrid = row.width_list[j + 1];
         const nextWidth = Number(nextGrid.replace('fr', ''));
@@ -369,7 +394,10 @@ export default {
           return;
         }
         const ratio = (offsetX / document.querySelector('.row').offsetWidth) * 100;
-
+        const _w = width + ratio;
+        if ((_w / 100) * row_width < min_width) {
+          return;
+        }
         row.width_list[j + 1] = `${nextWidth - ratio}fr`;
         row.width_list[j] = `${width + ratio}fr`;
       }
@@ -803,6 +831,11 @@ export default {
 
       return { leftMarginDifference, topMarginDifference, isInsideCanvas };
     },
+
+    // 获取子组件
+    findChildComponentByKey(key) {
+      return this.$children.find((child) => child.$vnode.key === key);
+    },
   },
 };
 </script>

+ 15 - 2
src/views/book/courseware/create/components/PreviewEdit.vue

@@ -2,7 +2,7 @@
   <div class="preview">
     <template v-for="(row, i) in rowList">
       <!-- 行 -->
-      <div :key="i" class="row" :style="getMultipleColStyle(i)">
+      <div :key="i" :class="['row', `row-${i}`]" :style="getMultipleColStyle(i)">
         <!-- 列 -->
         <template v-for="(col, j) in row.col_list">
           <div :key="j" :class="['col', `col-${i}-${j}`]" :style="computedColStyle(col)">
@@ -28,7 +28,7 @@
                 :is="previewComponentList[grid.type]"
                 :id="grid.id"
                 ref="preview"
-                :key="k"
+                :key="`preview-${grid.id}`"
                 :courseware-id="coursewareId"
                 :class="[grid.id]"
                 :style="{
@@ -263,6 +263,12 @@ export default {
       const offsetX = clientX - startX;
       const offsetY = clientY - startY;
 
+      let el = this.findChildComponentByKey(`preview-${id}`);
+      let { min_height, min_width } = el.data;
+
+      // 获取行的宽度
+      const row_width = document.getElementsByClassName(`row-${i}`)[0].getBoundingClientRect().width;
+      console.log(min_width, min_height);
       this.$emit('computedMoveData', {
         i,
         j,
@@ -271,6 +277,9 @@ export default {
         offsetY,
         type,
         id,
+        min_width,
+        min_height,
+        row_width,
       });
 
       this.drag.startX = clientX;
@@ -292,6 +301,10 @@ export default {
       this.bgColor = '#ebebeb';
       document.body.style.cursor = 'auto';
     },
+    // 获取子组件
+    findChildComponentByKey(key) {
+      return this.$children.find((child) => child.$vnode.key === key);
+    },
   },
 };
 </script>

+ 0 - 1
src/views/book/courseware/create/components/base/common/CorrectPinyin.vue

@@ -45,7 +45,6 @@ export default {
         )
         .filter((item) => item.length > 0)
         .join(' ');
-      console.log(this.tonePinyin);
     },
     dialogClose() {
       this.$emit('update:visible', false);

+ 13 - 1
src/views/book/courseware/create/components/common/SelectUpload.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="select-upload">
+  <div class="select-upload" :style="{ width: width }">
     <span v-if="label" class="label-text">{{ label }}</span>
     <el-upload
       ref="upload"
@@ -35,6 +35,10 @@ export default {
       type: String,
       default: '',
     },
+    width: {
+      type: String,
+      default: '100%',
+    },
   },
   data() {
     return {
@@ -51,6 +55,10 @@ export default {
           label: '音频',
           accept: '.mp3,.acc,.wma',
         },
+        lrc: {
+          label: 'lrc',
+          accept: '.lrc',
+        },
       },
       fileList: [],
     };
@@ -102,6 +110,10 @@ export default {
   flex: 1;
   align-items: center;
 
+  :deep + & {
+    margin-top: 12px;
+  }
+
   .label-text {
     padding-right: 16px;
   }

+ 47 - 0
src/views/book/courseware/create/components/question/voice_matrix/VoiceMatrix.vue

@@ -0,0 +1,47 @@
+<template>
+  <ModuleBase :type="data.type">
+    <template #content>
+      <div v-for="(item, i) in data.option_list" :key="i" class="voice-matrix">
+        <div v-for="li in item" :key="li.mark"></div>
+      </div>
+      <SelectUpload label="矩阵音频" type="audio" width="500px" />
+      <SelectUpload label="lrc 文件" type="lrc" width="500px" />
+    </template>
+  </ModuleBase>
+</template>
+
+<script>
+import ModuleMixin from '../../common/ModuleMixin';
+import SelectUpload from '@/views/book/courseware/create/components/common/SelectUpload.vue';
+
+import { getVoiceMatrixData } from '@/views/book/courseware/data/voiceMatrix';
+
+export default {
+  name: 'VoiceMatrix',
+  components: {
+    SelectUpload,
+  },
+  mixins: [ModuleMixin],
+  data() {
+    return {
+      data: getVoiceMatrixData(),
+    };
+  },
+  watch: {
+    'data.property.row_count': {
+      handler(val) {},
+    },
+    'data.property.column_count': {
+      handler(val) {},
+    },
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped>
+.voice-matrix {
+  display: flex;
+  row-gap: 16px;
+}
+</style>

+ 72 - 0
src/views/book/courseware/create/components/question/voice_matrix/VoiceMatrixSetting.vue

@@ -0,0 +1,72 @@
+<template>
+  <div>
+    <el-form :model="property" label-width="72px">
+      <el-form-item label="序号" class="serial-number">
+        <el-input v-model="property.serial_number" />
+        <SvgIcon icon-class="switch" size="14" @click="switchSerialNumber(property)" />
+      </el-form-item>
+      <el-form-item>
+        <el-radio
+          v-for="{ value, label } in snGenerationMethodList"
+          :key="value"
+          v-model="property.sn_generation_method"
+          :label="value"
+        >
+          {{ label }}
+        </el-radio>
+      </el-form-item>
+      <el-form-item label="序号位置">
+        <SerialNumberPosition :position="property.sn_position" @changeNumberPosition="changeNumberPosition" />
+      </el-form-item>
+      <el-divider />
+      <el-form-item label="行数">
+        <el-input-number v-model="property.row_count" :min="1" />
+      </el-form-item>
+      <el-form-item label="列数">
+        <el-input-number v-model="property.column_count" :min="1" />
+      </el-form-item>
+      <el-form-item label="按列播放">
+        <el-radio-group v-model="property.is_enable_column_play">
+          <el-radio v-for="{ value, label } in switchOption" :key="value" :label="value">{{ label }}</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="按行播放">
+        <el-radio-group v-model="property.is_enable_row_play">
+          <el-radio v-for="{ value, label } in switchOption" :key="value" :label="value">{{ label }}</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-divider />
+      <el-form-item label="录音功能">
+        <el-radio-group v-model="property.is_enable_record">
+          <el-radio v-for="{ value, label } in switchOption" :key="value" :label="value">{{ label }}</el-radio>
+        </el-radio-group>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import SettingMixin from '@/views/book/courseware/create/components/common/SettingMixin';
+
+import { getVoiceMatrixProperty, switchOption } from '@/views/book/courseware/data/voiceMatrix';
+
+export default {
+  name: 'VoiceMatrixSetting',
+  mixins: [SettingMixin],
+  data() {
+    return {
+      property: getVoiceMatrixProperty(),
+      switchOption,
+    };
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.el-form {
+  @include setting-base;
+}
+</style>

+ 1 - 1
src/views/book/courseware/create/index.vue

@@ -63,7 +63,7 @@
 <script>
 import { bookTypeOption, componentSettingList } from '../data/bookType';
 
-import CreateCanvas from './components/createCanvas.vue';
+import CreateCanvas from './components/CreateCanvas.vue';
 import SelectBackground from './components/SelectBackground.vue';
 import WarnSave from './components/WarnSave.vue';
 

+ 10 - 0
src/views/book/courseware/data/bookType.js

@@ -20,6 +20,8 @@ import MatchingPage from '../create/components/question/matching/Matching.vue';
 import MatchingSetting from '../create/components/question/matching/MatchingSetting.vue';
 import SortPage from '../create/components/question/sort/Sort.vue';
 import SortSetting from '../create/components/question/sort/SortSetting.vue';
+import VoiceMatrix from '../create/components/question/voice_matrix/VoiceMatrix.vue';
+import VoiceMatrixSetting from '../create/components/question/voice_matrix/VoiceMatrixSetting.vue';
 
 import AudioPreview from '@/views/book/courseware/preview/components/audio/AudioPreview.vue';
 import DividerPreview from '@/views/book/courseware/preview/components/divider/DividerPreview.vue';
@@ -133,6 +135,14 @@ export const bookTypeOption = [
         set: SortSetting,
         preview: SortPreview,
       },
+      {
+        value: 'voice_matrix',
+        label: '语音矩阵',
+        icon: '',
+        component: VoiceMatrix,
+        set: VoiceMatrixSetting,
+        preview: '',
+      },
     ],
   },
 ];

+ 2 - 0
src/views/book/courseware/data/matching.js

@@ -59,6 +59,8 @@ export function getMatchingData() {
     answer: {
       answer_list,
     },
+    min_height: 120,
+    min_width: 290,
     property: getMatchingProperty(),
   };
 }

+ 2 - 0
src/views/book/courseware/data/select.js

@@ -39,6 +39,8 @@ export function getSelectData() {
     answer: {
       answer_list: [],
     },
+    min_height: 200,
+    min_width: 280,
     property: getSelectProperty(),
   };
 }

+ 2 - 0
src/views/book/courseware/data/sort.js

@@ -31,6 +31,8 @@ export function getSortData() {
     type: 'sort',
     title: '排序',
     option_list: [getOption(), getOption(), getOption()],
+    min_height: 80,
+    min_width: 280,
     property: getSortProperty(),
   };
 }

+ 40 - 0
src/views/book/courseware/data/voiceMatrix.js

@@ -0,0 +1,40 @@
+import {
+  snGenerationMethodList,
+  serialNumberTypeList,
+  serialNumberPositionList,
+  switchOption,
+} from '@/views/book/courseware/data/common';
+import { getRandomNumber } from '@/utils';
+export { switchOption };
+
+export function getVoiceMatrixProperty() {
+  return {
+    serial_number: 1,
+    sn_type: serialNumberTypeList[0].value,
+    sn_position: serialNumberPositionList[0].value,
+    sn_generation_method: snGenerationMethodList[0].value,
+    is_enable_row_play: switchOption[0].value, // 是否开启行播放
+    is_enable_column_play: switchOption[0].value, // 是否开启列播放
+    is_enable_record: switchOption[0].value, // 是否开启录音
+    row_count: 2,
+    column_count: 5,
+  };
+}
+
+export function getVoiceMatrixOption() {
+  return {
+    content: '',
+    mark: getRandomNumber(),
+  };
+}
+
+export function getVoiceMatrixData() {
+  return {
+    type: 'voice_matrix',
+    title: '语音矩阵',
+    option_list: Array.from({ length: 2 }, () => Array.from({ length: 5 }, getVoiceMatrixOption)),
+    min_height: 100,
+    min_width: 300,
+    property: getVoiceMatrixProperty(),
+  };
+}

+ 15 - 0
src/views/book/courseware/preview/components/picture/PicturePreview.vue

@@ -55,6 +55,7 @@ export default {
       elementHeight: 0,
       viewLeftRightBtn: false,
       fileLen: 0,
+      resizeObserver: null, // ResizeObserver 实例,用于监听元素大小变化
     };
   },
   watch: {
@@ -81,8 +82,22 @@ export default {
       this.elementHeight = newHeight;
     },
   },
+  mounted() {
+    this.$nextTick(() => {
+      this.resizeObserver = new ResizeObserver((entries) => {
+        for (let entry of entries) {
+          this.elementWidth = entry.contentRect.width;
+          this.elementHeight = entry.contentRect.height;
+        }
+      });
+      this.resizeObserver.observe(this.$el);
+    });
+  },
   beforeDestroy() {
     window.removeEventListener('resize', this.handleResize);
+    if (this.resizeObserver) {
+      this.resizeObserver.disconnect();
+    }
   },
   methods: {
     handleResize() {