浏览代码

feat: 富文本添加

zhangjie 1 年之前
父节点
当前提交
a43b430e5b

+ 5 - 0
config/vite.config.base.ts

@@ -2,6 +2,7 @@ import { resolve } from 'path';
 import { defineConfig } from 'vite';
 import { defineConfig } from 'vite';
 import vue from '@vitejs/plugin-vue';
 import vue from '@vitejs/plugin-vue';
 import svgLoader from 'vite-svg-loader';
 import svgLoader from 'vite-svg-loader';
+import inject from '@rollup/plugin-inject';
 import configArcoStyleImportPlugin from './plugin/arcoStyleImport';
 import configArcoStyleImportPlugin from './plugin/arcoStyleImport';
 
 
 export default defineConfig({
 export default defineConfig({
@@ -9,6 +10,10 @@ export default defineConfig({
     vue(),
     vue(),
     svgLoader({ svgoConfig: {} }),
     svgLoader({ svgoConfig: {} }),
     configArcoStyleImportPlugin(),
     configArcoStyleImportPlugin(),
+    inject({
+      'window.Quill': ['@vueup/vue-quill', 'Quill'],
+      'Quill': ['@vueup/vue-quill', 'Quill'],
+    }),
   ],
   ],
   resolve: {
   resolve: {
     alias: [
     alias: [

+ 3 - 0
package.json

@@ -30,6 +30,7 @@
   },
   },
   "dependencies": {
   "dependencies": {
     "@arco-design/web-vue": "^2.44.7",
     "@arco-design/web-vue": "^2.44.7",
+    "@vueup/vue-quill": "^1.2.0",
     "@vueuse/core": "^9.3.0",
     "@vueuse/core": "^9.3.0",
     "axios": "^0.24.0",
     "axios": "^0.24.0",
     "crypto-js": "^4.2.0",
     "crypto-js": "^4.2.0",
@@ -41,6 +42,7 @@
     "pinia": "^2.0.23",
     "pinia": "^2.0.23",
     "pinia-plugin-persistedstate": "^3.2.1",
     "pinia-plugin-persistedstate": "^3.2.1",
     "query-string": "^8.0.3",
     "query-string": "^8.0.3",
+    "quill-image-resize-module": "^3.0.0",
     "vue": "^3.2.40",
     "vue": "^3.2.40",
     "vue-ls": "^4.2.0",
     "vue-ls": "^4.2.0",
     "vue-router": "^4.0.14"
     "vue-router": "^4.0.14"
@@ -49,6 +51,7 @@
     "@arco-plugins/vite-vue": "^1.4.5",
     "@arco-plugins/vite-vue": "^1.4.5",
     "@commitlint/cli": "^17.1.2",
     "@commitlint/cli": "^17.1.2",
     "@commitlint/config-conventional": "^17.1.0",
     "@commitlint/config-conventional": "^17.1.0",
+    "@rollup/plugin-inject": "^5.0.5",
     "@types/crypto-js": "^4.2.1",
     "@types/crypto-js": "^4.2.1",
     "@types/lodash": "^4.14.186",
     "@types/lodash": "^4.14.186",
     "@types/mockjs": "^1.0.7",
     "@types/mockjs": "^1.0.7",

+ 132 - 20
pnpm-lock.yaml

@@ -10,6 +10,7 @@ specifiers:
   '@arco-plugins/vite-vue': ^1.4.5
   '@arco-plugins/vite-vue': ^1.4.5
   '@commitlint/cli': ^17.1.2
   '@commitlint/cli': ^17.1.2
   '@commitlint/config-conventional': ^17.1.0
   '@commitlint/config-conventional': ^17.1.0
+  '@rollup/plugin-inject': ^5.0.5
   '@types/crypto-js': ^4.2.1
   '@types/crypto-js': ^4.2.1
   '@types/lodash': ^4.14.186
   '@types/lodash': ^4.14.186
   '@types/mockjs': ^1.0.7
   '@types/mockjs': ^1.0.7
@@ -20,6 +21,7 @@ specifiers:
   '@vitejs/plugin-vue': ^3.1.2
   '@vitejs/plugin-vue': ^3.1.2
   '@vitejs/plugin-vue-jsx': ^2.0.1
   '@vitejs/plugin-vue-jsx': ^2.0.1
   '@vue/babel-plugin-jsx': ^1.1.1
   '@vue/babel-plugin-jsx': ^1.1.1
+  '@vueup/vue-quill': ^1.2.0
   '@vueuse/core': ^9.3.0
   '@vueuse/core': ^9.3.0
   axios: ^0.24.0
   axios: ^0.24.0
   consola: ^2.15.3
   consola: ^2.15.3
@@ -46,6 +48,7 @@ specifiers:
   postcss-html: ^1.5.0
   postcss-html: ^1.5.0
   prettier: ^2.7.1
   prettier: ^2.7.1
   query-string: ^8.0.3
   query-string: ^8.0.3
+  quill-image-resize-module: ^3.0.0
   rollup: ^2.56.3
   rollup: ^2.56.3
   rollup-plugin-visualizer: ^5.8.2
   rollup-plugin-visualizer: ^5.8.2
   typescript: ^4.8.4
   typescript: ^4.8.4
@@ -62,6 +65,7 @@ specifiers:
 
 
 dependencies:
 dependencies:
   '@arco-design/web-vue': 2.55.1_vue@3.4.23
   '@arco-design/web-vue': 2.55.1_vue@3.4.23
+  '@vueup/vue-quill': 1.2.0_vue@3.4.23
   '@vueuse/core': 9.13.0_vue@3.4.23
   '@vueuse/core': 9.13.0_vue@3.4.23
   axios: 0.24.0
   axios: 0.24.0
   crypto-js: 4.2.0
   crypto-js: 4.2.0
@@ -73,6 +77,7 @@ dependencies:
   pinia: 2.1.7_45upmq37rakspwiy7aartj6jum
   pinia: 2.1.7_45upmq37rakspwiy7aartj6jum
   pinia-plugin-persistedstate: 3.2.1_pinia@2.1.7
   pinia-plugin-persistedstate: 3.2.1_pinia@2.1.7
   query-string: 8.2.0
   query-string: 8.2.0
+  quill-image-resize-module: 3.0.0
   vue: 3.4.23_typescript@4.9.5
   vue: 3.4.23_typescript@4.9.5
   vue-ls: 4.2.0
   vue-ls: 4.2.0
   vue-router: 4.3.2_vue@3.4.23
   vue-router: 4.3.2_vue@3.4.23
@@ -81,6 +86,7 @@ devDependencies:
   '@arco-plugins/vite-vue': 1.4.5
   '@arco-plugins/vite-vue': 1.4.5
   '@commitlint/cli': 17.8.1
   '@commitlint/cli': 17.8.1
   '@commitlint/config-conventional': 17.8.1
   '@commitlint/config-conventional': 17.8.1
+  '@rollup/plugin-inject': 5.0.5_rollup@2.79.1
   '@types/crypto-js': 4.2.2
   '@types/crypto-js': 4.2.2
   '@types/lodash': 4.17.0
   '@types/lodash': 4.17.0
   '@types/mockjs': 1.0.10
   '@types/mockjs': 1.0.10
@@ -787,6 +793,21 @@ packages:
       fastq: 1.17.1
       fastq: 1.17.1
     dev: true
     dev: true
 
 
+  /@rollup/plugin-inject/5.0.5_rollup@2.79.1:
+    resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==}
+    engines: {node: '>=14.0.0'}
+    peerDependencies:
+      rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
+    peerDependenciesMeta:
+      rollup:
+        optional: true
+    dependencies:
+      '@rollup/pluginutils': 5.1.0_rollup@2.79.1
+      estree-walker: 2.0.2
+      magic-string: 0.30.10
+      rollup: 2.79.1
+    dev: true
+
   /@rollup/pluginutils/4.2.1:
   /@rollup/pluginutils/4.2.1:
     resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
     resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
     engines: {node: '>= 8.0.0'}
     engines: {node: '>= 8.0.0'}
@@ -1333,6 +1354,16 @@ packages:
   /@vue/shared/3.4.23:
   /@vue/shared/3.4.23:
     resolution: {integrity: sha512-wBQ0gvf+SMwsCQOyusNw/GoXPV47WGd1xB5A1Pgzy0sQ3Bi5r5xm3n+92y3gCnB3MWqnRDdvfkRGxhKtbBRNgg==}
     resolution: {integrity: sha512-wBQ0gvf+SMwsCQOyusNw/GoXPV47WGd1xB5A1Pgzy0sQ3Bi5r5xm3n+92y3gCnB3MWqnRDdvfkRGxhKtbBRNgg==}
 
 
+  /@vueup/vue-quill/1.2.0_vue@3.4.23:
+    resolution: {integrity: sha512-kd5QPSHMDpycklojPXno2Kw2JSiKMYduKYQckTm1RJoVDA557MnyUXgcuuDpry4HY/Rny9nGNcK+m3AHk94wag==}
+    peerDependencies:
+      vue: ^3.2.41
+    dependencies:
+      quill: 1.3.7
+      quill-delta: 4.2.2
+      vue: 3.4.23_typescript@4.9.5
+    dev: false
+
   /@vueuse/core/9.13.0_vue@3.4.23:
   /@vueuse/core/9.13.0_vue@3.4.23:
     resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
     resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
     dependencies:
     dependencies:
@@ -1740,7 +1771,6 @@ packages:
       function-bind: 1.1.2
       function-bind: 1.1.2
       get-intrinsic: 1.2.4
       get-intrinsic: 1.2.4
       set-function-length: 1.2.2
       set-function-length: 1.2.2
-    dev: true
 
 
   /callsites/3.1.0:
   /callsites/3.1.0:
     resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
     resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
@@ -1871,6 +1901,11 @@ packages:
       mimic-response: 1.0.1
       mimic-response: 1.0.1
     dev: true
     dev: true
 
 
+  /clone/2.1.2:
+    resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
+    engines: {node: '>=0.8'}
+    dev: false
+
   /color-convert/1.9.3:
   /color-convert/1.9.3:
     resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
     resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
     dependencies:
     dependencies:
@@ -2287,6 +2322,18 @@ packages:
       strip-dirs: 2.1.0
       strip-dirs: 2.1.0
     dev: true
     dev: true
 
 
+  /deep-equal/1.1.2:
+    resolution: {integrity: sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      is-arguments: 1.1.1
+      is-date-object: 1.0.5
+      is-regex: 1.1.4
+      object-is: 1.1.6
+      object-keys: 1.1.1
+      regexp.prototype.flags: 1.5.2
+    dev: false
+
   /deep-is/0.1.4:
   /deep-is/0.1.4:
     resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
     resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
     dev: true
     dev: true
@@ -2298,7 +2345,6 @@ packages:
       es-define-property: 1.0.0
       es-define-property: 1.0.0
       es-errors: 1.3.0
       es-errors: 1.3.0
       gopd: 1.0.1
       gopd: 1.0.1
-    dev: true
 
 
   /define-lazy-prop/2.0.0:
   /define-lazy-prop/2.0.0:
     resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
     resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
@@ -2312,7 +2358,6 @@ packages:
       define-data-property: 1.1.4
       define-data-property: 1.1.4
       has-property-descriptors: 1.0.2
       has-property-descriptors: 1.0.2
       object-keys: 1.1.1
       object-keys: 1.1.1
-    dev: true
 
 
   /diff/4.0.2:
   /diff/4.0.2:
     resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
     resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
@@ -2546,12 +2591,10 @@ packages:
     engines: {node: '>= 0.4'}
     engines: {node: '>= 0.4'}
     dependencies:
     dependencies:
       get-intrinsic: 1.2.4
       get-intrinsic: 1.2.4
-    dev: true
 
 
   /es-errors/1.3.0:
   /es-errors/1.3.0:
     resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
     resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
     engines: {node: '>= 0.4'}
     engines: {node: '>= 0.4'}
-    dev: true
 
 
   /es-object-atoms/1.0.0:
   /es-object-atoms/1.0.0:
     resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==}
     resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==}
@@ -3314,6 +3357,10 @@ packages:
     engines: {node: '>=0.10.0'}
     engines: {node: '>=0.10.0'}
     dev: true
     dev: true
 
 
+  /eventemitter3/2.0.3:
+    resolution: {integrity: sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==}
+    dev: false
+
   /eventemitter3/5.0.1:
   /eventemitter3/5.0.1:
     resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
     resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
     dev: true
     dev: true
@@ -3422,10 +3469,22 @@ packages:
       sort-keys-length: 1.0.1
       sort-keys-length: 1.0.1
     dev: true
     dev: true
 
 
+  /extend/3.0.2:
+    resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
+    dev: false
+
   /fast-deep-equal/3.1.3:
   /fast-deep-equal/3.1.3:
     resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
     resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
     dev: true
     dev: true
 
 
+  /fast-diff/1.1.2:
+    resolution: {integrity: sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==}
+    dev: false
+
+  /fast-diff/1.2.0:
+    resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==}
+    dev: false
+
   /fast-diff/1.3.0:
   /fast-diff/1.3.0:
     resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
     resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
     dev: true
     dev: true
@@ -3647,7 +3706,6 @@ packages:
 
 
   /function-bind/1.1.2:
   /function-bind/1.1.2:
     resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
     resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
-    dev: true
 
 
   /function.prototype.name/1.1.6:
   /function.prototype.name/1.1.6:
     resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==}
     resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==}
@@ -3661,7 +3719,6 @@ packages:
 
 
   /functions-have-names/1.2.3:
   /functions-have-names/1.2.3:
     resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
     resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
-    dev: true
 
 
   /gensync/1.0.0-beta.2:
   /gensync/1.0.0-beta.2:
     resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
     resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
@@ -3682,7 +3739,6 @@ packages:
       has-proto: 1.0.3
       has-proto: 1.0.3
       has-symbols: 1.0.3
       has-symbols: 1.0.3
       hasown: 2.0.2
       hasown: 2.0.2
-    dev: true
 
 
   /get-proxy/2.1.0:
   /get-proxy/2.1.0:
     resolution: {integrity: sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==}
     resolution: {integrity: sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==}
@@ -3848,7 +3904,6 @@ packages:
     resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
     resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
     dependencies:
     dependencies:
       get-intrinsic: 1.2.4
       get-intrinsic: 1.2.4
-    dev: true
 
 
   /got/7.1.0:
   /got/7.1.0:
     resolution: {integrity: sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw==}
     resolution: {integrity: sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw==}
@@ -3935,12 +3990,10 @@ packages:
     resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
     resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
     dependencies:
     dependencies:
       es-define-property: 1.0.0
       es-define-property: 1.0.0
-    dev: true
 
 
   /has-proto/1.0.3:
   /has-proto/1.0.3:
     resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==}
     resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==}
     engines: {node: '>= 0.4'}
     engines: {node: '>= 0.4'}
-    dev: true
 
 
   /has-symbol-support-x/1.4.2:
   /has-symbol-support-x/1.4.2:
     resolution: {integrity: sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==}
     resolution: {integrity: sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==}
@@ -3949,7 +4002,6 @@ packages:
   /has-symbols/1.0.3:
   /has-symbols/1.0.3:
     resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
     resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
     engines: {node: '>= 0.4'}
     engines: {node: '>= 0.4'}
-    dev: true
 
 
   /has-to-string-tag-x/1.4.1:
   /has-to-string-tag-x/1.4.1:
     resolution: {integrity: sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==}
     resolution: {integrity: sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==}
@@ -3962,14 +4014,12 @@ packages:
     engines: {node: '>= 0.4'}
     engines: {node: '>= 0.4'}
     dependencies:
     dependencies:
       has-symbols: 1.0.3
       has-symbols: 1.0.3
-    dev: true
 
 
   /hasown/2.0.2:
   /hasown/2.0.2:
     resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
     resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
     engines: {node: '>= 0.4'}
     engines: {node: '>= 0.4'}
     dependencies:
     dependencies:
       function-bind: 1.1.2
       function-bind: 1.1.2
-    dev: true
 
 
   /he/1.2.0:
   /he/1.2.0:
     resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
     resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
@@ -4190,6 +4240,14 @@ packages:
       p-is-promise: 1.1.0
       p-is-promise: 1.1.0
     dev: true
     dev: true
 
 
+  /is-arguments/1.1.1:
+    resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      call-bind: 1.0.7
+      has-tostringtag: 1.0.2
+    dev: false
+
   /is-array-buffer/3.0.4:
   /is-array-buffer/3.0.4:
     resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==}
     resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==}
     engines: {node: '>= 0.4'}
     engines: {node: '>= 0.4'}
