Merge branch 'test' into feature
This commit is contained in:
commit
e05df4e92b
@ -820,6 +820,7 @@ class RichText {
|
|||||||
|
|
||||||
// 处理导入数据
|
// 处理导入数据
|
||||||
handleSetData(data) {
|
handleSetData(data) {
|
||||||
|
if (!data) return
|
||||||
// 短期处理,为了兼容老数据,长期会去除
|
// 短期处理,为了兼容老数据,长期会去除
|
||||||
const isOldRichTextVersion =
|
const isOldRichTextVersion =
|
||||||
!data.smmVersion || compareVersion(data.smmVersion, '0.13.0') === '<'
|
!data.smmVersion || compareVersion(data.smmVersion, '0.13.0') === '<'
|
||||||
|
|||||||
@ -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 elRect = mindMap.elRect
|
||||||
let { scaleX, scaleY, translateX, translateY } = mindMap.draw.transform()
|
let { scaleX, scaleY, translateX, translateY } = mindMap.draw.transform()
|
||||||
let { left, top, width, height } = node
|
let { left, top, width, height } = node
|
||||||
@ -408,17 +408,17 @@ export const checkNodeOuter = (mindMap, node) => {
|
|||||||
top = top * scaleY + translateY
|
top = top * scaleY + translateY
|
||||||
let offsetLeft = 0
|
let offsetLeft = 0
|
||||||
let offsetTop = 0
|
let offsetTop = 0
|
||||||
if (left < 0) {
|
if (left < 0 + offsetX) {
|
||||||
offsetLeft = -left
|
offsetLeft = -left + offsetX
|
||||||
}
|
}
|
||||||
if (right > elRect.width) {
|
if (right > elRect.width - offsetX) {
|
||||||
offsetLeft = -(right - elRect.width)
|
offsetLeft = -(right - elRect.width) - offsetX
|
||||||
}
|
}
|
||||||
if (top < 0) {
|
if (top < 0 + offsetY) {
|
||||||
offsetTop = -top
|
offsetTop = -top + offsetY
|
||||||
}
|
}
|
||||||
if (bottom > elRect.height) {
|
if (bottom > elRect.height - offsetY) {
|
||||||
offsetTop = -(bottom - elRect.height)
|
offsetTop = -(bottom - elRect.height) - offsetY
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
isOuter: offsetLeft !== 0 || offsetTop !== 0,
|
isOuter: offsetLeft !== 0 || offsetTop !== 0,
|
||||||
@ -508,7 +508,7 @@ export const loadImage = imgFile => {
|
|||||||
|
|
||||||
// 移除字符串中的html实体
|
// 移除字符串中的html实体
|
||||||
export const removeHTMLEntities = str => {
|
export const removeHTMLEntities = str => {
|
||||||
;[[' ', ' ']].forEach(item => {
|
[[' ', ' ']].forEach(item => {
|
||||||
str = str.replace(new RegExp(item[0], 'g'), item[1])
|
str = str.replace(new RegExp(item[0], 'g'), item[1])
|
||||||
})
|
})
|
||||||
return str
|
return str
|
||||||
@ -1069,7 +1069,7 @@ export const generateColorByContent = str => {
|
|||||||
|
|
||||||
// html转义
|
// html转义
|
||||||
export const htmlEscape = str => {
|
export const htmlEscape = str => {
|
||||||
;[
|
[
|
||||||
['&', '&'],
|
['&', '&'],
|
||||||
['<', '<'],
|
['<', '<'],
|
||||||
['>', '>']
|
['>', '>']
|
||||||
|
|||||||
217
web/package-lock.json
generated
217
web/package-lock.json
generated
@ -31,6 +31,7 @@
|
|||||||
"esbuild": "^0.17.15",
|
"esbuild": "^0.17.15",
|
||||||
"eslint": "^6.7.2",
|
"eslint": "^6.7.2",
|
||||||
"eslint-plugin-vue": "^6.2.2",
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
|
"express": "^4.21.2",
|
||||||
"less": "^3.12.2",
|
"less": "^3.12.2",
|
||||||
"less-loader": "^7.1.0",
|
"less-loader": "^7.1.0",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
@ -4161,9 +4162,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bytes": "3.1.2",
|
"bytes": "3.1.2",
|
||||||
@ -4174,7 +4175,7 @@
|
|||||||
"http-errors": "2.0.0",
|
"http-errors": "2.0.0",
|
||||||
"iconv-lite": "0.4.24",
|
"iconv-lite": "0.4.24",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "2.4.1",
|
||||||
"qs": "6.11.0",
|
"qs": "6.13.0",
|
||||||
"raw-body": "2.5.2",
|
"raw-body": "2.5.2",
|
||||||
"type-is": "~1.6.18",
|
"type-is": "~1.6.18",
|
||||||
"unpipe": "1.0.0"
|
"unpipe": "1.0.0"
|
||||||
@ -4200,12 +4201,12 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/body-parser/node_modules/qs": {
|
"node_modules/body-parser/node_modules/qs": {
|
||||||
"version": "6.11.0",
|
"version": "6.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"side-channel": "^1.0.4"
|
"side-channel": "^1.0.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
@ -5240,9 +5241,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "0.5.0",
|
"version": "0.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
@ -6623,9 +6624,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/encodeurl": {
|
"node_modules/encodeurl": {
|
||||||
"version": "1.0.2",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
@ -7353,37 +7354,37 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.18.3",
|
"version": "4.21.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||||
"integrity": "sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==",
|
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
"body-parser": "1.20.2",
|
"body-parser": "1.20.3",
|
||||||
"content-disposition": "0.5.4",
|
"content-disposition": "0.5.4",
|
||||||
"content-type": "~1.0.4",
|
"content-type": "~1.0.4",
|
||||||
"cookie": "0.5.0",
|
"cookie": "0.7.1",
|
||||||
"cookie-signature": "1.0.6",
|
"cookie-signature": "1.0.6",
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"depd": "2.0.0",
|
"depd": "2.0.0",
|
||||||
"encodeurl": "~1.0.2",
|
"encodeurl": "~2.0.0",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"etag": "~1.8.1",
|
"etag": "~1.8.1",
|
||||||
"finalhandler": "1.2.0",
|
"finalhandler": "1.3.1",
|
||||||
"fresh": "0.5.2",
|
"fresh": "0.5.2",
|
||||||
"http-errors": "2.0.0",
|
"http-errors": "2.0.0",
|
||||||
"merge-descriptors": "1.0.1",
|
"merge-descriptors": "1.0.3",
|
||||||
"methods": "~1.1.2",
|
"methods": "~1.1.2",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "2.4.1",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
"path-to-regexp": "0.1.7",
|
"path-to-regexp": "0.1.12",
|
||||||
"proxy-addr": "~2.0.7",
|
"proxy-addr": "~2.0.7",
|
||||||
"qs": "6.11.0",
|
"qs": "6.13.0",
|
||||||
"range-parser": "~1.2.1",
|
"range-parser": "~1.2.1",
|
||||||
"safe-buffer": "5.2.1",
|
"safe-buffer": "5.2.1",
|
||||||
"send": "0.18.0",
|
"send": "0.19.0",
|
||||||
"serve-static": "1.15.0",
|
"serve-static": "1.16.2",
|
||||||
"setprototypeof": "1.2.0",
|
"setprototypeof": "1.2.0",
|
||||||
"statuses": "2.0.1",
|
"statuses": "2.0.1",
|
||||||
"type-is": "~1.6.18",
|
"type-is": "~1.6.18",
|
||||||
@ -7392,6 +7393,10 @@
|
|||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.10.0"
|
"node": ">= 0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express/node_modules/debug": {
|
"node_modules/express/node_modules/debug": {
|
||||||
@ -7410,12 +7415,12 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/express/node_modules/qs": {
|
"node_modules/express/node_modules/qs": {
|
||||||
"version": "6.11.0",
|
"version": "6.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"side-channel": "^1.0.4"
|
"side-channel": "^1.0.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
@ -7712,13 +7717,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/finalhandler": {
|
"node_modules/finalhandler": {
|
||||||
"version": "1.2.0",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||||
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
|
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"encodeurl": "~1.0.2",
|
"encodeurl": "~2.0.0",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "2.4.1",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
@ -10253,10 +10258,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/merge-descriptors": {
|
"node_modules/merge-descriptors": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==",
|
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/merge-source-map": {
|
"node_modules/merge-source-map": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
@ -11572,9 +11580,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "0.1.7",
|
"version": "0.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==",
|
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/path-type": {
|
"node_modules/path-type": {
|
||||||
@ -13295,9 +13303,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/send": {
|
"node_modules/send": {
|
||||||
"version": "0.18.0",
|
"version": "0.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||||
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
|
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
@ -13333,6 +13341,15 @@
|
|||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/send/node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@ -13427,15 +13444,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/serve-static": {
|
"node_modules/serve-static": {
|
||||||
"version": "1.15.0",
|
"version": "1.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||||
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
|
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"encodeurl": "~1.0.2",
|
"encodeurl": "~2.0.0",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
"send": "0.18.0"
|
"send": "0.19.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
@ -20333,9 +20350,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"body-parser": {
|
"body-parser": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"bytes": "3.1.2",
|
"bytes": "3.1.2",
|
||||||
@ -20346,7 +20363,7 @@
|
|||||||
"http-errors": "2.0.0",
|
"http-errors": "2.0.0",
|
||||||
"iconv-lite": "0.4.24",
|
"iconv-lite": "0.4.24",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "2.4.1",
|
||||||
"qs": "6.11.0",
|
"qs": "6.13.0",
|
||||||
"raw-body": "2.5.2",
|
"raw-body": "2.5.2",
|
||||||
"type-is": "~1.6.18",
|
"type-is": "~1.6.18",
|
||||||
"unpipe": "1.0.0"
|
"unpipe": "1.0.0"
|
||||||
@ -20368,12 +20385,12 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"qs": {
|
"qs": {
|
||||||
"version": "6.11.0",
|
"version": "6.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"side-channel": "^1.0.4"
|
"side-channel": "^1.0.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -21195,9 +21212,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"cookie": {
|
"cookie": {
|
||||||
"version": "0.5.0",
|
"version": "0.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"cookie-signature": {
|
"cookie-signature": {
|
||||||
@ -22285,9 +22302,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"encodeurl": {
|
"encodeurl": {
|
||||||
"version": "1.0.2",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"end-of-stream": {
|
"end-of-stream": {
|
||||||
@ -22857,37 +22874,37 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"express": {
|
"express": {
|
||||||
"version": "4.18.3",
|
"version": "4.21.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||||
"integrity": "sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==",
|
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
"body-parser": "1.20.2",
|
"body-parser": "1.20.3",
|
||||||
"content-disposition": "0.5.4",
|
"content-disposition": "0.5.4",
|
||||||
"content-type": "~1.0.4",
|
"content-type": "~1.0.4",
|
||||||
"cookie": "0.5.0",
|
"cookie": "0.7.1",
|
||||||
"cookie-signature": "1.0.6",
|
"cookie-signature": "1.0.6",
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"depd": "2.0.0",
|
"depd": "2.0.0",
|
||||||
"encodeurl": "~1.0.2",
|
"encodeurl": "~2.0.0",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"etag": "~1.8.1",
|
"etag": "~1.8.1",
|
||||||
"finalhandler": "1.2.0",
|
"finalhandler": "1.3.1",
|
||||||
"fresh": "0.5.2",
|
"fresh": "0.5.2",
|
||||||
"http-errors": "2.0.0",
|
"http-errors": "2.0.0",
|
||||||
"merge-descriptors": "1.0.1",
|
"merge-descriptors": "1.0.3",
|
||||||
"methods": "~1.1.2",
|
"methods": "~1.1.2",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "2.4.1",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
"path-to-regexp": "0.1.7",
|
"path-to-regexp": "0.1.12",
|
||||||
"proxy-addr": "~2.0.7",
|
"proxy-addr": "~2.0.7",
|
||||||
"qs": "6.11.0",
|
"qs": "6.13.0",
|
||||||
"range-parser": "~1.2.1",
|
"range-parser": "~1.2.1",
|
||||||
"safe-buffer": "5.2.1",
|
"safe-buffer": "5.2.1",
|
||||||
"send": "0.18.0",
|
"send": "0.19.0",
|
||||||
"serve-static": "1.15.0",
|
"serve-static": "1.16.2",
|
||||||
"setprototypeof": "1.2.0",
|
"setprototypeof": "1.2.0",
|
||||||
"statuses": "2.0.1",
|
"statuses": "2.0.1",
|
||||||
"type-is": "~1.6.18",
|
"type-is": "~1.6.18",
|
||||||
@ -22911,12 +22928,12 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"qs": {
|
"qs": {
|
||||||
"version": "6.11.0",
|
"version": "6.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"side-channel": "^1.0.4"
|
"side-channel": "^1.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
@ -23145,13 +23162,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"finalhandler": {
|
"finalhandler": {
|
||||||
"version": "1.2.0",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||||
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
|
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"encodeurl": "~1.0.2",
|
"encodeurl": "~2.0.0",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "2.4.1",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
@ -25081,9 +25098,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"merge-descriptors": {
|
"merge-descriptors": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==",
|
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"merge-source-map": {
|
"merge-source-map": {
|
||||||
@ -26137,9 +26154,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"path-to-regexp": {
|
"path-to-regexp": {
|
||||||
"version": "0.1.7",
|
"version": "0.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==",
|
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"path-type": {
|
"path-type": {
|
||||||
@ -27595,9 +27612,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"send": {
|
"send": {
|
||||||
"version": "0.18.0",
|
"version": "0.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||||
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
|
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"debug": "2.6.9",
|
"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": {
|
"ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@ -27718,15 +27741,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"serve-static": {
|
"serve-static": {
|
||||||
"version": "1.15.0",
|
"version": "1.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||||
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
|
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"encodeurl": "~1.0.2",
|
"encodeurl": "~2.0.0",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
"send": "0.18.0"
|
"send": "0.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"set-blocking": {
|
"set-blocking": {
|
||||||
|
|||||||
@ -8,7 +8,8 @@
|
|||||||
"lint": "vue-cli-service lint",
|
"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",
|
"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/*/*/*/*",
|
"format": "prettier --write src/* src/*/* src/*/*/* src/*/*/*/*",
|
||||||
"createNodeImageList": "node ./scripts/createNodeImageList.js"
|
"createNodeImageList": "node ./scripts/createNodeImageList.js",
|
||||||
|
"ai:serve": "node ./scripts/ai.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@toast-ui/editor": "^3.1.5",
|
"@toast-ui/editor": "^3.1.5",
|
||||||
@ -34,6 +35,7 @@
|
|||||||
"esbuild": "^0.17.15",
|
"esbuild": "^0.17.15",
|
||||||
"eslint": "^6.7.2",
|
"eslint": "^6.7.2",
|
||||||
"eslint-plugin-vue": "^6.2.2",
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
|
"express": "^4.21.2",
|
||||||
"less": "^3.12.2",
|
"less": "^3.12.2",
|
||||||
"less-loader": "^7.1.0",
|
"less-loader": "^7.1.0",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
|
|||||||
108
web/scripts/ai.js
Normal file
108
web/scripts/ai.js
Normal file
@ -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}`)
|
||||||
|
})
|
||||||
@ -1,8 +1,8 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: "iconfont"; /* Project id 2479351 */
|
font-family: "iconfont"; /* Project id 2479351 */
|
||||||
src: url('iconfont.woff2?t=1739152990179') format('woff2'),
|
src: url('iconfont.woff2?t=1739843331607') format('woff2'),
|
||||||
url('iconfont.woff?t=1739152990179') format('woff'),
|
url('iconfont.woff?t=1739843331607') format('woff'),
|
||||||
url('iconfont.ttf?t=1739152990179') format('truetype');
|
url('iconfont.ttf?t=1739843331607') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@ -13,6 +13,10 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.iconAIshengcheng:before {
|
||||||
|
content: "\e6b5";
|
||||||
|
}
|
||||||
|
|
||||||
.iconprinting:before {
|
.iconprinting:before {
|
||||||
content: "\ea28";
|
content: "\ea28";
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -444,6 +444,11 @@ export const sidebarTriggerList = [
|
|||||||
value: 'setting',
|
value: 'setting',
|
||||||
icon: 'iconshezhi'
|
icon: 'iconshezhi'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'AI',
|
||||||
|
value: 'ai',
|
||||||
|
icon: 'iconAIshengcheng'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'ShortcutKey',
|
name: 'ShortcutKey',
|
||||||
value: 'shortcutKey',
|
value: 'shortcutKey',
|
||||||
|
|||||||
@ -534,6 +534,11 @@ export const sidebarTriggerList = [
|
|||||||
value: 'outline',
|
value: 'outline',
|
||||||
icon: 'iconfuhao-dagangshu'
|
icon: 'iconfuhao-dagangshu'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'AI',
|
||||||
|
value: 'ai',
|
||||||
|
icon: 'iconAIshengcheng'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: '设置',
|
name: '设置',
|
||||||
value: 'setting',
|
value: 'setting',
|
||||||
|
|||||||
@ -439,6 +439,11 @@ export const sidebarTriggerList = [
|
|||||||
value: 'outline',
|
value: 'outline',
|
||||||
icon: 'iconfuhao-dagangshu'
|
icon: 'iconfuhao-dagangshu'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'AI',
|
||||||
|
value: 'ai',
|
||||||
|
icon: 'iconAIshengcheng'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: '設置',
|
name: '設置',
|
||||||
value: 'setting',
|
value: 'setting',
|
||||||
|
|||||||
@ -134,7 +134,8 @@ export default {
|
|||||||
expandNodeChild: 'Expand all sub nodes',
|
expandNodeChild: 'Expand all sub nodes',
|
||||||
unExpandNodeChild: 'Un expand all sub nodes',
|
unExpandNodeChild: 'Un expand all sub nodes',
|
||||||
addToDo: 'Add toDo',
|
addToDo: 'Add toDo',
|
||||||
removeToDo: 'Remove toDo'
|
removeToDo: 'Remove toDo',
|
||||||
|
aiCreate: 'AI Continuation'
|
||||||
},
|
},
|
||||||
count: {
|
count: {
|
||||||
words: 'Words',
|
words: 'Words',
|
||||||
@ -329,7 +330,8 @@ export default {
|
|||||||
newFileTip:
|
newFileTip:
|
||||||
'Please export the currently edited file before creating a new one, Beware of content loss',
|
'Please export the currently edited file before creating a new one, Beware of content loss',
|
||||||
openFileTip:
|
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: {
|
edit: {
|
||||||
newFeatureNoticeTitle: 'New feature reminder',
|
newFeatureNoticeTitle: 'New feature reminder',
|
||||||
|
|||||||
@ -133,7 +133,8 @@ export default {
|
|||||||
expandNodeChild: '展开所有下级节点',
|
expandNodeChild: '展开所有下级节点',
|
||||||
unExpandNodeChild: '收起所有下级节点',
|
unExpandNodeChild: '收起所有下级节点',
|
||||||
addToDo: '添加待办',
|
addToDo: '添加待办',
|
||||||
removeToDo: '删除待办'
|
removeToDo: '删除待办',
|
||||||
|
aiCreate: 'AI续写'
|
||||||
},
|
},
|
||||||
count: {
|
count: {
|
||||||
words: '字数',
|
words: '字数',
|
||||||
@ -323,7 +324,8 @@ export default {
|
|||||||
creatingTip: '正在创建文件',
|
creatingTip: '正在创建文件',
|
||||||
directory: '目录',
|
directory: '目录',
|
||||||
newFileTip: '新建文件前请先导出当前编辑的文件,谨防内容丢失',
|
newFileTip: '新建文件前请先导出当前编辑的文件,谨防内容丢失',
|
||||||
openFileTip: '打开文件前请先导出当前编辑的文件,谨防内容丢失'
|
openFileTip: '打开文件前请先导出当前编辑的文件,谨防内容丢失',
|
||||||
|
ai: 'AI'
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
newFeatureNoticeTitle: '新特性提醒',
|
newFeatureNoticeTitle: '新特性提醒',
|
||||||
@ -412,5 +414,8 @@ export default {
|
|||||||
nodeTagStyle: {
|
nodeTagStyle: {
|
||||||
placeholder: '请输入标签内容',
|
placeholder: '请输入标签内容',
|
||||||
delete: '删除此标签'
|
delete: '删除此标签'
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
chatTitle: 'AI对话'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -134,7 +134,8 @@ export default {
|
|||||||
expandNodeChild: '展開所有下級節點',
|
expandNodeChild: '展開所有下級節點',
|
||||||
unExpandNodeChild: '收起所有下級節點',
|
unExpandNodeChild: '收起所有下級節點',
|
||||||
addToDo: '添加待辦',
|
addToDo: '添加待辦',
|
||||||
removeToDo: '刪除待辦'
|
removeToDo: '刪除待辦',
|
||||||
|
aiCreate: 'AI續寫'
|
||||||
},
|
},
|
||||||
count: {
|
count: {
|
||||||
words: '字數',
|
words: '字數',
|
||||||
@ -323,7 +324,8 @@ export default {
|
|||||||
creatingTip: '正在建立檔案',
|
creatingTip: '正在建立檔案',
|
||||||
directory: '目錄',
|
directory: '目錄',
|
||||||
newFileTip: '新增檔案前,請先匯出目前編輯的檔案,以免內容遺失',
|
newFileTip: '新增檔案前,請先匯出目前編輯的檔案,以免內容遺失',
|
||||||
openFileTip: '開啟檔案前,請先匯出目前編輯的檔案,以免內容遺失'
|
openFileTip: '開啟檔案前,請先匯出目前編輯的檔案,以免內容遺失',
|
||||||
|
ai: 'AI'
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
newFeatureNoticeTitle: '新功能提醒',
|
newFeatureNoticeTitle: '新功能提醒',
|
||||||
|
|||||||
@ -35,4 +35,3 @@ if (window.takeOverApp) {
|
|||||||
} else {
|
} else {
|
||||||
initApp()
|
initApp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
290
web/src/pages/Edit/components/AiChat.vue
Normal file
290
web/src/pages/Edit/components/AiChat.vue
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
<template>
|
||||||
|
<Sidebar ref="sidebar" :title="$t('ai.chatTitle')">
|
||||||
|
<div class="aiChatBox" :class="{ isDark: isDark }">
|
||||||
|
<div class="chatHeader">
|
||||||
|
<el-button size="mini" @click="clear">清空记录</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="chatResBox customScrollbar" ref="chatResBoxRef">
|
||||||
|
<div
|
||||||
|
class="chatItem"
|
||||||
|
v-for="item in chatList"
|
||||||
|
:key="item.id"
|
||||||
|
:class="[item.type]"
|
||||||
|
>
|
||||||
|
<div class="chatItemInner" v-if="item.type === 'user'">
|
||||||
|
<div class="avatar">
|
||||||
|
<span class="icon el-icon-user"></span>
|
||||||
|
</div>
|
||||||
|
<div class="content">{{ item.content }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="chatItemInner" v-else-if="item.type === 'ai'">
|
||||||
|
<div class="avatar">
|
||||||
|
<span class="icon iconfont iconAIshengcheng"></span>
|
||||||
|
</div>
|
||||||
|
<div class="content" v-html="item.content"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chatInputBox">
|
||||||
|
<textarea
|
||||||
|
v-model="text"
|
||||||
|
class="customScrollbar"
|
||||||
|
placeholder="Enter 发送,Shift + Enter 换行。"
|
||||||
|
@keydown.enter.prevent
|
||||||
|
@keyup.enter.prevent="send"
|
||||||
|
></textarea>
|
||||||
|
<el-button class="btn" size="mini" @click="send" :loading="isCreating"
|
||||||
|
>发送</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Sidebar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Sidebar from './Sidebar'
|
||||||
|
import { mapState, mapMutations } from 'vuex'
|
||||||
|
import { createUid } from 'simple-mind-map/src/utils'
|
||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
|
||||||
|
let md = null
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Sidebar
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
text: '',
|
||||||
|
chatList: [],
|
||||||
|
isCreating: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
isDark: state => state.localConfig.isDark,
|
||||||
|
activeSidebar: state => state.activeSidebar
|
||||||
|
})
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
activeSidebar(val) {
|
||||||
|
if (val === 'ai') {
|
||||||
|
this.$refs.sidebar.show = true
|
||||||
|
} else {
|
||||||
|
this.$refs.sidebar.show = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {},
|
||||||
|
beforeDestroy() {},
|
||||||
|
methods: {
|
||||||
|
send() {
|
||||||
|
if (this.isCreating) return
|
||||||
|
const text = this.text.trim()
|
||||||
|
if (!text) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.text = ''
|
||||||
|
this.chatList.push({
|
||||||
|
id: createUid(),
|
||||||
|
type: 'user',
|
||||||
|
content: text
|
||||||
|
})
|
||||||
|
this.chatList.push({
|
||||||
|
id: createUid(),
|
||||||
|
type: 'ai',
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
this.isCreating = true
|
||||||
|
this.$bus.$emit(
|
||||||
|
'ai_chat',
|
||||||
|
text,
|
||||||
|
res => {
|
||||||
|
console.log(res)
|
||||||
|
if (!md) {
|
||||||
|
md = new MarkdownIt()
|
||||||
|
}
|
||||||
|
this.chatList[this.chatList.length - 1].content = md.render(res)
|
||||||
|
this.$refs.chatResBoxRef.scrollTop = this.$refs.chatResBoxRef.scrollHeight
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.isCreating = false
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.isCreating = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.chatList = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.aiChatBox {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&.isDark {
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatHeader {
|
||||||
|
height: 50px;
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatResBox {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 12px;
|
||||||
|
margin: 12px 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
.chatItem {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border: 1px solid;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ai {
|
||||||
|
border-color: #409eff;
|
||||||
|
|
||||||
|
.chatItemInner {
|
||||||
|
.avatar {
|
||||||
|
border-color: #409eff;
|
||||||
|
left: -12px;
|
||||||
|
top: -12px;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.user {
|
||||||
|
border-color: #f56c6c;
|
||||||
|
|
||||||
|
.chatItemInner {
|
||||||
|
.avatar {
|
||||||
|
border-color: #f56c6c;
|
||||||
|
right: -12px;
|
||||||
|
top: -12px;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatItemInner {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: absolute;
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .content {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #3f4a54;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 85%;
|
||||||
|
white-space: break-spaces;
|
||||||
|
background-color: rgba(175, 184, 193, 0.2);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
|
||||||
|
Liberation Mono, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
padding: 12px;
|
||||||
|
background-color: rgba(175, 184, 193, 0.2);
|
||||||
|
|
||||||
|
code {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatInputBox {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 150px;
|
||||||
|
border-top: 1px solid #e8e8e8;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
outline: none;
|
||||||
|
padding: 12px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
154
web/src/pages/Edit/components/AiConfigDialog.vue
Normal file
154
web/src/pages/Edit/components/AiConfigDialog.vue
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
class="aiConfigDialog"
|
||||||
|
title="AI配置"
|
||||||
|
:visible.sync="aiConfigDialogVisible"
|
||||||
|
width="550px"
|
||||||
|
append-to-body
|
||||||
|
>
|
||||||
|
<div class="aiConfigBox">
|
||||||
|
<el-form
|
||||||
|
:model="ruleForm"
|
||||||
|
:rules="rules"
|
||||||
|
ref="ruleFormRef"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<p class="title">火山方舟大模型配置:</p>
|
||||||
|
<p class="desc">
|
||||||
|
目前仅支持火山方舟大模型,需要自行去获取key,详细操作步骤见:<a
|
||||||
|
href=""
|
||||||
|
>教程</a
|
||||||
|
>。
|
||||||
|
</p>
|
||||||
|
<el-form-item label="API Key" prop="key">
|
||||||
|
<el-input v-model="ruleForm.key"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="推理接入点" prop="model">
|
||||||
|
<el-input v-model="ruleForm.model"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="接口" prop="api">
|
||||||
|
<el-input v-model="ruleForm.api"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="请求方式" prop="method">
|
||||||
|
<el-select v-model="ruleForm.method" placeholder="请选择">
|
||||||
|
<el-option key="POST" label="POST" value="POST"></el-option>
|
||||||
|
<el-option key="GET" label="GET" value="GET"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<p class="title">思绪思维导图客户端配置:</p>
|
||||||
|
<el-form-item label="端口" prop="port">
|
||||||
|
<el-input v-model="ruleForm.port"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="cancel">取消</el-button>
|
||||||
|
<el-button type="primary" @click="confirm">确认</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState, mapMutations } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
model: {
|
||||||
|
prop: 'visible',
|
||||||
|
event: 'change'
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
aiConfigDialogVisible: false,
|
||||||
|
ruleForm: {
|
||||||
|
api: '',
|
||||||
|
key: '',
|
||||||
|
model: '',
|
||||||
|
port: '',
|
||||||
|
method: ''
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
api: [{ required: true, message: '请输入接口', trigger: 'blur' }],
|
||||||
|
key: [{ required: true, message: '请输入API Key', trigger: 'blur' }],
|
||||||
|
model: [
|
||||||
|
{ required: true, message: '请输入推理接入点', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
port: [{ required: true, message: '请输入端口', trigger: 'blur' }],
|
||||||
|
method: [{ required: true, message: '请选择', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(['aiConfig'])
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
visible(val) {
|
||||||
|
this.aiConfigDialogVisible = val
|
||||||
|
},
|
||||||
|
aiConfigDialogVisible(val, oldVal) {
|
||||||
|
if (!val && oldVal) {
|
||||||
|
this.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.initFormData()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapMutations(['setLocalConfig']),
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.$emit('change', false)
|
||||||
|
},
|
||||||
|
|
||||||
|
initFormData() {
|
||||||
|
Object.keys(this.aiConfig).forEach(key => {
|
||||||
|
this.ruleForm[key] = this.aiConfig[key]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.close()
|
||||||
|
this.initFormData()
|
||||||
|
},
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
this.$refs.ruleFormRef.validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
this.close()
|
||||||
|
this.setLocalConfig({
|
||||||
|
...this.ruleForm
|
||||||
|
})
|
||||||
|
this.$message.success('配置保存成功')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.aiConfigDialog {
|
||||||
|
/deep/ .el-dialog__body {
|
||||||
|
padding: 12px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aiConfigBox {
|
||||||
|
.title {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-left: 12px;
|
||||||
|
border-left: 5px solid #ccc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
546
web/src/pages/Edit/components/AiCreate.vue
Normal file
546
web/src/pages/Edit/components/AiCreate.vue
Normal file
@ -0,0 +1,546 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- 客户端连接失败提示弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
class="clientTipDialog"
|
||||||
|
title="客户端连接失败提示"
|
||||||
|
:visible.sync="clientTipDialogVisible"
|
||||||
|
width="400px"
|
||||||
|
append-to-body
|
||||||
|
>
|
||||||
|
<div class="tipBox">
|
||||||
|
<p>客户端连接失败,请检查:</p>
|
||||||
|
<p>
|
||||||
|
1.是否安装了思绪思维导图客户端,如果没有请点此安装:<a
|
||||||
|
href="https://pan.baidu.com/s/1huasEbKsGNH2Af68dvWiOg?pwd=3bp3"
|
||||||
|
>百度网盘</a
|
||||||
|
>、<a href="https://github.com/wanglin2/mind-map/releases">Github</a>
|
||||||
|
</p>
|
||||||
|
<p>2.如果安装了客户端,请确认是否打开了客户端。</p>
|
||||||
|
<P>3.如果已经安装并启动了,那么可以尝试关闭然后重新启动。</P>
|
||||||
|
<p>
|
||||||
|
完成以上步骤后可点击:<el-button size="small" @click="testConnect"
|
||||||
|
>连接检测</el-button
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="clientTipDialogVisible = false"
|
||||||
|
>关闭</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
<!-- ai内容输入弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
class="createDialog"
|
||||||
|
title="一键生成思维导图"
|
||||||
|
:visible.sync="createDialogVisible"
|
||||||
|
width="450px"
|
||||||
|
append-to-body
|
||||||
|
>
|
||||||
|
<div class="inputBox">
|
||||||
|
<el-input
|
||||||
|
type="textarea"
|
||||||
|
:rows="5"
|
||||||
|
placeholder="请输入一个主题,AI会根据你的主题生成思维导图,如:杭州周末出游计划。"
|
||||||
|
v-model="aiInput"
|
||||||
|
>
|
||||||
|
</el-input>
|
||||||
|
<div class="tip warning">
|
||||||
|
重要提示:一键生成会覆盖现有数据,建议先导出当前数据。
|
||||||
|
</div>
|
||||||
|
<div class="tip">
|
||||||
|
想要修改AI配置?请点击:<el-button
|
||||||
|
size="small"
|
||||||
|
@click="aiConfigDialogVisible = true"
|
||||||
|
>修改配置</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="closeAiCreateDialog">取消</el-button>
|
||||||
|
<el-button type="primary" @click="doAiCreate">确认</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
<!-- ai生成中添加一个透明层,防止期间用户进行操作 -->
|
||||||
|
<div
|
||||||
|
class="aiCreatingMask"
|
||||||
|
ref="aiCreatingMaskRef"
|
||||||
|
v-show="aiCreatingMaskVisible"
|
||||||
|
>
|
||||||
|
<el-button type="warning" class="btn" @click="stopCreate"
|
||||||
|
>停止生成</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<AiConfigDialog v-model="aiConfigDialogVisible"></AiConfigDialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Ai from '@/utils/ai'
|
||||||
|
import { transformMarkdownTo } from 'simple-mind-map/src/parse/markdownTo'
|
||||||
|
import {
|
||||||
|
createUid,
|
||||||
|
isUndef,
|
||||||
|
checkNodeOuter,
|
||||||
|
getStrWithBrFromHtml
|
||||||
|
} from 'simple-mind-map/src/utils'
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
import AiConfigDialog from './AiConfigDialog.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
AiConfigDialog
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
mindMap: {
|
||||||
|
type: Object
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
aiInstance: null,
|
||||||
|
isAiCreating: false,
|
||||||
|
aiCreatingContent: '',
|
||||||
|
|
||||||
|
isLoopRendering: false,
|
||||||
|
uidMap: {},
|
||||||
|
latestUid: '',
|
||||||
|
|
||||||
|
clientTipDialogVisible: false,
|
||||||
|
createDialogVisible: false,
|
||||||
|
aiInput: '',
|
||||||
|
aiCreatingMaskVisible: false,
|
||||||
|
aiConfigDialogVisible: false,
|
||||||
|
|
||||||
|
mindMapDataCache: '',
|
||||||
|
beingAiCreateNodeUid: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(['aiConfig'])
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.$bus.$on('ai_create_all', this.aiCrateAll)
|
||||||
|
this.$bus.$on('ai_create_part', this.aiCreatePart)
|
||||||
|
this.$bus.$on('ai_chat', this.aiChat)
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
document.body.appendChild(this.$refs.aiCreatingMaskRef)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$bus.$off('ai_create_all', this.aiCrateAll)
|
||||||
|
this.$bus.$off('ai_create_part', this.aiCreatePart)
|
||||||
|
this.$bus.$off('ai_chat', this.aiChat)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 客户端连接检测
|
||||||
|
async testConnect() {
|
||||||
|
try {
|
||||||
|
await fetch(`http://localhost:${this.aiConfig.port}/ai/test`, {
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
this.$message.success('连接成功')
|
||||||
|
this.clientTipDialogVisible = false
|
||||||
|
this.createDialogVisible = true
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
this.$message.error('连接失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 检测ai是否可用
|
||||||
|
async aiTest() {
|
||||||
|
// 检查配置
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
this.aiConfig.api &&
|
||||||
|
this.aiConfig.key &&
|
||||||
|
this.aiConfig.model &&
|
||||||
|
this.aiConfig.port
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.aiConfigDialogVisible = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 检查连接
|
||||||
|
await fetch(`http://localhost:${this.aiConfig.port}/ai/test`, {
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// AI生成整体
|
||||||
|
async aiCrateAll() {
|
||||||
|
try {
|
||||||
|
await this.aiTest()
|
||||||
|
this.createDialogVisible = true
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
this.clientTipDialogVisible = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 关闭ai内容输入弹窗
|
||||||
|
closeAiCreateDialog() {
|
||||||
|
this.createDialogVisible = false
|
||||||
|
this.aiInput = ''
|
||||||
|
},
|
||||||
|
|
||||||
|
// 确认生成
|
||||||
|
doAiCreate() {
|
||||||
|
const aiInputText = this.aiInput.trim()
|
||||||
|
if (!aiInputText) {
|
||||||
|
this.$message.warning('请输入内容')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.closeAiCreateDialog()
|
||||||
|
this.aiCreatingMaskVisible = true
|
||||||
|
// 发起请求
|
||||||
|
this.isAiCreating = true
|
||||||
|
this.aiInstance = new Ai({
|
||||||
|
port: this.aiConfig.port
|
||||||
|
})
|
||||||
|
this.aiInstance.init('huoshan', this.aiConfig)
|
||||||
|
this.mindMap.renderer.setRootNodeCenter()
|
||||||
|
this.mindMap.setData(null)
|
||||||
|
this.aiInstance.request(
|
||||||
|
{
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: `帮我写一个【${aiInputText}】,需要以Markdown格式返回,并且只能使用Markdown的标题和无序列表两种语法,可以支持多层嵌套。只需返回内容即可。`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
content => {
|
||||||
|
if (content && /\n$/.test(content)) {
|
||||||
|
this.aiCreatingContent = content
|
||||||
|
}
|
||||||
|
this.loopRenderOnAiCreating()
|
||||||
|
},
|
||||||
|
content => {
|
||||||
|
this.aiCreatingContent = content
|
||||||
|
this.resetOnAiCreatingStop()
|
||||||
|
this.$message.success('AI生成完成')
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.resetOnAiCreatingStop()
|
||||||
|
this.resetOnRenderEnd()
|
||||||
|
this.$message.error('生成失败')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
// AI请求完成或出错后需要复位的数据
|
||||||
|
resetOnAiCreatingStop() {
|
||||||
|
this.aiCreatingMaskVisible = false
|
||||||
|
this.isAiCreating = false
|
||||||
|
this.aiInstance = null
|
||||||
|
},
|
||||||
|
|
||||||
|
// 渲染结束后需要复位的数据
|
||||||
|
resetOnRenderEnd() {
|
||||||
|
this.isLoopRendering = false
|
||||||
|
this.uidMap = {}
|
||||||
|
this.aiCreatingContent = ''
|
||||||
|
this.mindMapDataCache = ''
|
||||||
|
this.beingAiCreateNodeUid = ''
|
||||||
|
},
|
||||||
|
|
||||||
|
// 停止生成
|
||||||
|
stopCreate() {
|
||||||
|
this.aiInstance.stop()
|
||||||
|
this.isAiCreating = false
|
||||||
|
this.aiCreatingMaskVisible = false
|
||||||
|
this.$message.success('已停止生成')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 轮询进行渲染
|
||||||
|
loopRenderOnAiCreating() {
|
||||||
|
if (!this.aiCreatingContent.trim() || this.isLoopRendering) return
|
||||||
|
this.isLoopRendering = true
|
||||||
|
const treeData = transformMarkdownTo(this.aiCreatingContent)
|
||||||
|
this.addUid(treeData)
|
||||||
|
let lastTreeData = JSON.stringify(treeData)
|
||||||
|
|
||||||
|
// 在当前渲染完成时再进行下一次渲染
|
||||||
|
const onRenderEnd = () => {
|
||||||
|
// 处理超出画布的节点
|
||||||
|
this.checkNodeOuter()
|
||||||
|
|
||||||
|
// 如果生成结束数据渲染完毕,那么解绑事件
|
||||||
|
if (!this.isAiCreating && !this.aiCreatingContent) {
|
||||||
|
this.mindMap.off('node_tree_render_end', onRenderEnd)
|
||||||
|
this.latestUid = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const treeData = transformMarkdownTo(this.aiCreatingContent)
|
||||||
|
this.addUid(treeData)
|
||||||
|
// 正在生成中
|
||||||
|
if (this.isAiCreating) {
|
||||||
|
// 如果和上次数据一样则不触发重新渲染
|
||||||
|
const curTreeData = JSON.stringify(treeData)
|
||||||
|
if (curTreeData === lastTreeData) {
|
||||||
|
setTimeout(() => {
|
||||||
|
onRenderEnd()
|
||||||
|
}, 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastTreeData = curTreeData
|
||||||
|
this.mindMap.updateData(treeData)
|
||||||
|
} else {
|
||||||
|
// 已经生成结束
|
||||||
|
// 还要触发一遍渲染,否则会丢失数据
|
||||||
|
this.mindMap.updateData(treeData)
|
||||||
|
this.resetOnRenderEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.mindMap.on('node_tree_render_end', onRenderEnd)
|
||||||
|
|
||||||
|
this.mindMap.setData(treeData)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 处理超出画布的节点
|
||||||
|
checkNodeOuter() {
|
||||||
|
if (this.latestUid) {
|
||||||
|
const latestNode = this.mindMap.renderer.findNodeByUid(this.latestUid)
|
||||||
|
if (latestNode) {
|
||||||
|
const { isOuter, offsetLeft, offsetTop } = checkNodeOuter(
|
||||||
|
this.mindMap,
|
||||||
|
latestNode,
|
||||||
|
100,
|
||||||
|
100
|
||||||
|
)
|
||||||
|
if (isOuter) {
|
||||||
|
this.mindMap.view.translateXY(offsetLeft, offsetTop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 给AI生成的数据添加uid
|
||||||
|
addUid(data) {
|
||||||
|
const checkRepeatUidMap = {}
|
||||||
|
const walk = (node, pUid = '') => {
|
||||||
|
if (!node.data) {
|
||||||
|
node.data = {}
|
||||||
|
}
|
||||||
|
if (isUndef(node.data.uid)) {
|
||||||
|
// 根据pUid+文本内容来复用上一次生成数据的uid
|
||||||
|
const key = pUid + '-' + node.data.text
|
||||||
|
node.data.uid = this.uidMap[key] || createUid()
|
||||||
|
// 当前uid和之前的重复,那么重新生成一个。这种情况很少,但是以防万一
|
||||||
|
if (checkRepeatUidMap[node.data.uid]) {
|
||||||
|
node.data.uid = createUid()
|
||||||
|
}
|
||||||
|
this.latestUid = this.uidMap[key] = node.data.uid
|
||||||
|
checkRepeatUidMap[node.data.uid] = true
|
||||||
|
}
|
||||||
|
if (node.children && node.children.length > 0) {
|
||||||
|
node.children.forEach(child => {
|
||||||
|
walk(child, node.data.uid)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(data)
|
||||||
|
},
|
||||||
|
|
||||||
|
// AI生成部分
|
||||||
|
async aiCreatePart(node) {
|
||||||
|
try {
|
||||||
|
await this.aiTest()
|
||||||
|
this.beingAiCreateNodeUid = node.getData('uid')
|
||||||
|
const currentMindMapData = this.mindMap.getData()
|
||||||
|
this.mindMapDataCache = JSON.stringify(currentMindMapData)
|
||||||
|
this.aiCreatingMaskVisible = true
|
||||||
|
// 发起请求
|
||||||
|
this.isAiCreating = true
|
||||||
|
this.aiInstance = new Ai({
|
||||||
|
port: this.aiConfig.port
|
||||||
|
})
|
||||||
|
this.aiInstance.init('huoshan', this.aiConfig)
|
||||||
|
this.aiInstance.request(
|
||||||
|
{
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: `我有一个主题为【${getStrWithBrFromHtml(
|
||||||
|
currentMindMapData.data.text
|
||||||
|
)}】的思维导图,帮我续写其中一个内容为【${getStrWithBrFromHtml(
|
||||||
|
node.getData('text')
|
||||||
|
)}】的节点的下级内容,需要以Markdown格式返回,并且只能使用Markdown的标题和无序列表两种语法,可以支持多层嵌套。只需返回内容即可。`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
content => {
|
||||||
|
if (content && /\n$/.test(content)) {
|
||||||
|
this.aiCreatingContent = content
|
||||||
|
}
|
||||||
|
this.loopRenderOnAiCreatingPart()
|
||||||
|
},
|
||||||
|
content => {
|
||||||
|
this.aiCreatingContent = content
|
||||||
|
this.resetOnAiCreatingStop()
|
||||||
|
this.$message.success('AI生成完成')
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.resetOnAiCreatingStop()
|
||||||
|
this.resetOnRenderEnd()
|
||||||
|
this.$message.error('生成失败')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 将生成的数据添加到指定节点上
|
||||||
|
addToTargetNode(newChildren = []) {
|
||||||
|
const initData = JSON.parse(this.mindMapDataCache)
|
||||||
|
const walk = node => {
|
||||||
|
if (node.data.uid === this.beingAiCreateNodeUid) {
|
||||||
|
if (!node.children) {
|
||||||
|
node.children = []
|
||||||
|
}
|
||||||
|
node.children.push(...newChildren)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (node.children && node.children.length > 0) {
|
||||||
|
node.children.forEach(child => {
|
||||||
|
walk(child)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(initData)
|
||||||
|
return initData
|
||||||
|
},
|
||||||
|
|
||||||
|
// 轮询进行部分渲染
|
||||||
|
loopRenderOnAiCreatingPart() {
|
||||||
|
if (!this.aiCreatingContent.trim() || this.isLoopRendering) return
|
||||||
|
this.isLoopRendering = true
|
||||||
|
const partData = transformMarkdownTo(this.aiCreatingContent)
|
||||||
|
this.addUid(partData)
|
||||||
|
let lastPartData = JSON.stringify(partData)
|
||||||
|
const treeData = this.addToTargetNode(partData.children || [])
|
||||||
|
|
||||||
|
// 在当前渲染完成时再进行下一次渲染
|
||||||
|
const onRenderEnd = () => {
|
||||||
|
// 处理超出画布的节点
|
||||||
|
this.checkNodeOuter()
|
||||||
|
|
||||||
|
// 如果生成结束数据渲染完毕,那么解绑事件
|
||||||
|
if (!this.isAiCreating && !this.aiCreatingContent) {
|
||||||
|
this.mindMap.off('node_tree_render_end', onRenderEnd)
|
||||||
|
this.latestUid = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const partData = transformMarkdownTo(this.aiCreatingContent)
|
||||||
|
this.addUid(partData)
|
||||||
|
const treeData = this.addToTargetNode(partData.children || [])
|
||||||
|
|
||||||
|
if (this.isAiCreating) {
|
||||||
|
// 如果和上次数据一样则不触发重新渲染
|
||||||
|
const curPartData = JSON.stringify(partData)
|
||||||
|
if (curPartData === lastPartData) {
|
||||||
|
setTimeout(() => {
|
||||||
|
onRenderEnd()
|
||||||
|
}, 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastPartData = curPartData
|
||||||
|
this.mindMap.updateData(treeData)
|
||||||
|
} else {
|
||||||
|
this.mindMap.updateData(treeData)
|
||||||
|
this.resetOnRenderEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.mindMap.on('node_tree_render_end', onRenderEnd)
|
||||||
|
// 因为是续写,所以首次也直接使用updateData方法渲染
|
||||||
|
this.mindMap.updateData(treeData)
|
||||||
|
},
|
||||||
|
|
||||||
|
// AI对话
|
||||||
|
async aiChat(text, progress = () => {}, end = () => {}, err = () => {}) {
|
||||||
|
try {
|
||||||
|
await this.aiTest()
|
||||||
|
// 发起请求
|
||||||
|
this.isAiCreating = true
|
||||||
|
this.aiInstance = new Ai({
|
||||||
|
port: this.aiConfig.port
|
||||||
|
})
|
||||||
|
this.aiInstance.init('huoshan', this.aiConfig)
|
||||||
|
this.aiInstance.request(
|
||||||
|
{
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: text
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
content => {
|
||||||
|
progress(content)
|
||||||
|
},
|
||||||
|
content => {
|
||||||
|
end(content)
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
err(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.clientTipDialog,
|
||||||
|
.createDialog {
|
||||||
|
/deep/ .el-dialog__body {
|
||||||
|
padding: 12px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tipBox {
|
||||||
|
p {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBox {
|
||||||
|
.tip {
|
||||||
|
margin-top: 12px;
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.aiCreatingMask {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 99999;
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 100px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -995,8 +995,6 @@ export default {
|
|||||||
this.$bus.$off('setData', this.onSetData)
|
this.$bus.$off('setData', this.onSetData)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations(['setLocalConfig']),
|
|
||||||
|
|
||||||
onSetData() {
|
onSetData() {
|
||||||
if (this.activeSidebar !== 'baseStyle') return
|
if (this.activeSidebar !== 'baseStyle') return
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@ -140,6 +140,10 @@
|
|||||||
<div class="item" @click="exec('EXPORT_CUR_NODE_TO_PNG')">
|
<div class="item" @click="exec('EXPORT_CUR_NODE_TO_PNG')">
|
||||||
<span class="name">{{ $t('contextmenu.exportNodeToPng') }}</span>
|
<span class="name">{{ $t('contextmenu.exportNodeToPng') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="splitLine"></div>
|
||||||
|
<div class="item" @click="aiCreate">
|
||||||
|
<span class="name">{{ $t('contextmenu.aiCreate') }}</span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="type === 'svg'">
|
<template v-if="type === 'svg'">
|
||||||
<div class="item" @click="exec('RETURN_CENTER')">
|
<div class="item" @click="exec('RETURN_CENTER')">
|
||||||
@ -569,6 +573,12 @@ export default {
|
|||||||
console.log(error)
|
console.log(error)
|
||||||
this.$message.error(this.$t('contextmenu.copyFail'))
|
this.$message.error(this.$t('contextmenu.copyFail'))
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// AI续写
|
||||||
|
aiCreate() {
|
||||||
|
this.$bus.$emit('ai_create_part', this.node)
|
||||||
|
this.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -119,7 +119,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 740px) {
|
@media screen and (max-width: 900px) {
|
||||||
.countContainer {
|
.countContainer {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,6 +47,8 @@
|
|||||||
v-if="mindMap"
|
v-if="mindMap"
|
||||||
:mindMap="mindMap"
|
:mindMap="mindMap"
|
||||||
></NodeImgPlacementToolbar>
|
></NodeImgPlacementToolbar>
|
||||||
|
<AiCreate v-if="mindMap" :mindMap="mindMap"></AiCreate>
|
||||||
|
<AiChat></AiChat>
|
||||||
<div
|
<div
|
||||||
class="dragMask"
|
class="dragMask"
|
||||||
v-if="showDragMask"
|
v-if="showDragMask"
|
||||||
@ -133,6 +135,8 @@ import NodeTagStyle from './NodeTagStyle.vue'
|
|||||||
import Setting from './Setting.vue'
|
import Setting from './Setting.vue'
|
||||||
import AssociativeLineStyle from './AssociativeLineStyle.vue'
|
import AssociativeLineStyle from './AssociativeLineStyle.vue'
|
||||||
import NodeImgPlacementToolbar from './NodeImgPlacementToolbar.vue'
|
import NodeImgPlacementToolbar from './NodeImgPlacementToolbar.vue'
|
||||||
|
import AiCreate from './AiCreate.vue'
|
||||||
|
import AiChat from './AiChat.vue'
|
||||||
|
|
||||||
// 注册插件
|
// 注册插件
|
||||||
MindMap.usePlugin(MiniMap)
|
MindMap.usePlugin(MiniMap)
|
||||||
@ -187,7 +191,9 @@ export default {
|
|||||||
NodeTagStyle,
|
NodeTagStyle,
|
||||||
Setting,
|
Setting,
|
||||||
AssociativeLineStyle,
|
AssociativeLineStyle,
|
||||||
NodeImgPlacementToolbar
|
NodeImgPlacementToolbar,
|
||||||
|
AiCreate,
|
||||||
|
AiChat
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="navigatorContainer" :class="{ isDark: isDark }">
|
<div class="navigatorContainer customScrollbar" :class="{ isDark: isDark }">
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="lang"
|
v-model="lang"
|
||||||
@ -193,7 +193,8 @@ export default {
|
|||||||
url = 'https://wanglin2.github.io/mind-map-docs/help/help1.html'
|
url = 'https://wanglin2.github.io/mind-map-docs/help/help1.html'
|
||||||
break
|
break
|
||||||
case 'devDoc':
|
case 'devDoc':
|
||||||
url = 'https://wanglin2.github.io/mind-map-docs/start/introduction.html'
|
url =
|
||||||
|
'https://wanglin2.github.io/mind-map-docs/start/introduction.html'
|
||||||
break
|
break
|
||||||
case 'site':
|
case 'site':
|
||||||
url = 'https://wanglin2.github.io/mind-map-docs/'
|
url = 'https://wanglin2.github.io/mind-map-docs/'
|
||||||
@ -268,7 +269,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 590px) {
|
@media screen and (max-width: 700px) {
|
||||||
.navigatorContainer {
|
.navigatorContainer {
|
||||||
left: 20px;
|
left: 20px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="sidebarTriggerContainer"
|
class="sidebarTriggerContainer "
|
||||||
@click.stop
|
@click.stop
|
||||||
:class="{ hasActive: show && activeSidebar, show: show, isDark: isDark }"
|
:class="{ hasActive: show && activeSidebar, show: show, isDark: isDark }"
|
||||||
|
:style="{ maxHeight: maxHeight + 'px' }"
|
||||||
>
|
>
|
||||||
<div class="toggleShowBtn" :class="{ hide: !show }" @click="show = !show">
|
<div class="toggleShowBtn" :class="{ hide: !show }" @click="show = !show">
|
||||||
<span class="iconfont iconjiantouyou"></span>
|
<span class="iconfont iconjiantouyou"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="trigger">
|
<div class="trigger customScrollbar">
|
||||||
<div
|
<div
|
||||||
class="triggerItem"
|
class="triggerItem"
|
||||||
v-for="item in triggerList"
|
v-for="item in triggerList"
|
||||||
@ -35,7 +36,8 @@ export default {
|
|||||||
name: 'SidebarTrigger',
|
name: 'SidebarTrigger',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
show: true
|
show: true,
|
||||||
|
maxHeight: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -62,11 +64,28 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
window.addEventListener('resize', this.onResize)
|
||||||
|
this.updateSize()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.onResize)
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations(['setActiveSidebar']),
|
...mapMutations(['setActiveSidebar']),
|
||||||
|
|
||||||
trigger(item) {
|
trigger(item) {
|
||||||
this.setActiveSidebar(item.value)
|
this.setActiveSidebar(item.value)
|
||||||
|
},
|
||||||
|
|
||||||
|
onResize() {
|
||||||
|
this.updateSize()
|
||||||
|
},
|
||||||
|
|
||||||
|
updateSize() {
|
||||||
|
const topMargin = 110
|
||||||
|
const bottomMargin = 80
|
||||||
|
this.maxHeight = window.innerHeight - topMargin - bottomMargin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -75,11 +94,13 @@ export default {
|
|||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.sidebarTriggerContainer {
|
.sidebarTriggerContainer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
top: 110px;
|
||||||
|
bottom: 80px;
|
||||||
right: -60px;
|
right: -60px;
|
||||||
margin-top: 110px;
|
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
top: 50%;
|
display: flex;
|
||||||
transform: translateY(-50%);
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
&.isDark {
|
&.isDark {
|
||||||
.trigger {
|
.trigger {
|
||||||
@ -145,7 +166,9 @@ export default {
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
box-shadow: 0 2px 16px 0 rgba(0, 0, 0, 0.06);
|
box-shadow: 0 2px 16px 0 rgba(0, 0, 0, 0.06);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
max-height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
.triggerItem {
|
.triggerItem {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
|
|||||||
@ -195,7 +195,8 @@ export default {
|
|||||||
'formula',
|
'formula',
|
||||||
// 'attachment',
|
// 'attachment',
|
||||||
'outerFrame',
|
'outerFrame',
|
||||||
'annotation'
|
'annotation',
|
||||||
|
'ai'
|
||||||
],
|
],
|
||||||
horizontalList: [],
|
horizontalList: [],
|
||||||
verticalList: [],
|
verticalList: [],
|
||||||
|
|||||||
@ -184,6 +184,17 @@
|
|||||||
:dir="dir"
|
:dir="dir"
|
||||||
@setAnnotation="onSetAnnotation"
|
@setAnnotation="onSetAnnotation"
|
||||||
></NodeAnnotationBtn>
|
></NodeAnnotationBtn>
|
||||||
|
<div
|
||||||
|
v-if="item === 'ai'"
|
||||||
|
class="toolbarBtn"
|
||||||
|
:class="{
|
||||||
|
disabled: hasGeneralization
|
||||||
|
}"
|
||||||
|
@click="aiCrate"
|
||||||
|
>
|
||||||
|
<span class="icon iconfont iconAIshengcheng"></span>
|
||||||
|
<span class="text">{{ $t('toolbar.ai') }}</span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -299,6 +310,11 @@ export default {
|
|||||||
// 设置标记
|
// 设置标记
|
||||||
onSetAnnotation(...args) {
|
onSetAnnotation(...args) {
|
||||||
this.$bus.$emit('execCommand', 'SET_NOTATION', this.activeNodes, ...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 {
|
.text {
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -38,7 +38,14 @@ const store = new Vuex.Store({
|
|||||||
supportCheckbox: false, // 是否支持Checkbox插件
|
supportCheckbox: false, // 是否支持Checkbox插件
|
||||||
supportLineFlow: false, // 是否支持LineFlow插件
|
supportLineFlow: false, // 是否支持LineFlow插件
|
||||||
supportMomentum: false, // 是否支持Momentum插件
|
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: {
|
mutations: {
|
||||||
// 设置思维导图数据
|
// 设置思维导图数据
|
||||||
@ -53,11 +60,18 @@ const store = new Vuex.Store({
|
|||||||
|
|
||||||
// 设置本地配置
|
// 设置本地配置
|
||||||
setLocalConfig(state, data) {
|
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,
|
...state.localConfig,
|
||||||
...data
|
...state.aiConfig
|
||||||
}
|
})
|
||||||
storeLocalConfig(state.localConfig)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 设置当前显示的侧边栏
|
// 设置当前显示的侧边栏
|
||||||
|
|||||||
120
web/src/utils/ai.js
Normal file
120
web/src/utils/ai.js
Normal file
@ -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
|
||||||
@ -38,5 +38,13 @@ module.exports = {
|
|||||||
'@': path.resolve(__dirname, './src/')
|
'@': path.resolve(__dirname, './src/')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
proxy: {
|
||||||
|
'^/api/v3/': {
|
||||||
|
target: 'http://ark.cn-beijing.volces.com',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user