diff --git a/simple-mind-map/src/plugins/RichText.js b/simple-mind-map/src/plugins/RichText.js index 433d2a71..4f9c690a 100644 --- a/simple-mind-map/src/plugins/RichText.js +++ b/simple-mind-map/src/plugins/RichText.js @@ -820,6 +820,7 @@ class RichText { // 处理导入数据 handleSetData(data) { + if (!data) return // 短期处理,为了兼容老数据,长期会去除 const isOldRichTextVersion = !data.smmVersion || compareVersion(data.smmVersion, '0.13.0') === '<' diff --git a/simple-mind-map/src/utils/index.js b/simple-mind-map/src/utils/index.js index 94d42aab..b3524ad8 100644 --- a/simple-mind-map/src/utils/index.js +++ b/simple-mind-map/src/utils/index.js @@ -398,7 +398,7 @@ export const nextTick = function (fn, ctx) { } // 检查节点是否超出画布 -export const checkNodeOuter = (mindMap, node) => { +export const checkNodeOuter = (mindMap, node, offsetX = 0, offsetY = 0) => { let elRect = mindMap.elRect let { scaleX, scaleY, translateX, translateY } = mindMap.draw.transform() let { left, top, width, height } = node @@ -408,17 +408,17 @@ export const checkNodeOuter = (mindMap, node) => { top = top * scaleY + translateY let offsetLeft = 0 let offsetTop = 0 - if (left < 0) { - offsetLeft = -left + if (left < 0 + offsetX) { + offsetLeft = -left + offsetX } - if (right > elRect.width) { - offsetLeft = -(right - elRect.width) + if (right > elRect.width - offsetX) { + offsetLeft = -(right - elRect.width) - offsetX } - if (top < 0) { - offsetTop = -top + if (top < 0 + offsetY) { + offsetTop = -top + offsetY } - if (bottom > elRect.height) { - offsetTop = -(bottom - elRect.height) + if (bottom > elRect.height - offsetY) { + offsetTop = -(bottom - elRect.height) - offsetY } return { isOuter: offsetLeft !== 0 || offsetTop !== 0, @@ -508,7 +508,7 @@ export const loadImage = imgFile => { // 移除字符串中的html实体 export const removeHTMLEntities = str => { - ;[[' ', ' ']].forEach(item => { + [[' ', ' ']].forEach(item => { str = str.replace(new RegExp(item[0], 'g'), item[1]) }) return str @@ -1069,7 +1069,7 @@ export const generateColorByContent = str => { // html转义 export const htmlEscape = str => { - ;[ + [ ['&', '&'], ['<', '<'], ['>', '>'] diff --git a/web/package-lock.json b/web/package-lock.json index 0e5e0d2e..e84cb3c7 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -31,6 +31,7 @@ "esbuild": "^0.17.15", "eslint": "^6.7.2", "eslint-plugin-vue": "^6.2.2", + "express": "^4.21.2", "less": "^3.12.2", "less-loader": "^7.1.0", "markdown-it": "^13.0.1", @@ -4161,9 +4162,9 @@ "dev": true }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dev": true, "dependencies": { "bytes": "3.1.2", @@ -4174,7 +4175,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -4200,12 +4201,12 @@ "dev": true }, "node_modules/body-parser/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -5240,9 +5241,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, "engines": { "node": ">= 0.6" @@ -6623,9 +6624,9 @@ } }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "engines": { "node": ">= 0.8" @@ -7353,37 +7354,37 @@ "dev": true }, "node_modules/express": { - "version": "4.18.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz", - "integrity": "sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dev": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -7392,6 +7393,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/debug": { @@ -7410,12 +7415,12 @@ "dev": true }, "node_modules/express/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -7712,13 +7717,13 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -10253,10 +10258,13 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "dev": true + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-source-map": { "version": "1.1.0", @@ -11572,9 +11580,9 @@ "dev": true }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "dev": true }, "node_modules/path-type": { @@ -13295,9 +13303,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, "dependencies": { "debug": "2.6.9", @@ -13333,6 +13341,15 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -13427,15 +13444,15 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -20333,9 +20350,9 @@ "dev": true }, "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dev": true, "requires": { "bytes": "3.1.2", @@ -20346,7 +20363,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -20368,12 +20385,12 @@ "dev": true }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } } } @@ -21195,9 +21212,9 @@ "dev": true }, "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true }, "cookie-signature": { @@ -22285,9 +22302,9 @@ "dev": true }, "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true }, "end-of-stream": { @@ -22857,37 +22874,37 @@ } }, "express": { - "version": "4.18.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz", - "integrity": "sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dev": true, "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -22911,12 +22928,12 @@ "dev": true }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } }, "safe-buffer": { @@ -23145,13 +23162,13 @@ } }, "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, "requires": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -25081,9 +25098,9 @@ } }, "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "dev": true }, "merge-source-map": { @@ -26137,9 +26154,9 @@ "dev": true }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "dev": true }, "path-type": { @@ -27595,9 +27612,9 @@ "dev": true }, "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, "requires": { "debug": "2.6.9", @@ -27632,6 +27649,12 @@ } } }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -27718,15 +27741,15 @@ } }, "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, "requires": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" } }, "set-blocking": { diff --git a/web/package.json b/web/package.json index 7a4101e4..c145d173 100644 --- a/web/package.json +++ b/web/package.json @@ -8,7 +8,8 @@ "lint": "vue-cli-service lint", "buildLibrary": "node ./scripts/updateVersion.js && vue-cli-service build --mode library --target lib --name simpleMindMap ../simple-mind-map/full.js --dest ../simple-mind-map/dist && esbuild ../simple-mind-map/full.js --bundle --external:buffer --format=esm --outfile=../simple-mind-map/dist/simpleMindMap.esm.js && esbuild ../simple-mind-map/full.js --bundle --minify --external:buffer --format=esm --outfile=../simple-mind-map/dist/simpleMindMap.esm.min.js", "format": "prettier --write src/* src/*/* src/*/*/* src/*/*/*/*", - "createNodeImageList": "node ./scripts/createNodeImageList.js" + "createNodeImageList": "node ./scripts/createNodeImageList.js", + "ai:serve": "node ./scripts/ai.js" }, "dependencies": { "@toast-ui/editor": "^3.1.5", @@ -34,6 +35,7 @@ "esbuild": "^0.17.15", "eslint": "^6.7.2", "eslint-plugin-vue": "^6.2.2", + "express": "^4.21.2", "less": "^3.12.2", "less-loader": "^7.1.0", "markdown-it": "^13.0.1", diff --git a/web/scripts/ai.js b/web/scripts/ai.js new file mode 100644 index 00000000..67eb6f7e --- /dev/null +++ b/web/scripts/ai.js @@ -0,0 +1,108 @@ +const express = require('express') +const http = require('http') +const { pipeline } = require('stream') + +const port = 3456 + +// 起个服务 +const app = express() +app.use(express.json()) +app.use(express.urlencoded({ extended: true })) + +// 允许跨域 +app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*') // 允许所有来源的跨域请求,或者指定一个域名 + res.header('Access-Control-Allow-Methods', '*') // 允许的方法 + res.header('Access-Control-Allow-Headers', '*') // 允许的头部信息 + next() +}) + +// 监听对话请求 +app.get('/ai/test', (req, res) => { + res + .json({ + code: 0, + data: null, + msg: '连接成功' + }) + .end() +}) +app.post('/ai/chat', (req, res) => { + // 设置SSE响应头 + res.setHeader('Content-Type', 'text/event-stream') + res.setHeader('Cache-Control', 'no-cache') + res.setHeader('Connection', 'keep-alive') + + const { api, method, headers, data } = req.body + + // 创建代理请求 + const proxyReq = http.request( + api, + { + method: method || 'POST', + headers: { + ...headers + } + }, + proxyRes => { + // 检查目标服务响应状态 + if (proxyRes.statusCode !== 200) { + proxyRes.resume() + return res.status(proxyRes.statusCode).end() + } + + // 使用双向流管道 + const pipelinePromise = new Promise(resolve => { + pipeline(proxyRes, res, err => { + // 过滤客户端主动断开的情况 + if (err && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') { + console.error('Pipeline error:', err) + } + resolve() + }) + }) + + // 处理流结束 + proxyRes.on('end', () => { + if (!res.writableEnded) { + res.end() + } + }) + + return pipelinePromise + } + ) + + // 错误处理增强 + const handleError = err => { + if (!res.headersSent) { + res.status(502).end('Bad Gateway') + } + cleanupStreams() + } + + // 流清理函数 + const cleanupStreams = () => { + proxyReq.destroy() + res.destroy() + } + + // 事件监听器 + proxyReq.on('error', handleError) + res.on('error', handleError) + + // 处理客户端提前断开 + req.on('close', () => { + if (!res.writableFinished) { + console.log('Client disconnected prematurely') + cleanupStreams() + } + }) + + proxyReq.write(JSON.stringify(data)) + proxyReq.end() +}) + +app.listen(port, () => { + console.log(`app listening on port ${port}`) +}) diff --git a/web/src/assets/icon-font/iconfont.css b/web/src/assets/icon-font/iconfont.css index 2e741b11..71be3af5 100644 --- a/web/src/assets/icon-font/iconfont.css +++ b/web/src/assets/icon-font/iconfont.css @@ -1,8 +1,8 @@ @font-face { font-family: "iconfont"; /* Project id 2479351 */ - src: url('iconfont.woff2?t=1739152990179') format('woff2'), - url('iconfont.woff?t=1739152990179') format('woff'), - url('iconfont.ttf?t=1739152990179') format('truetype'); + src: url('iconfont.woff2?t=1739843331607') format('woff2'), + url('iconfont.woff?t=1739843331607') format('woff'), + url('iconfont.ttf?t=1739843331607') format('truetype'); } .iconfont { @@ -13,6 +13,10 @@ -moz-osx-font-smoothing: grayscale; } +.iconAIshengcheng:before { + content: "\e6b5"; +} + .iconprinting:before { content: "\ea28"; } diff --git a/web/src/assets/icon-font/iconfont.ttf b/web/src/assets/icon-font/iconfont.ttf index 06a6deea..f8154868 100644 Binary files a/web/src/assets/icon-font/iconfont.ttf and b/web/src/assets/icon-font/iconfont.ttf differ diff --git a/web/src/assets/icon-font/iconfont.woff b/web/src/assets/icon-font/iconfont.woff index b98983ec..c9ebb1e3 100644 Binary files a/web/src/assets/icon-font/iconfont.woff and b/web/src/assets/icon-font/iconfont.woff differ diff --git a/web/src/assets/icon-font/iconfont.woff2 b/web/src/assets/icon-font/iconfont.woff2 index 09818b06..16588279 100644 Binary files a/web/src/assets/icon-font/iconfont.woff2 and b/web/src/assets/icon-font/iconfont.woff2 differ diff --git a/web/src/config/en.js b/web/src/config/en.js index dae1310f..1f3a3755 100644 --- a/web/src/config/en.js +++ b/web/src/config/en.js @@ -444,6 +444,11 @@ export const sidebarTriggerList = [ value: 'setting', icon: 'iconshezhi' }, + { + name: 'AI', + value: 'ai', + icon: 'iconAIshengcheng' + }, { name: 'ShortcutKey', value: 'shortcutKey', diff --git a/web/src/config/zh.js b/web/src/config/zh.js index 75793ad9..3813a5e4 100644 --- a/web/src/config/zh.js +++ b/web/src/config/zh.js @@ -534,6 +534,11 @@ export const sidebarTriggerList = [ value: 'outline', icon: 'iconfuhao-dagangshu' }, + { + name: 'AI', + value: 'ai', + icon: 'iconAIshengcheng' + }, { name: '设置', value: 'setting', diff --git a/web/src/config/zhtw.js b/web/src/config/zhtw.js index 56dff3d1..106441f7 100644 --- a/web/src/config/zhtw.js +++ b/web/src/config/zhtw.js @@ -439,6 +439,11 @@ export const sidebarTriggerList = [ value: 'outline', icon: 'iconfuhao-dagangshu' }, + { + name: 'AI', + value: 'ai', + icon: 'iconAIshengcheng' + }, { name: '設置', value: 'setting', diff --git a/web/src/lang/en_us.js b/web/src/lang/en_us.js index fbabd0cf..6d856a22 100644 --- a/web/src/lang/en_us.js +++ b/web/src/lang/en_us.js @@ -134,7 +134,8 @@ export default { expandNodeChild: 'Expand all sub nodes', unExpandNodeChild: 'Un expand all sub nodes', addToDo: 'Add toDo', - removeToDo: 'Remove toDo' + removeToDo: 'Remove toDo', + aiCreate: 'AI Continuation' }, count: { words: 'Words', @@ -329,7 +330,8 @@ export default { newFileTip: 'Please export the currently edited file before creating a new one, Beware of content loss', openFileTip: - 'Please export the currently edited file before opening it, Beware of content loss' + 'Please export the currently edited file before opening it, Beware of content loss', + ai: 'AI' }, edit: { newFeatureNoticeTitle: 'New feature reminder', diff --git a/web/src/lang/zh_cn.js b/web/src/lang/zh_cn.js index d4b58234..9262cb13 100644 --- a/web/src/lang/zh_cn.js +++ b/web/src/lang/zh_cn.js @@ -133,7 +133,8 @@ export default { expandNodeChild: '展开所有下级节点', unExpandNodeChild: '收起所有下级节点', addToDo: '添加待办', - removeToDo: '删除待办' + removeToDo: '删除待办', + aiCreate: 'AI续写' }, count: { words: '字数', @@ -323,7 +324,8 @@ export default { creatingTip: '正在创建文件', directory: '目录', newFileTip: '新建文件前请先导出当前编辑的文件,谨防内容丢失', - openFileTip: '打开文件前请先导出当前编辑的文件,谨防内容丢失' + openFileTip: '打开文件前请先导出当前编辑的文件,谨防内容丢失', + ai: 'AI' }, edit: { newFeatureNoticeTitle: '新特性提醒', @@ -412,5 +414,8 @@ export default { nodeTagStyle: { placeholder: '请输入标签内容', delete: '删除此标签' + }, + ai: { + chatTitle: 'AI对话' } } diff --git a/web/src/lang/zh_tw.js b/web/src/lang/zh_tw.js index a22f76d8..2bb41eb2 100644 --- a/web/src/lang/zh_tw.js +++ b/web/src/lang/zh_tw.js @@ -134,7 +134,8 @@ export default { expandNodeChild: '展開所有下級節點', unExpandNodeChild: '收起所有下級節點', addToDo: '添加待辦', - removeToDo: '刪除待辦' + removeToDo: '刪除待辦', + aiCreate: 'AI續寫' }, count: { words: '字數', @@ -323,7 +324,8 @@ export default { creatingTip: '正在建立檔案', directory: '目錄', newFileTip: '新增檔案前,請先匯出目前編輯的檔案,以免內容遺失', - openFileTip: '開啟檔案前,請先匯出目前編輯的檔案,以免內容遺失' + openFileTip: '開啟檔案前,請先匯出目前編輯的檔案,以免內容遺失', + ai: 'AI' }, edit: { newFeatureNoticeTitle: '新功能提醒', diff --git a/web/src/main.js b/web/src/main.js index 22261c40..cbdb93bd 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -35,4 +35,3 @@ if (window.takeOverApp) { } else { initApp() } - diff --git a/web/src/pages/Edit/components/AiChat.vue b/web/src/pages/Edit/components/AiChat.vue new file mode 100644 index 00000000..f1ad934d --- /dev/null +++ b/web/src/pages/Edit/components/AiChat.vue @@ -0,0 +1,290 @@ + + + + + diff --git a/web/src/pages/Edit/components/AiConfigDialog.vue b/web/src/pages/Edit/components/AiConfigDialog.vue new file mode 100644 index 00000000..d9a53de6 --- /dev/null +++ b/web/src/pages/Edit/components/AiConfigDialog.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/web/src/pages/Edit/components/AiCreate.vue b/web/src/pages/Edit/components/AiCreate.vue new file mode 100644 index 00000000..c84054f4 --- /dev/null +++ b/web/src/pages/Edit/components/AiCreate.vue @@ -0,0 +1,546 @@ + + + + + diff --git a/web/src/pages/Edit/components/BaseStyle.vue b/web/src/pages/Edit/components/BaseStyle.vue index 59a107fc..7d3c72fe 100644 --- a/web/src/pages/Edit/components/BaseStyle.vue +++ b/web/src/pages/Edit/components/BaseStyle.vue @@ -995,8 +995,6 @@ export default { this.$bus.$off('setData', this.onSetData) }, methods: { - ...mapMutations(['setLocalConfig']), - onSetData() { if (this.activeSidebar !== 'baseStyle') return setTimeout(() => { diff --git a/web/src/pages/Edit/components/Contextmenu.vue b/web/src/pages/Edit/components/Contextmenu.vue index 02efa1eb..c5046e41 100644 --- a/web/src/pages/Edit/components/Contextmenu.vue +++ b/web/src/pages/Edit/components/Contextmenu.vue @@ -140,6 +140,10 @@
{{ $t('contextmenu.exportNodeToPng') }}
+
+
+ {{ $t('contextmenu.aiCreate') }} +
@@ -299,6 +310,11 @@ export default { // 设置标记 onSetAnnotation(...args) { this.$bus.$emit('execCommand', 'SET_NOTATION', this.activeNodes, ...args) + }, + + // AI生成整体 + aiCrate() { + this.$bus.$emit('ai_create_all') } } } @@ -376,6 +392,7 @@ export default { .text { margin-top: 3px; + text-align: center; } } diff --git a/web/src/store.js b/web/src/store.js index 3c99fedb..ff2e0f79 100644 --- a/web/src/store.js +++ b/web/src/store.js @@ -38,7 +38,14 @@ const store = new Vuex.Store({ supportCheckbox: false, // 是否支持Checkbox插件 supportLineFlow: false, // 是否支持LineFlow插件 supportMomentum: false, // 是否支持Momentum插件 - isDragOutlineTreeNode: false // 当前是否正在拖拽大纲树的节点 + isDragOutlineTreeNode: false, // 当前是否正在拖拽大纲树的节点 + aiConfig: { + api: 'http://ark.cn-beijing.volces.com/api/v3/chat/completions', + key: '', + model: '', + port: 3456, + method: 'POST' + } }, mutations: { // 设置思维导图数据 @@ -53,11 +60,18 @@ const store = new Vuex.Store({ // 设置本地配置 setLocalConfig(state, data) { - state.localConfig = { + const aiConfigKeys = Object.keys(state.aiConfig) + Object.keys(data).forEach(key => { + if (aiConfigKeys.includes(key)) { + state.aiConfig[key] = data[key] + } else { + state.localConfig[key] = data[key] + } + }) + storeLocalConfig({ ...state.localConfig, - ...data - } - storeLocalConfig(state.localConfig) + ...state.aiConfig + }) }, // 设置当前显示的侧边栏 diff --git a/web/src/utils/ai.js b/web/src/utils/ai.js new file mode 100644 index 00000000..2f3156ea --- /dev/null +++ b/web/src/utils/ai.js @@ -0,0 +1,120 @@ +class Ai { + constructor(options = {}) { + this.options = options + + this.baseData = {} + this.controller = null + this.currentChunk = '' + this.content = '' + } + + init(type = 'huoshan', options = {}) { + // 火山引擎接口 + if (type === 'huoshan') { + this.baseData = { + api: options.api, + method: options.method, + headers: { + Authorization: 'Bearer ' + options.key + }, + data: { + model: options.model, + stream: true + } + } + } + } + + async request(data, progress = () => {}, end = () => {}, err = () => {}) { + try { + const res = await this.postMsg(data) + const decoder = new TextDecoder() + while (1) { + const { done, value } = await res.read() + if (done) { + return + } + // 拿到当前切片的数据 + const text = decoder.decode(value) + // 处理切片数据 + let chunk = this.handleChunkData(text) + // 判断是否有不完整切片,如果有,合并下一次处理,没有则获取数据 + if (this.currentChunk) continue + let isEnd = false + const list = chunk + .split('\n') + .filter(item => { + isEnd = item.includes('[DONE]') + return !!item && !isEnd + }) + .map(item => { + return JSON.parse(item.replace(/^data:/, '')) + }) + list.forEach(item => { + this.content += item.choices + .map(item2 => { + return item2.delta.content + }) + .join('') + }) + progress(this.content) + if (isEnd) { + end(this.content) + } + } + } catch (error) { + console.log(error) + // 手动停止请求不需要触发错误回调 + if (!(error && error.name === 'AbortError')) { + err(error) + } + } + } + + async postMsg(data) { + this.controller = new AbortController() + const res = await fetch(`http://localhost:${this.options.port}/ai/chat`, { + signal: this.controller.signal, + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + ...this.baseData, + data: { + ...this.baseData.data, + ...data + } + }) + }) + if (res.status && res.status !== 200) { + return false + } + return res.body.getReader() + } + + handleChunkData(chunk) { + chunk = chunk.trim() + // 如果存在上一个切片 + if (this.currentChunk) { + chunk = this.currentChunk + chunk + this.currentChunk = '' + } + // 如果存在done,认为是完整切片且是最后一个切片 + if (chunk.includes('[DONE]')) { + return chunk + } + // 最后一个字符串不为},则默认切片不完整,保存与下次拼接使用(这种方法不严谨,但已经能解决大部分场景的问题) + if (chunk[chunk.length - 1] !== '}') { + this.currentChunk = chunk + } + return chunk + } + + stop() { + this.controller.abort() + this.controller = new AbortController() + } +} + +export default Ai diff --git a/web/vue.config.js b/web/vue.config.js index b624cbc3..cd7b6cca 100644 --- a/web/vue.config.js +++ b/web/vue.config.js @@ -38,5 +38,13 @@ module.exports = { '@': path.resolve(__dirname, './src/') } } + }, + devServer: { + proxy: { + '^/api/v3/': { + target: 'http://ark.cn-beijing.volces.com', + changeOrigin: true + } + } } }