@@ -4256,7 +4314,6 @@ packages:
     engines: {node: '>= 0.4'}
     engines: {node: '>= 0.4'}
     dependencies:
     dependencies:
       has-tostringtag: 1.0.2
       has-tostringtag: 1.0.2
-    dev: true
 
 
   /is-docker/2.2.1:
   /is-docker/2.2.1:
     resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
     resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
@@ -4354,7 +4411,6 @@ packages:
     dependencies:
     dependencies:
       call-bind: 1.0.7
       call-bind: 1.0.7
       has-tostringtag: 1.0.2
       has-tostringtag: 1.0.2
-    dev: true
 
 
   /is-retry-allowed/1.2.0:
   /is-retry-allowed/1.2.0:
     resolution: {integrity: sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==}
     resolution: {integrity: sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==}
@@ -4683,6 +4739,14 @@ packages:
     resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
     resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
     dev: true
     dev: true
 
 
+  /lodash.clonedeep/4.5.0:
+    resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==}
+    dev: false
+
+  /lodash.isequal/4.5.0:
+    resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
+    dev: false
+
   /lodash.isfunction/3.0.9:
   /lodash.isfunction/3.0.9:
     resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==}
     resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==}
     dev: true
     dev: true
@@ -5108,10 +5172,17 @@ packages:
     resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==}
     resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==}
     dev: true
     dev: true
 
 
