首页对话相关

This commit is contained in:
2026-08-14 11:25:13 +08:00
parent c088894546
commit 4ec05b507a
9 changed files with 830 additions and 179 deletions
+103 -11
View File
@@ -23,17 +23,21 @@
"codemirror": "^6.0.1",
"countup.js": "^2.8.0",
"cropperjs": "^1.6.0",
"dompurify": "^3.4.13",
"echarts": "^5.5.0",
"echarts-gl": "^2.0.9",
"echarts-wordcloud": "^2.1.0",
"element-china-area-data": "^6.1.0",
"element-plus": "^2.10.7",
"file-saver": "^2.0.5",
"highlight.js": "^11.12.0",
"js-cookie": "^3.0.5",
"json-schema-editor": "github:wusij/json-schema#8827d0d6a360c37bc32a8945fb979260bd4a4c85",
"jsplumb": "^2.15.6",
"jszip": "^3.10.1",
"lodash": "^4.17.21",
"marked": "^18.0.9",
"marked-highlight": "^2.2.4",
"mitt": "^3.0.1",
"nprogress": "^0.2.0",
"pinia": "^2.1.7",
@@ -266,6 +270,7 @@
"resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
"integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"vue": "^3.2.0"
}
@@ -929,6 +934,7 @@
"resolved": "https://registry.npmjs.org/@interactjs/core/-/core-1.10.27.tgz",
"integrity": "sha512-SliUr/3ZbLAdED8LokzYzWHWMdCB5Cq+UnpXuRy+BIod1j97m4IUFf/D1iIKUBBjBcucgXbz28z96WnenVCB7Q==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"@interactjs/utils": "1.10.27"
}
@@ -999,6 +1005,7 @@
"resolved": "https://registry.npmjs.org/@interactjs/modifiers/-/modifiers-1.10.27.tgz",
"integrity": "sha512-ei/qfoQ+9/8k6WzNzdNqHI6cWkIV576N4Ap16r5CoqOWwhA6Xzj3OMHf1g0t1O4eSq2HdJsVJn3eLNfw9HsbeQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@interactjs/snappers": "1.10.27"
},
@@ -1065,7 +1072,8 @@
"version": "1.10.27",
"resolved": "https://registry.npmjs.org/@interactjs/utils/-/utils-1.10.27.tgz",
"integrity": "sha512-+qfLOio2OxQqg1cXSnRaCl+N8MQDQLDS9w+aOGxH8YLAhIMyt7Asxx/46//sT8orgsi16pmlBPtngPHT9s8zKw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@intlify/core-base": {
"version": "11.1.2",
@@ -1157,6 +1165,7 @@
"resolved": "https://registry.npmjs.org/@logicflow/core/-/core-2.2.1.tgz",
"integrity": "sha512-VzLPrCrT4eXnOLjoGQ5v4GUSay3+6rd3YNZD0qOJw4vME5e4WjQ5fd+hKK2zlIzgdRI4D54dXiEFJrS6xdV6yQ==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"classnames": "^2.3.2",
"lodash-es": "^4.17.21",
@@ -1910,6 +1919,7 @@
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/lodash": "*"
}
@@ -1920,6 +1930,7 @@
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -1945,6 +1956,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.16",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
@@ -1991,6 +2009,7 @@
"integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "7.18.0",
"@typescript-eslint/types": "7.18.0",
@@ -2166,6 +2185,7 @@
"resolved": "https://registry.npmjs.org/@uppy/core/-/core-2.3.4.tgz",
"integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@transloadit/prettier-bytes": "0.0.7",
"@uppy/store-default": "^2.1.1",
@@ -2197,6 +2217,7 @@
"resolved": "https://registry.npmjs.org/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz",
"integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@uppy/companion-client": "^2.2.2",
"@uppy/utils": "^4.1.2",
@@ -2274,6 +2295,7 @@
"resolved": "https://registry.npmjs.org/@vue-flow/core/-/core-1.48.2.tgz",
"integrity": "sha512-raxhgKWE+G/mcEvXJjGFUDYW9rAI3GOtiHR3ZkNpwBWuIaCC1EYiBmKGwJOoNzVFgwO7COgErnK7i08i287AFA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vueuse/core": "^10.5.0",
"d3-drag": "^3.0.0",
@@ -2517,6 +2539,7 @@
"resolved": "https://registry.npmjs.org/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz",
"integrity": "sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==",
"license": "MIT",
"peer": true,
"dependencies": {
"is-url": "^1.2.4"
},
@@ -2549,6 +2572,7 @@
"resolved": "https://registry.npmjs.org/@wangeditor/core/-/core-1.1.19.tgz",
"integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/event-emitter": "^0.3.3",
"event-emitter": "^0.3.5",
@@ -2667,6 +2691,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2922,6 +2947,7 @@
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
@@ -3105,6 +3131,7 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
@@ -3244,10 +3271,20 @@
"resolved": "https://registry.npmjs.org/dom7/-/dom7-3.0.0.tgz",
"integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"ssr-window": "^3.0.0-alpha.1"
}
},
"node_modules/dompurify": {
"version": "3.4.13",
"resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.4.13.tgz",
"integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -3280,6 +3317,7 @@
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": "2.3.0",
"zrender": "5.6.1"
@@ -3320,6 +3358,7 @@
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.11.8.tgz",
"integrity": "sha512-2wzSj2uubFU1f0t/gHkkE1d09mUgV18fSZX5excw3Ar6hyWcxph4E57U8dgYLDt7HwkKYv1BiqPyBdy0WqWlOA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@ctrl/tinycolor": "^3.4.1",
"@element-plus/icons-vue": "^2.3.2",
@@ -3508,6 +3547,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -4170,6 +4210,15 @@
"node": ">= 0.4"
}
},
"node_modules/highlight.js": {
"version": "11.12.0",
"resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.12.0.tgz",
"integrity": "sha512-nbfWpyRMcMrPMmDwJB+dhX/eiaPKtc2RB+0QZskqJ3WjRA/FDS0e9hZrx8EC/lbEv8gXy98FcDbNa/dspAaJMg==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/hoist-non-react-statics": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz",
@@ -4300,7 +4349,8 @@
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.2.0.tgz",
"integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/is-number": {
"version": "7.0.0",
@@ -4498,13 +4548,15 @@
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lodash-unified": {
"version": "1.0.3",
@@ -4521,32 +4573,37 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lodash.foreach": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
"integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lodash.merge": {
"version": "4.6.2",
@@ -4559,13 +4616,15 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
"integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lodash.toarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
"integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/magic-string": {
"version": "0.30.21",
@@ -4576,6 +4635,28 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/marked": {
"version": "18.0.9",
"resolved": "https://registry.npmmirror.com/marked/-/marked-18.0.9.tgz",
"integrity": "sha512-/Sa4qiiHZxf0/FQdBBowr9q4r10krCwMvpK48FUBdXdUXScDxiQGR9zCPrFgRVR5LU3iySOiIjy09ZQvADir1w==",
"license": "MIT",
"peer": true,
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/marked-highlight": {
"version": "2.2.4",
"resolved": "https://registry.npmmirror.com/marked-highlight/-/marked-highlight-2.2.4.tgz",
"integrity": "sha512-PZxisNMJDduSjc0q6uvjsnqqHCXc9s0eyzxDO9sB1eNGJnd/H1/Fu+z6g/liC1dfJdFW4SftMwMlLvsBhUPrqQ==",
"license": "MIT",
"peerDependencies": {
"marked": ">=4 <19"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -4678,6 +4759,7 @@
"resolved": "https://registry.npmjs.org/mobx/-/mobx-5.15.7.tgz",
"integrity": "sha512-wyM3FghTkhmC+hQjyPGGFdpehrcX1KOXsDuERhfK2YbJemkUhEB+6wzEN639T21onxlfYBmriA1PFnvxTUhcKw==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mobx"
@@ -4742,6 +4824,7 @@
}
],
"license": "MIT",
"peer": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -5028,6 +5111,7 @@
"resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz",
"integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
@@ -5288,6 +5372,7 @@
"integrity": "sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
@@ -5506,6 +5591,7 @@
"resolved": "https://registry.npmjs.org/slate/-/slate-0.72.8.tgz",
"integrity": "sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==",
"license": "MIT",
"peer": true,
"dependencies": {
"immer": "^9.0.6",
"is-plain-object": "^5.0.0",
@@ -5529,6 +5615,7 @@
"resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.6.3.tgz",
"integrity": "sha512-W2lHLLw2qR2Vv0DcMmcxXqcfdBaIcoN+y/86SmHv8fn4DazEQSH6KN3TjZcWvwujW56OHiiirsbHWZb4vx/0fg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.17.0"
}
@@ -5694,6 +5781,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -5771,6 +5859,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -5840,6 +5929,7 @@
"integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -6008,6 +6098,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -6027,6 +6118,7 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz",
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.25",
"@vue/compiler-sfc": "3.5.25",
+4
View File
@@ -28,17 +28,21 @@
"codemirror": "^6.0.1",
"countup.js": "^2.8.0",
"cropperjs": "^1.6.0",
"dompurify": "^3.4.13",
"echarts": "^5.5.0",
"echarts-gl": "^2.0.9",
"echarts-wordcloud": "^2.1.0",
"element-china-area-data": "^6.1.0",
"element-plus": "^2.10.7",
"file-saver": "^2.0.5",
"highlight.js": "^11.12.0",
"js-cookie": "^3.0.5",
"json-schema-editor": "github:wusij/json-schema#8827d0d6a360c37bc32a8945fb979260bd4a4c85",
"jsplumb": "^2.15.6",
"jszip": "^3.10.1",
"lodash": "^4.17.21",
"marked": "^18.0.9",
"marked-highlight": "^2.2.4",
"mitt": "^3.0.1",
"nprogress": "^0.2.0",
"pinia": "^2.1.7",
+429 -20
View File
@@ -8,10 +8,41 @@
>
<div class="bubble-wrap">
<div class="bubble">
<template v-if="msg.loading">
<span class="loading-text">思考中</span><span class="loading-dots"><i></i><i></i><i></i></span>
<template v-if="msg.isUser">{{ msg.content }}</template>
<template v-else>
<!-- 初始加载动画 -->
<template v-if="msg.loading && !msg.thinking && !msg.content">
<span class="loading-text">思考中</span><span class="loading-dots"><i></i><i></i><i></i></span>
</template>
<!-- 思考区DeepSeek 风格折叠块 -->
<div v-if="msg.thinking" class="thinking-block">
<button class="thinking-toggle" type="button" @click="toggleThinking(msg)">
<span class="thinking-icon">🧠</span>
<span class="thinking-title">{{ thinkingTitle(msg) }}</span>
<span class="thinking-arrow" :class="{ collapsed: !isThinkingExpanded(msg) }"></span>
</button>
<div v-if="isThinkingExpanded(msg)" class="thinking-content">
<div class="thinking-text">{{ msg.thinking }}</div>
</div>
</div>
<!-- 回答区Markdown 渲染 -->
<div
v-if="msg.content"
class="answer-block"
v-html="renderAnswer(msg.content)"
@click="handleAnswerClick"
></div>
<!-- 失败重试 -->
<button v-if="isErrorMsg(msg)" type="button" class="retry-btn" @click="emit('retry', msg)">
<span class="retry-icon"></span> 重新生成
</button>
</template>
<template v-else>{{ msg.content }}</template>
</div>
<!-- AI 消息操作栏hover 显示 -->
<div v-if="!msg.isUser && msg.content && !isErrorMsg(msg)" class="msg-actions">
<button class="msg-action" title="复制回答" @click="copyText(msg.content)">
<el-icon><DocumentCopy /></el-icon>
</button>
</div>
<div class="time">{{ msg.time }}</div>
</div>
@@ -20,7 +51,11 @@
</template>
<script setup lang="ts">
import { nextTick, ref, watch } from 'vue';
import { nextTick, reactive, ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { DocumentCopy } from '@element-plus/icons-vue';
import 'highlight.js/styles/github-dark.css';
import { renderMarkdown } from '../utils/markdown';
interface ChatMessage {
id: string;
@@ -28,6 +63,13 @@ interface ChatMessage {
time: string;
isUser: boolean;
loading?: boolean;
thinking?: string;
thinkingSeconds?: number;
retryQuestion?: string;
}
interface Emits {
(e: 'retry', msg: ChatMessage): void;
}
interface Props {
@@ -35,13 +77,63 @@ interface Props {
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const chatListRef = ref<HTMLElement | null>(null);
const isErrorMsg = (msg: ChatMessage): boolean => !msg.isUser && String(msg.content || '').startsWith('❌');
// 思考区折叠:未手动操作时,流式中展开、完成(出现答案)后自动收起(DeepSeek 形态)
const thinkingExpanded = reactive<Record<string, boolean>>({});
const isThinkingExpanded = (msg: ChatMessage): boolean =>
msg.id in thinkingExpanded ? thinkingExpanded[msg.id] : !msg.content;
const toggleThinking = (msg: ChatMessage) => {
thinkingExpanded[msg.id] = !isThinkingExpanded(msg);
};
// Markdown 渲染缓存(content 在作答完成后固定,避免重复解析)
const mdCache = new Map<string, string>();
const renderAnswer = (content: string): string => {
if (!mdCache.has(content)) mdCache.set(content, renderMarkdown(content));
return mdCache.get(content)!;
};
// 复制文本(剪贴板 API + 兼容回退)
const copyText = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
} catch {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
ElMessage.success('已复制');
};
// 代码块复制(事件委托:点击渲染出的 .code-copy-btn
const handleAnswerClick = (e: MouseEvent) => {
const el = e.target as HTMLElement;
const btn = el.closest('.code-copy-btn');
if (!btn) return;
const code = btn.closest('.code-wrapper')?.querySelector('code');
if (code) copyText(code.textContent || '');
};
// 思考区折叠标题:完成(有答案)显示"已深度思考(用时 X 秒)",流式中显示"思考中…"
const thinkingTitle = (msg: ChatMessage): string => {
if (!msg.content) return '思考中…';
return msg.thinkingSeconds != null ? `已深度思考(用时 ${msg.thinkingSeconds} 秒)` : '已深度思考';
};
// 消息新增或内容变化时自动滚动到底部
watch(
() => props.messages.map((m) => `${m.id}|${m.content}|${m.loading ? 1 : 0}`).join('\n'),
() => props.messages.map((m) => `${m.id}|${m.content}|${m.thinking || ''}|${m.loading ? 1 : 0}`).join('\n'),
() => {
nextTick(() => {
const el = chatListRef.value;
@@ -79,35 +171,352 @@ watch(
display: flex;
flex-direction: column;
gap: 5px;
max-width: min(84%, 1120px);
max-width: 100%;
align-items: flex-start;
.message-row.is-user & {
align-items: flex-end;
/* 明确宽度基准:打断 fit-content 与百分比 max-width 的循环,避免气泡被压得过窄导致文字竖排 */
width: 100%;
}
}
.bubble {
font-size: 14px;
font-size: 15px;
line-height: 1.75;
padding: 14px 16px;
border-radius: 16px;
padding: 0;
border-radius: 0;
word-break: break-word;
color: #1f2937;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.96) 0%, rgba(248, 250, 255, 0.92) 100%);
border: 1px solid rgba(226, 233, 244, 0.95);
backdrop-filter: blur(8px);
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.05);
color: #1f2328;
background: transparent;
border: none;
box-shadow: none;
.message-row.is-user & {
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 50%, #2563eb 100%);
border-color: rgba(59, 130, 246, 0.4);
color: #fff;
box-shadow: 0 8px 24px rgba(37, 99, 235, 0.35);
width: fit-content;
max-width: 85%;
padding: 10px 14px;
border-radius: 16px 16px 4px 16px;
background: #d3f5d8;
color: #1f2328;
box-shadow: none;
}
.message-row.is-error & {
background: #fef2f2;
border-color: rgba(254, 226, 226, 0.95);
color: #dc2626;
}
}
/* AI 消息操作栏(hover 显示) */
.msg-actions {
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 0.15s;
.message-row:hover & {
opacity: 1;
}
}
.msg-action {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
border-radius: 6px;
background: transparent;
color: #9ca3af;
cursor: pointer;
transition: background 0.15s, color 0.15s;
&:hover {
background: #f1f2f4;
color: #4b5563;
}
.el-icon {
font-size: 14px;
}
}
/* 失败重试 */
.retry-btn {
display: inline-flex;
align-items: center;
gap: 5px;
margin-top: 10px;
padding: 5px 12px;
border: 1px solid #fecaca;
border-radius: 8px;
background: #fef2f2;
color: #dc2626;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
&:hover {
background: #fee2e2;
border-color: #fca5a5;
}
.retry-icon {
font-size: 14px;
line-height: 1;
}
}
/* 思考区:DeepSeek 风格折叠块 */
.thinking-block {
margin: 2px 0 10px;
background: #f5f5f6;
border-radius: 10px;
overflow: hidden;
}
.thinking-toggle {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
padding: 8px 12px;
border: none;
background: transparent;
font-size: 13px;
font-weight: 500;
color: #8b8b98;
cursor: pointer;
text-align: left;
user-select: none;
transition: color 0.15s;
&:hover {
color: #6b7280;
}
}
.thinking-icon {
font-size: 14px;
line-height: 1;
}
.thinking-title {
font-size: 13px;
}
.thinking-arrow {
margin-left: auto;
display: inline-flex;
font-size: 11px;
line-height: 1;
opacity: 0.7;
transition: transform 0.2s;
&.collapsed {
transform: rotate(-90deg);
}
}
.thinking-content {
padding: 0 12px 12px;
animation: thinking-fade-in 0.2s ease;
}
.thinking-text {
font-size: 13px;
line-height: 1.7;
color: #9a9aab;
font-style: italic;
white-space: pre-wrap;
word-break: break-word;
border-top: 1px solid #ececee;
padding-top: 10px;
}
@keyframes thinking-fade-in {
from {
opacity: 0;
transform: translateY(-2px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 回答区:Markdown 渲染(DeepSeek 风格排版) */
.answer-block {
white-space: normal;
line-height: 1.75;
font-size: 15px;
word-break: break-word;
& > :first-child {
margin-top: 0;
}
& > :last-child {
margin-bottom: 0;
}
:deep(p) {
margin: 0 0 12px;
}
:deep(h1),
:deep(h2),
:deep(h3),
:deep(h4),
:deep(h5),
:deep(h6) {
font-weight: 600;
line-height: 1.4;
margin: 20px 0 10px;
color: #1f2328;
}
:deep(h1) {
font-size: 20px;
border-bottom: 1px solid #eaecef;
padding-bottom: 8px;
}
:deep(h2) {
font-size: 18px;
}
:deep(h3) {
font-size: 16px;
}
:deep(h4),
:deep(h5),
:deep(h6) {
font-size: 15px;
}
:deep(ul),
:deep(ol) {
margin: 0 0 12px;
padding-left: 22px;
}
:deep(li) {
margin: 3px 0;
}
:deep(li > ul),
:deep(li > ol) {
margin-bottom: 0;
}
:deep(blockquote) {
margin: 0 0 12px;
padding: 6px 14px;
border-left: 4px solid #e0e0e0;
color: #6a737d;
background: #fafafa;
border-radius: 0 8px 8px 0;
}
:deep(code) {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 13px;
padding: 2px 6px;
border-radius: 4px;
background: #f2f3f5;
color: #c7254e;
}
:deep(.code-wrapper) {
position: relative;
margin: 0 0 14px;
&:hover .code-copy-btn {
opacity: 1;
}
}
:deep(.code-copy-btn) {
position: absolute;
top: 8px;
right: 8px;
z-index: 2;
padding: 2px 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
background: rgba(255, 255, 255, 0.08);
color: #8b949e;
font-size: 12px;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s, background 0.15s;
&:hover {
background: rgba(255, 255, 255, 0.16);
color: #c9d1d9;
}
}
:deep(pre) {
margin: 0;
padding: 12px 14px;
border-radius: 10px;
background: #0d1117;
overflow-x: auto;
line-height: 1.6;
code {
display: block;
padding: 0;
background: transparent;
color: inherit;
font-size: 13px;
}
}
:deep(table) {
width: 100%;
margin: 0 0 14px;
border-collapse: collapse;
font-size: 14px;
th,
td {
border: 1px solid #e2e8f0;
padding: 6px 12px;
text-align: left;
}
th {
background: #f5f7fa;
font-weight: 600;
}
}
:deep(a) {
color: #2563eb;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
:deep(hr) {
border: none;
border-top: 1px solid #eaecef;
margin: 16px 0;
}
:deep(img) {
max-width: 100%;
border-radius: 6px;
}
}
/* 执行中占位 */
.loading-text {
color: #94a3b8;
+2 -2
View File
@@ -177,7 +177,7 @@ onMounted(() => {
}
.input-card {
max-width: 860px;
max-width: 800px;
margin: 0 auto;
background: #fff;
border: 1.5px solid #e2e8f0;
@@ -347,7 +347,7 @@ onMounted(() => {
/* 快捷工作流胶囊 */
.workflow-shortcuts {
max-width: 860px;
max-width: 800px;
margin: 10px auto 0;
display: flex;
flex-wrap: wrap;
+97 -120
View File
@@ -131,71 +131,42 @@
<!-- 对话页 — 无工作流但有消息时展示消息流 -->
<div v-else-if="hasMessages" class="content-body chat-body">
<ChatList :messages="messages" />
<ChatList :messages="messages" @retry="emit('retry', $event)" />
</div>
<!-- 默认占位 — 无工作流时显示引导 -->
<div v-else class="content-body placeholder-body">
<div class="placeholder-content">
<div class="placeholder-icon">
<div class="icon-ring ring-outer">
<div class="icon-ring ring-inner">
<div class="icon-ring ring-core">
<svg class="icon-sparkle" width="28" height="28" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2l1.09 6.26L18 4.5l-3.41 4.74L20 10l-5.41.76L18 15.5l-4.91-2.76L12 19l-1.09-6.26L6 15.5l3.41-4.74L4 10l5.41-.76L6 4.5l4.91 2.74L12 2z" fill="currentColor"/>
</svg>
</div>
</div>
</div>
</div>
<h2 class="placeholder-title">你好,我能帮你解决什么问题?</h2>
<p class="placeholder-desc">选择一个工作流,填写参数后一键生成</p>
<h2 class="placeholder-title">请选择下方工作流进行创作</h2>
<p class="placeholder-desc">点击工作流胶囊快速切换填写参数后一键生成内容</p>
<div class="placeholder-steps">
<div class="step-item">
<div class="step-num">1</div>
<div class="step-text">
<span class="step-label">选择工作流</span>
<span class="step-hint">点击胶囊切换不同创作模式</span>
</div>
</div>
<div class="step-arrow">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
</div>
<div class="step-item">
<div class="step-num">2</div>
<div class="step-text">
<span class="step-label">填写参数</span>
<span class="step-hint">按需配置内容与样式选项</span>
</div>
</div>
<div class="step-arrow">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
</div>
<div class="step-item">
<div class="step-num">3</div>
<div class="step-text">
<span class="step-label">一键生成</span>
<span class="step-hint">AI 自动产出结果同步到工作空间</span>
</div>
</div>
</div>
<div class="placeholder-footer">
<span class="footer-hint">还可从左侧对话记录查看历史或工作空间管理作品</span>
<div v-if="placeWorkflows.length" class="workflow-cards">
<button
v-for="wf in placeWorkflows"
:key="wf.id"
type="button"
class="workflow-card"
@click="handlePlaceSelect(wf)"
>
<el-icon class="wf-card-icon"><Promotion /></el-icon>
<span class="wf-card-name">{{ wf.name }}</span>
<span v-if="wf.isTemplate" class="wf-card-tag">模板</span>
</button>
</div>
<div v-else class="placeholder-empty">暂无工作流,可在工作流管理中创建</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue';
import { computed, onMounted, reactive, ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
import PatchTemplateEditor from '/@/components/patchTemplate/PatchTemplateEditor.vue';
import { uploadFile } from '/@/api/common/upload';
import { collectHomeFormFields } from '../utils/flowDsl';
import { getWorkflowList } from '/@/api/settings/creation';
import { Promotion } from '@element-plus/icons-vue';
import ChatList from './ChatList.vue';
interface Props {
@@ -205,10 +176,16 @@ interface Props {
messages?: any[];
}
interface Emits {
(e: 'retry', msg: any): void;
(e: 'workflow-select', id: string, isTemplate?: boolean): void;
}
const props = withDefaults(defineProps<Props>(), {
activeHistoryId: null,
messages: () => [],
});
const emit = defineEmits<Emits>();
const hasMessages = computed(() => Array.isArray(props.messages) && props.messages.length > 0);
const formValues = reactive<Record<string, any>>({});
@@ -427,6 +404,28 @@ watch(
},
{ immediate: true }
);
// 占位页工作流卡片(DeepSeek 式引导:居中标题 + 工作流卡片)
const placeWorkflows = ref<any[]>([]);
const loadPlaceWorkflows = async () => {
try {
const res: any = await getWorkflowList();
const userList = res.data?.listFlowUserRes?.list || [];
const tplList = res.data?.listFlowTemplateRes?.list || [];
placeWorkflows.value = [
...userList.map((w: any) => ({ id: String(w.id), name: w.flowName || '未命名', isTemplate: false })),
...tplList.map((w: any) => ({ id: String(w.id), name: w.flowTemplateName || '未命名', isTemplate: true })),
];
} catch {
placeWorkflows.value = [];
}
};
const handlePlaceSelect = (wf: any) => {
emit('workflow-select', wf.id, wf.isTemplate);
};
onMounted(() => {
loadPlaceWorkflows();
});
defineExpose({ formValues, fieldFiles, templates, validateFormFields });
</script>
@@ -447,7 +446,7 @@ defineExpose({ formValues, fieldFiles, templates, validateFormFields });
/* ===== 工作流表单 ===== */
.workflow-form-body {
width: min(820px, 100%);
width: min(800px, 100%);
margin: 0 auto;
height: 100%;
display: flex;
@@ -570,12 +569,12 @@ defineExpose({ formValues, fieldFiles, templates, validateFormFields });
/* ===== 对话页 ===== */
.chat-body {
width: min(1060px, 82%);
width: min(800px, 92%);
margin: 0 auto;
height: 100%;
display: flex;
flex-direction: column;
padding: 0 20px;
padding: 0 16px;
box-sizing: border-box;
overflow-y: auto;
scrollbar-width: none;
@@ -583,84 +582,62 @@ defineExpose({ formValues, fieldFiles, templates, validateFormFields });
}
.placeholder-body { display: flex; align-items: center; justify-content: center; }
.placeholder-content { text-align: center; padding: 20px; max-width: 480px; }
/* ===== 图标:层叠圆环 + 星形 ===== */
.placeholder-icon { margin-bottom: 28px; display: flex; justify-content: center; }
.icon-ring {
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.3s ease;
}
.ring-outer {
width: 88px; height: 88px;
background: radial-gradient(circle at 35% 30%, rgba(59, 130, 246, 0.10) 0%, rgba(139, 92, 246, 0.06) 100%);
border: 1.5px solid rgba(59, 130, 246, 0.15);
&:hover { transform: scale(1.05); }
}
.ring-inner {
width: 62px; height: 62px;
background: radial-gradient(circle at 35% 30%, rgba(59, 130, 246, 0.14) 0%, rgba(139, 92, 246, 0.08) 100%);
border: 1.5px solid rgba(59, 130, 246, 0.20);
}
.ring-core {
width: 42px; height: 42px;
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.35);
}
.icon-sparkle { color: #fff; }
.placeholder-content { text-align: center; padding: 24px 20px; width: 100%; max-width: 640px; }
/* ===== 标题与描述 ===== */
.placeholder-title { font-size: 18px; font-weight: 700; color: #0f172a; margin: 0 0 6px; letter-spacing: -0.2px; line-height: 1.4; }
.placeholder-desc { font-size: 14px; color: #94a3b8; margin: 0 0 32px; line-height: 1.5; }
.placeholder-title { font-size: 20px; font-weight: 700; color: #0f172a; margin: 0 0 8px; letter-spacing: -0.2px; line-height: 1.4; }
.placeholder-desc { font-size: 14px; color: #94a3b8; margin: 0 0 28px; line-height: 1.5; }
/* ===== 步骤引导 ===== */
.placeholder-steps {
display: flex;
align-items: center;
justify-content: center;
gap: 0;
margin-bottom: 28px;
/* ===== 工作流功能卡片(DeepSeek 式引导 ===== */
.workflow-cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
text-align: left;
}
.step-item {
.workflow-card {
display: flex;
align-items: center;
gap: 10px;
text-align: left;
}
.step-num {
width: 28px; height: 28px;
border-radius: 50%;
background: linear-gradient(135deg, #eff6ff 0%, #f0f0ff 100%);
border: 1.5px solid #bfdbfe;
color: #3b82f6;
font-size: 13px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s;
.step-item:hover & {
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
color: #fff;
border-color: transparent;
padding: 14px 16px;
border: 1px solid #e2e8f0;
border-radius: 12px;
background: #fff;
cursor: pointer;
transition: all 0.15s;
outline: none;
&:hover {
border-color: #93c5fd;
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.12);
transform: translateY(-1px);
}
}
.step-text { display: flex; flex-direction: column; gap: 1px; }
.step-label { font-size: 13px; font-weight: 600; color: #334155; line-height: 1.3; white-space: nowrap; }
.step-hint { font-size: 11px; color: #94a3b8; line-height: 1.3; white-space: nowrap; }
.step-arrow {
color: #cbd5e1;
display: flex;
align-items: center;
margin: 0 6px;
.wf-card-icon {
font-size: 18px;
color: #3b82f6;
flex-shrink: 0;
}
/* ===== 底部辅助提示 ===== */
.placeholder-footer { margin-top: 4px; }
.footer-hint { font-size: 12px; color: #cbd5e1; line-height: 1.5; }
.wf-card-name {
flex: 1;
font-size: 14px;
font-weight: 600;
color: #1e293b;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.wf-card-tag {
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
background: #fff7e6;
color: #d97706;
border: 1px solid #fde68a;
white-space: nowrap;
}
.placeholder-empty {
font-size: 13px;
color: #cbd5e1;
padding: 24px 0;
}
</style>
+101 -26
View File
@@ -21,6 +21,8 @@
:workflow-detail="selectedWorkflowDetail"
:active-history-id="activeHistoryId"
:messages="currentMessages"
@retry="handleRetry"
@workflow-select="handlePlaceWorkflowSelect"
/>
<InputBar ref="inputBarRef" :send-disabled="isSendDisabled" :workflow-locked="isHistoryWorkflow" @send="handleSend" @workflow-select="handleWorkflowSelect" />
</div>
@@ -60,6 +62,7 @@ import TemplateCompleteDialog from './components/TemplateCompleteDialog.vue';
import { applyHomeFormValues } from './utils/flowDsl';
import { getChatModel } from '/@/api/settings/modelConfigV2';
import { openWsExecute } from './utils/wsExecute';
import { parseWsMessage, getDelta, getAnswer, getErrorText } from './utils/wsMessage';
import { getSessionMessages } from '/@/api/settings/session';
import type { ExecutionTreeItem } from '/@/api/settings/creation';
import {
@@ -86,6 +89,9 @@ interface ChatMessage {
time: string;
isUser: boolean;
loading?: boolean;
thinking?: string;
thinkingSeconds?: number;
retryQuestion?: string;
}
interface TreeNode {
@@ -331,6 +337,15 @@ const handleTemplateSaved = async (newId: string) => {
ib?.selectWorkflow?.(newId);
};
// 占位页工作流卡片点击:模板走补全弹窗;用户工作流同步选中输入框,进入表单页
const handlePlaceWorkflowSelect = (id: string, isTemplate?: boolean) => {
if (isTemplate) {
handleWorkflowSelect(id, true);
} else {
inputBarRef.value?.selectWorkflow?.(id);
}
};
const handleMenuChange = (menu: string) => {
activeMenu.value = menu;
};
@@ -427,6 +442,47 @@ const handleSend = async (message: string) => {
}
};
// ===== 失败重试:重新发送同一条用户消息 =====
const handleRetry = async (msg: ChatMessage) => {
let sid = '';
for (const [k, list] of sessionMessages.value) {
if (list.some((m) => m.id === msg.id)) {
sid = k;
break;
}
}
if (!sid || sendingSessions[sid]) return;
const session = historyList.value.find((h) => h.id === sid);
if (!session) return;
const list = sessionMessages.value.get(sid) || [];
const idx = list.findIndex((m) => m.id === msg.id);
if (idx < 0) return;
// 待重发的用户问题:优先失败消息上记录的原文,其次向上找最近一条用户消息
let question = msg.retryQuestion || '';
if (!question) {
for (let i = idx - 1; i >= 0; i--) {
if (list[i].isUser) {
question = list[i].content;
break;
}
}
}
if (!question) return;
// 移除失败消息,重新执行
list.splice(idx, 1);
saveChatCache(sid, list);
sendingSessions[sid] = true;
session.status = 'executing';
const sessionId = session.sessionId || getSessionId();
try {
await runChat(sid, sessionId, question);
} catch {
session.status = 'failed';
} finally {
delete sendingSessions[sid];
}
};
// ===== 工作流执行:选中工作流 → 表单页 WS 执行 → 完成后切回对话页 =====
const runWorkflow = async (sid: string, sessionId: string, message: string, mc: any) => {
const curSession = historyList.value.find((h) => h.id === sid);
@@ -551,6 +607,8 @@ const runChat = async (sid: string, sessionId: string, message: string) => {
let done = false;
let failed = false;
// 思考/生成过程计时起点(收到首帧时开始)
let thinkStart = 0;
const finishChat = (success: boolean, errorMsg?: string) => {
if (done) return;
done = true;
@@ -561,6 +619,8 @@ const runChat = async (sid: string, sessionId: string, message: string) => {
if (list && aiIdx >= 0) {
list[aiIdx].loading = false;
list[aiIdx].content = content;
list[aiIdx].thinking = '';
list[aiIdx].retryQuestion = message;
} else {
addMessage({ id: 'msg-' + Date.now() + '-fail', content, time: formatTime(new Date()), isUser: false });
}
@@ -612,28 +672,50 @@ const runChat = async (sid: string, sessionId: string, message: string) => {
question: message || '',
systemPrompt: '',
onMessage: (raw) => {
let data: any = raw;
if (typeof raw === 'string') {
try { data = JSON.parse(raw); } catch { data = raw; }
}
// 提取回复文本(兼容常见字段)
let text = '';
if (typeof data === 'string') text = data;
else if (data && typeof data === 'object') {
text = data.content ?? data.message ?? data.reply ?? data.text ?? data.answer ?? '';
}
const str = text ? text : typeof data === 'string' ? data : JSON.stringify(data || '');
const msg = parseWsMessage(raw);
if (!msg) return;
const list = sessionMessages.value.get(sid);
const aiIdx = list ? list.findIndex((m) => m.id === aiMsgId) : -1;
if (aiIdx >= 0 && str) {
const ai = list[aiIdx];
if (ai.loading) ai.loading = false;
ai.content = ai.content ? ai.content + (str.startsWith(' ') ? str : '\n' + str) : str;
saveChatCache(sid, list || []);
const ai = aiIdx >= 0 && list ? list[aiIdx] : null;
// 思考步骤:思考区为空时给出占位提示(虚化)
if (msg.type === 'agent_step') {
if (ai && !ai.thinking && !ai.content) {
ai.loading = false;
ai.thinking = '思考中…';
}
if (!thinkStart) thinkStart = Date.now();
return;
}
if (/done|finish|complete|end|success/i.test(str)) {
// 流式 token:思考区逐字累加(虚化显示)
if (msg.type === 'agent_token') {
const delta = getDelta(msg);
if (ai && delta) {
ai.loading = false;
ai.thinking = ai.thinking && ai.thinking !== '思考中…' ? ai.thinking + delta : delta;
saveChatCache(sid, list || []);
}
if (!thinkStart) thinkStart = Date.now();
return;
}
// 作答完成:回答区输出完整答案,记录思考用时并结束
if (msg.type === 'agent_answer') {
const answer = getAnswer(msg);
if (ai && answer) {
ai.loading = false;
ai.content = answer;
ai.thinkingSeconds = thinkStart ? Math.max(1, Math.round((Date.now() - thinkStart) / 1000)) : undefined;
saveChatCache(sid, list || []);
}
finishChat(true);
return;
}
// 错误:展示错误信息并结束
if (msg.type === 'error') {
finishChat(false, getErrorText(msg));
return;
}
// 其余(ack / agent / 未知)忽略
},
onError: () => {
failed = true;
@@ -784,12 +866,7 @@ onMounted(() => {
bottom: 0;
display: flex;
overflow: hidden;
background:
radial-gradient(1200px 600px at 0% 0%, rgba(59, 130, 246, 0.18) 0%, rgba(59, 130, 246, 0) 45%),
radial-gradient(1000px 500px at 100% 0%, rgba(139, 92, 246, 0.15) 0%, rgba(139, 92, 246, 0) 50%),
radial-gradient(800px 400px at 0% 100%, rgba(236, 72, 153, 0.12) 0%, rgba(236, 72, 153, 0) 55%),
radial-gradient(600px 300px at 100% 100%, rgba(16, 185, 129, 0.1) 0%, rgba(16, 185, 129, 0) 60%),
linear-gradient(135deg, #f8fbff 0%, #e6f0ff 60%, #f0f4fa 100%);
background: linear-gradient(180deg, #f7f7f8 0%, #ffffff 100%);
}
.main-wrapper {
@@ -805,9 +882,7 @@ onMounted(() => {
position: absolute;
inset: 0;
pointer-events: none;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.35) 0%, rgba(255, 255, 255, 0) 32%),
radial-gradient(900px 360px at 15% 115%, rgba(99, 102, 241, 0.06) 0%, rgba(99, 102, 241, 0) 70%);
background: transparent;
}
}
+22
View File
@@ -0,0 +1,22 @@
// ===== 首页回答区 Markdown 渲染(DeepSeek 风格)=====
// marked 负责 Markdown 语法,自定义 renderer.code 用 highlight.js 做代码语法高亮,
// 并为代码块包一层带"复制"按钮的结构;DOMPurify 消毒避免 v-html 注入 XSS。
import { marked } from 'marked';
import hljs from 'highlight.js/lib/common';
import DOMPurify from 'dompurify';
const renderer = new marked.Renderer();
renderer.code = ({ text, lang }) => {
const language = lang && hljs.getLanguage(lang) ? lang : 'plaintext';
const highlighted = hljs.highlight(text, { language }).value;
return `<div class="code-wrapper"><button type="button" class="code-copy-btn" data-copy>复制</button><pre><code class="hljs language-${language}">${highlighted}</code></pre></div>`;
};
marked.use({ renderer });
// gfm:GitHub 风格(表格、任务列表等);breaks:单换行转 <br>,贴近对话输出
marked.setOptions({ gfm: true, breaks: true });
/** 将 Markdown 渲染为安全 HTML(含代码高亮与复制按钮,已消毒) */
export function renderMarkdown(content: string): string {
return DOMPurify.sanitize(marked.parse(content) as string);
}
+9
View File
@@ -56,7 +56,16 @@ export function openWsExecute(opts: WsExecuteOptions): WebSocket | null {
ws.onopen = () => {
if (opts.flowContent) {
// 工作流执行:连接后发送 flowContentworkflow_exec 的完整 payload 结构待后端确认)
ws.send(JSON.stringify({ flowContent: opts.flowContent, systemPrompt: opts.systemPrompt || '' }));
} else {
// 普通对话:连接后需发送启动消息(type=agent),后端据此开始推流
ws.send(
JSON.stringify({
type: 'agent',
payload: { modelId: opts.modelId != null ? String(opts.modelId) : '', question: opts.question || '' },
})
);
}
opts.onOpen?.(ws);
};
+63
View File
@@ -0,0 +1,63 @@
// ===== 首页 WebSocket 流式消息解析工具 =====
// 服务端推送统一为 JSON 文本帧:
// {"type":"ack","message":"WebSocket连接成功"} 连接/流程确认(message 为提示文案)
// {"type":"agent","payload":{"modelId":"...","question":"..."}} agent 启动信息
// {"type":"agent_step","message":"模型思考中","data":{"maxStep":15,"step":1}} 思考步骤进度
// {"type":"agent_token","message":"思考中","data":{"delta":"你"}} 流式 token 增量
// {"type":"agent_answer","message":"作答完成","data":{"answer":"..."}} 作答完成(最终答案)
// {"type":"error","message":"..."} 执行失败
// 完成/失败判定均以 type 为准,不再用正则猜测文本。
export interface WsStreamMessage {
type: string;
message?: string;
data?: any;
payload?: any;
}
/**
* 解析服务端推送帧。
* 非 JSON、无 type 的帧返回 null(调用方忽略)。
*/
export function parseWsMessage(raw: any): WsStreamMessage | null {
let data: any = raw;
if (typeof raw === 'string') {
try {
data = JSON.parse(raw);
} catch {
return null;
}
}
if (!data || typeof data !== 'object' || typeof data.type !== 'string') return null;
return data;
}
/** agent_token → data.delta 流式增量 */
export function getDelta(msg: WsStreamMessage): string {
return typeof msg?.data?.delta === 'string' ? msg.data.delta : '';
}
/** agent_answer → data.answer 最终答案 */
export function getAnswer(msg: WsStreamMessage): string {
return typeof msg?.data?.answer === 'string' ? msg.data.answer : '';
}
/** agent_step → { step, maxStep },缺失时 maxStep 为 0 */
export function getStepInfo(msg: WsStreamMessage): { step: number; maxStep: number } | null {
const step = msg?.data?.step;
if (typeof step !== 'number') return null;
const maxStep = msg?.data?.maxStep;
return { step, maxStep: typeof maxStep === 'number' ? maxStep : 0 };
}
/** error → 错误文案(message 优先,其次 data.message / data.error / data.msg */
export function getErrorText(msg: WsStreamMessage): string {
if (typeof msg?.message === 'string' && msg.message) return msg.message;
const d = msg?.data;
if (typeof d === 'string') return d;
if (d && typeof d === 'object') {
const nested = d.message ?? d.error ?? d.msg;
if (typeof nested === 'string' && nested) return nested;
}
return '执行失败,请重试';
}