+  /object-is/1.1.6:
+    resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      call-bind: 1.0.7
+      define-properties: 1.2.1
+    dev: false
+
   /object-keys/1.1.1:
   /object-keys/1.1.1:
     resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
     resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
     engines: {node: '>= 0.4'}
     engines: {node: '>= 0.4'}
-    dev: true
 
 
   /object.assign/4.1.5:
   /object.assign/4.1.5:
     resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==}
     resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==}
@@ -5328,6 +5399,10 @@ packages:
     engines: {node: '>=6'}
     engines: {node: '>=6'}
     dev: true
     dev: true
 
 
+  /parchment/1.1.4:
+    resolution: {integrity: sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==}
+    dev: false
+
   /parent-module/1.0.1:
   /parent-module/1.0.1:
     resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
     resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
     engines: {node: '>=6'}
     engines: {node: '>=6'}
@@ -5621,6 +5696,46 @@ packages:
     engines: {node: '>=8'}
     engines: {node: '>=8'}
     dev: true
     dev: true
 
 
+  /quill-delta/3.6.3:
+    resolution: {integrity: sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==}
+    engines: {node: '>=0.10'}
+    dependencies:
+      deep-equal: 1.1.2
+      extend: 3.0.2
+      fast-diff: 1.1.2
+    dev: false
+
+  /quill-delta/4.2.2:
+    resolution: {integrity: sha512-qjbn82b/yJzOjstBgkhtBjN2TNK+ZHP/BgUQO+j6bRhWQQdmj2lH6hXG7+nwwLF41Xgn//7/83lxs9n2BkTtTg==}
+    dependencies:
+      fast-diff: 1.2.0
+      lodash.clonedeep: 4.5.0
+      lodash.isequal: 4.5.0
+    dev: false
+
+  /quill-image-resize-module/3.0.0:
+    resolution: {integrity: sha512-1TZBnUxU/WIx5dPyVjQ9yN7C6mLZSp04HyWBEMqT320DIq4MW4JgzlOPDZX5ZpBM3bU6sacU4kTLUc8VgYQZYw==}
+    dependencies:
+      lodash: 4.17.21
+      quill: 1.3.7
+      raw-loader: 0.5.1
+    dev: false
+
+  /quill/1.3.7:
+    resolution: {integrity: sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==}
+    dependencies:
+      clone: 2.1.2
+      deep-equal: 1.1.2
+      eventemitter3: 2.0.3
+      extend: 3.0.2
+      parchment: 1.1.4
+      quill-delta: 3.6.3
+    dev: false
+
+  /raw-loader/0.5.1:
+    resolution: {integrity: sha512-sf7oGoLuaYAScB4VGr0tzetsYlS8EJH6qnTCfQ/WVEa89hALQ4RQfCKt5xCyPQKPDUbVUAIP1QsxAwfAjlDp7Q==}
+    dev: false
+
   /read-pkg-up/1.0.1:
   /read-pkg-up/1.0.1:
     resolution: {integrity: sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==}
     resolution: {integrity: sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==}
     engines: {node: '>=0.10.0'}
     engines: {node: '>=0.10.0'}
@@ -5709,7 +5824,6 @@ packages:
       define-properties: 1.2.1
       define-properties: 1.2.1
       es-errors: 1.3.0
       es-errors: 1.3.0
       set-function-name: 2.0.2
       set-function-name: 2.0.2
-    dev: true
 
 
   /repeating/2.0.1:
   /repeating/2.0.1:
     resolution: {integrity: sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==}
     resolution: {integrity: sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==}
@@ -5933,7 +6047,6 @@ packages:
       get-intrinsic: 1.2.4
       get-intrinsic: 1.2.4
       gopd: 1.0.1
       gopd: 1.0.1
       has-property-descriptors: 1.0.2
       has-property-descriptors: 1.0.2
-    dev: true
 
 
   /set-function-name/2.0.2:
   /set-function-name/2.0.2:
     resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
     resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
@@ -5943,7 +6056,6 @@ packages:
       es-errors: 1.3.0
       es-errors: 1.3.0
       functions-have-names: 1.2.3
       functions-have-names: 1.2.3
       has-property-descriptors: 1.0.2
       has-property-descriptors: 1.0.2
-    dev: true
 
 
   /shebang-command/1.2.0:
   /shebang-command/1.2.0:
     resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}
     resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}

+ 113 - 0
src/components/rich-editor/index.vue

@@ -0,0 +1,113 @@
+<template>
+  <div class="rich-editor">
+    <QuillEditor
+      ref="editorRef"
+      v-model:content="content"
+      theme="snow"
+      content-type="html"
+      :placeholder="placeholder"
+      :toolbar="toolbar"
+      @update:content="contentChange"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, watch } from 'vue';
+  import { QuillEditor } from '@vueup/vue-quill';
+  import '@vueup/vue-quill/dist/vue-quill.snow.css';
+  // import ImageResize from 'quill-image-resize-module';
+
+  import useParser from './useParser';
+  import useRender from './useRender';
+  import { RichTextJSON } from './types';
+
+  defineOptions({
+    name: 'RichEditor',
+  });
+
+  const props = withDefaults(
+    defineProps<{
+      modelValue: string | RichTextJSON | null;
+      placeholder?: string;
+      autoEmit?: boolean;
+    }>(),
+    {
+      placeholder: '请输入',
+      autoEmit: false,
+    }
+  );
+  const emit = defineEmits(['update:modelValue', 'change']);
+
+  const toolbar = [
+    [
+      'bold',
+      'italic',
+      'underline',
+      { script: 'sub' },
+      { script: 'super' },
+      'image',
+    ],
+    ['clean'],
+  ];
+
+  // const modules = {
+  //   name: 'imageResize',
+  //   module: ImageResize,
+  //   options: {
+  //     modules: ['Resize'],
+  //   },
+  // };
+
+  const editorRef = ref();
+  const content = ref<string>('');
+
+  const { parseRichText } = useParser();
+  const { renderRichText } = useRender();
+
+  function contentChange() {
+    if (!props.autoEmit) return;
+    const valJson = getValJson();
+    emit('update:modelValue', valJson);
+    emit('change', valJson);
+  }
+
+  function getValJson() {
+    const editorContainer = editorRef.value.getEditor();
+    return parseRichText(editorContainer.childNodes[0]);
+  }
+
+  watch(
+    () => props.modelValue,
+    (val) => {
+      let valJson = {} as RichTextJSON;
+      if (val === null || val === '') {
+        valJson = { sections: [] };
+      } else if (typeof val === 'string') {
+        try {
+          valJson = JSON.parse(val);
+        } catch (error) {
+          console.error(error);
+          valJson = { sections: [] };
+        }
+      } else {
+        valJson = val;
+      }
+      content.value = renderRichText(valJson);
+    },
+    {
+      immediate: true,
+    }
+  );
+
+  defineExpose({ getValJson });
+</script>
+
+<style>
+  .rich-editor {
+    height: 400px;
+  }
+  .rich-editor em {
+    font-style: italic;
+  }
+</style>

+ 37 - 0
src/components/rich-editor/types.ts

@@ -0,0 +1,37 @@
+export interface RichBaseBlock {
+  type: string;
+  value: string;
+}
+
+export interface RichTextBlock extends RichBaseBlock {
+  param?: {
+    underline?: boolean;
+    bold?: boolean;
+    italic?: boolean;
+    sup?: boolean;
+    sub?: boolean;
+  };
+}
+
+export interface RichImageBlock extends RichBaseBlock {
+  param?: {
+    width: number;
+    height: number;
+    latex?: string;
+  };
+}
+export interface RichAudioBlock extends RichBaseBlock {
+  param?: {
+    duration: number;
+  };
+}
+
+export type RichBlock = RichTextBlock | RichImageBlock | RichAudioBlock;
+
+export interface RichSection {
+  blocks: RichBlock[];
+}
+
+export interface RichTextJSON {
+  sections: RichSection[];
+}

+ 138 - 0
src/components/rich-editor/useParser.ts

@@ -0,0 +1,138 @@
+import { RichBlock, RichSection, RichTextJSON } from './types';
+
+export default function useParser() {
+  function parseRichText(editorContainer: HTMLDivElement) {
+    const newSections = [] as RichSection[];
+    const nodesSectionsCollector = parseNodeSections(editorContainer);
+    nodesSectionsCollector.forEach((section) => {
+      const newSection = { blocks: [] as RichBlock[] };
+      section.forEach((elem) => {
+        const newBlock = parseBlockJson(elem);
+        newSection.blocks.push(newBlock);
+      });
+
+      if (!newSection.blocks.length) {
+        // 空行特殊处理
+        newSection.blocks = [{ type: 'text', value: '' }];
+      }
+      newSections.push(newSection);
+    });
+
+    const result = { sections: newSections } as RichTextJSON;
+    return result;
+  }
+
+  function parseNodeSections(node: HTMLElement) {
+    const sections = [] as HTMLElement[][];
+    let curSection = [] as HTMLElement[];
+
+    const checkIsDiv = (elem: HTMLElement) => {
+      return elem.nodeType === Node.ELEMENT_NODE && elem.nodeName === 'P';
+    };
+
+    const parseNode = (nnode: HTMLElement) => {
+      nnode.childNodes.forEach((elem) => {
+        const nelem = elem as HTMLElement;
+        if (checkIsDiv(nelem) && curSection.length) {
+          sections.push(curSection);
+          curSection = [];
+        }
+
+        if (elem.childNodes && elem.childNodes.length) {
+          parseNode(nelem);
+        } else {
+          curSection.push(nelem);
+        }
+      });
+    };
+
+    parseNode(node);
+    if (curSection.length) {
+      sections.push(curSection);
+    }
+
+    return sections;
+  }
+
+  function checkAncestorElementTag(e: HTMLElement) {
+    let parentNode = e.parentNode as HTMLElement;
+    const elementTags = [];
+    const validTags = ['EM', 'STRONG', 'U', 'SUB', 'SUP'];
+    while (parentNode && !parentNode.className?.includes('ql-editor')) {
+      if (validTags.includes(parentNode.nodeName)) {
+        elementTags.push(parentNode.nodeName);
+      }
+      parentNode = parentNode.parentNode as HTMLElement;
+    }
+    return elementTags;
+  }
+
+  function parseBlockJson(e: HTMLElement) {
+    // 图片
+    if (e.nodeType === Node.ELEMENT_NODE && e.nodeName === 'IMG') {
+      return parseImageJson(e as HTMLImageElement);
+    }
+    // 文本
+    if (e.nodeType === Node.TEXT_NODE || e.nodeType === Node.ELEMENT_NODE) {
+      return parseTextJson(e);
+    }
+
+    // 空div和br直接返回空段落
+    const block = {} as RichBlock;
+    if (e.nodeType === Node.ELEMENT_NODE && ['BR', 'P'].includes(e.nodeName)) {
+      block.type = 'text';
+      block.value = '';
+    } else {
+      console.log('toJSONBlock: 非法', e);
+    }
+    return block;
+  }
+
+  function parseTextJson(e: HTMLElement) {
+    const block = {} as RichBlock;
+    if (e.nodeType === Node.TEXT_NODE) {
+      block.type = 'text';
+      block.value = e.textContent || '';
+      const elementTags = checkAncestorElementTag(e);
+      const param = {
+        italic: elementTags.includes('EM'),
+        bold: elementTags.includes('STRONG'),
+        underline: elementTags.includes('U'),
+        sup: elementTags.includes('SUP'),
+        sub: elementTags.includes('SUB'),
+      };
+      const allFalse = !Object.values(param).some((v) => v);
+      block.param = allFalse ? undefined : param;
+    }
+
+    if (e.nodeType === Node.ELEMENT_NODE) {
+      block.type = 'text';
+      block.value = e.textContent || '';
+      if (e.nodeName === 'U') {
+        block.param = { underline: true };
+      } else if (e.nodeName === 'B') {
+        block.param = { bold: true };
+      } else if (e.nodeName === 'I') {
+        block.param = { italic: true };
+      }
+    }
+
+    return block;
+  }
+
+  function parseImageJson(e: HTMLImageElement) {
+    const block = {} as RichBlock;
+
+    block.type = 'image';
+    block.value = e.src;
+    block.param = {
+      width: e.clientWidth,
+      height: e.clientHeight,
+    };
+    return block;
+  }
+
+  return {
+    parseRichText,
+  };
+}

+ 107 - 0
src/components/rich-editor/useRender.ts

@@ -0,0 +1,107 @@
+import {
+  RichBlock,
+  RichImageBlock,
+  RichSection,
+  RichTextBlock,
+  RichTextJSON,
+} from './types';
+
+export default function useRender() {
+  function renderRichText(body: RichTextJSON) {
+    const editorContainer = document.createElement('div');
+
+    const sections = body.sections || [];
+    const nodes = [] as HTMLParagraphElement[];
+    sections.forEach((section) => {
+      nodes.push(renderSection(section));
+    });
+
+    nodes.forEach((node) => {
+      editorContainer.appendChild(node);
+    });
+
+    return editorContainer.innerHTML;
+  }
+
+  function renderSection(section: RichSection) {
+    const blocks = section.blocks || [];
+    const inline = blocks.length > 1;
+    const node = document.createElement('p');
+    blocks.forEach((block) => {
+      const bnode = renderBlock(block, inline);
+      if (bnode) node.appendChild(bnode);
+    });
+    return node;
+  }
+
+  function renderBlock(block: RichBlock, inline: boolean): HTMLElement | null {
+    if (block.type === 'text') {
+      return renderText(block as RichTextBlock);
+    }
+
+    if (block.type === 'image') {
+      return renderImage(block as RichImageBlock, inline);
+    }
+
+    return null;
+  }
+
+  function renderText(block: RichTextBlock) {
+    let node = null;
+
+    if (block.param) {
+      const nodeParams = {
+        u: 'underline',
+        strong: 'bold',
+        em: 'italic',
+        sup: 'sup',
+        sub: 'sub',
+      };
+      const nodeNames = ['u', 'strong', 'em', 'sup', 'sub'] as Array<
+        keyof typeof nodeParams
+      >;
+
+      const nodes = nodeNames
+        .filter(
+          (nodeName) =>
+            block.param &&
+            block.param[nodeParams[nodeName] as keyof RichTextBlock['param']]
+        )
+        .map((nodeName) => {
+          return document.createElement(nodeName);
+        });
+      // @ts-ignore
+      nodes.push(document.createTextNode(block.value));
+
+      // 将不为空的元素依次append
+      node = nodes.reduceRight((p, c) => {
+        c.appendChild(p);
+        return c;
+      });
+    } else {
+      node = document.createTextNode(block.value);
+    }
+    return node as HTMLElement;
+  }
+
+  function renderImage(block: RichImageBlock, inline: boolean) {
+    const node = document.createElement('img');
+    if (inline) node.classList.add('inline');
+
+    node.src = block.value;
+    // node.dataset.isImage = true;
+    // param
+    if (block.param) {
+      // 公式latex表达式
+      if (block.param.latex) node.dataset.latex = block.param.latex;
+      if (block.param.width) node.style.width = `${block.param.width}px`;
+      if (block.param.height) node.style.height = `${block.param.height}px`;
+    }
+
+    return node;
+  }
+
+  return {
+    renderRichText,
+  };
+}

+ 19 - 10
src/views/order/task-manage/noticeForm.vue

@@ -1,14 +1,17 @@
 <template>
 <template>
   <div class="part-box" :style="{ minHeight: '300px' }">
   <div class="part-box" :style="{ minHeight: '300px' }">
-    <a-form ref="formRef" :model="formData" :rules="rules" auto-label-width>
-      <a-form-item field="notice" label="考试说明">
-        <a-textarea
+    <a-form
+      ref="formRef"
+      :model="formData"
+      :rules="rules"
+      auto-label-width
+      layout="vertical"
+    >
+      <a-form-item field="notice" label="考试说明" :content-flex="false">
+        <rich-editor
+          ref="richEditorRef"
           v-model="formData.notice"
           v-model="formData.notice"
           placeholder="请输入"
           placeholder="请输入"
-          :auto-size="{
-            minRows: 4,
-            maxRows: 10,
-          }"
           :max-length="999"
           :max-length="999"
         />
         />
       </a-form-item>
       </a-form-item>
@@ -29,9 +32,11 @@
   import { updateTaskNotice } from '@/api/order';
   import { updateTaskNotice } from '@/api/order';
   import useLoading from '@/hooks/loading';
   import useLoading from '@/hooks/loading';
   import type { FormInstance, FieldRule } from '@arco-design/web-vue/es/form';
   import type { FormInstance, FieldRule } from '@arco-design/web-vue/es/form';
-  import { objAssign, objModifyAssign } from '@/utils/utils';
+  import { objModifyAssign } from '@/utils/utils';
   import { TaskItemDetail } from '@/api/types/order';
   import { TaskItemDetail } from '@/api/types/order';
 
 
+  import RichEditor from '@/components/rich-editor/index.vue';
+
   defineOptions({
   defineOptions({
     name: 'NoticeForm',
     name: 'NoticeForm',
   });
   });
@@ -46,6 +51,7 @@
   };
   };
   type FormDataType = typeof defaultFormData;
   type FormDataType = typeof defaultFormData;
 
 
+  const richEditorRef = ref();
   const formRef = ref<FormInstance>();
   const formRef = ref<FormInstance>();
   const formData = reactive<FormDataType>({ ...defaultFormData });
   const formData = reactive<FormDataType>({ ...defaultFormData });
   const rules: Partial<Record<keyof FormDataType, FieldRule[]>> = {
   const rules: Partial<Record<keyof FormDataType, FieldRule[]>> = {
@@ -68,9 +74,12 @@
     if (err) return;
     if (err) return;
 
 
     setLoading(true);
     setLoading(true);
-    const datas = objAssign(formData, {});
     let res = true;
     let res = true;
-    await updateTaskNotice(datas).catch(() => {
+    formData.notice = richEditorRef.value.getValJson();
+    await updateTaskNotice({
+      id: formData.id,
+      notice: JSON.stringify(formData.notice),
+    }).catch(() => {
       res = false;
       res = false;
     });
     });
     setLoading(false);
     setLoading(false);

+ 16 - 6
tsconfig.json

@@ -10,11 +10,21 @@
     "esModuleInterop": true,
     "esModuleInterop": true,
     "baseUrl": ".",
     "baseUrl": ".",
     "paths": {
     "paths": {
-      "@/*": ["src/*"]
+      "@/*": [
+        "src/*"
+      ]
     },
     },
-    "lib": ["es2020", "dom"],
-    "skipLibCheck": true
+    "lib": [
+      "es2020",
+      "dom"
+    ],
+    "skipLibCheck": true,
   },
   },
-  "include": ["src/**/*", "src/**/*.vue"],
-  "exclude": ["node_modules"]
-}
+  "include": [
+    "src/**/*",
+    "src/**/*.vue"
+  ],
+  "exclude": [
+    "node_modules"
+  ]
+}