Commit 2a5514dd authored by nick zheng's avatar nick zheng

feat: 应用创建及发布

parent 51026394
node_modules node_modules
build build
dist dist
src/components/markdown-render/style
...@@ -24,6 +24,7 @@ export default [ ...@@ -24,6 +24,7 @@ export default [
AnyObject: 'readonly', AnyObject: 'readonly',
ConversationMessageItem: 'readonly', ConversationMessageItem: 'readonly',
ConversationMessageItemInfo: 'readonly', ConversationMessageItemInfo: 'readonly',
MittEvents: 'readonly',
}, },
parser: vueParser, parser: vueParser,
parserOptions: { parserOptions: {
......
...@@ -4,7 +4,8 @@ ...@@ -4,7 +4,8 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>POC-FE</title> <title>POC-FE</title>
</head> </head>
......
...@@ -17,9 +17,17 @@ ...@@ -17,9 +17,17 @@
}, },
"dependencies": { "dependencies": {
"@iconify/vue": "^4.1.2", "@iconify/vue": "^4.1.2",
"@microsoft/fetch-event-source": "^2.0.1",
"@traptitech/markdown-it-katex": "^3.6.0",
"@unocss/reset": "^0.61.3", "@unocss/reset": "^0.61.3",
"@vueuse/core": "^10.11.0", "@vueuse/core": "^10.11.0",
"axios": "^1.7.2", "axios": "^1.7.2",
"clipboardy": "^4.0.0",
"dayjs": "^1.11.13",
"highlight.js": "^11.10.0",
"markdown-it": "^14.1.0",
"markdown-it-link-attributes": "^4.0.1",
"mitt": "^3.0.1",
"nanoid": "^5.0.7", "nanoid": "^5.0.7",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.31", "vue": "^3.4.31",
...@@ -31,6 +39,8 @@ ...@@ -31,6 +39,8 @@
"@commitlint/config-conventional": "^19.2.2", "@commitlint/config-conventional": "^19.2.2",
"@commitlint/types": "^19.0.3", "@commitlint/types": "^19.0.3",
"@intlify/unplugin-vue-i18n": "^4.0.0", "@intlify/unplugin-vue-i18n": "^4.0.0",
"@types/markdown-it": "^14.1.2",
"@types/markdown-it-link-attributes": "^3.0.5",
"@types/node": "^20.14.10", "@types/node": "^20.14.10",
"@typescript-eslint/parser": "^7.15.0", "@typescript-eslint/parser": "^7.15.0",
"@unocss/eslint-config": "^0.61.3", "@unocss/eslint-config": "^0.61.3",
......
...@@ -11,6 +11,12 @@ importers: ...@@ -11,6 +11,12 @@ importers:
'@iconify/vue': '@iconify/vue':
specifier: ^4.1.2 specifier: ^4.1.2
version: 4.1.2(vue@3.4.31(typescript@5.5.3)) version: 4.1.2(vue@3.4.31(typescript@5.5.3))
'@microsoft/fetch-event-source':
specifier: ^2.0.1
version: 2.0.1
'@traptitech/markdown-it-katex':
specifier: ^3.6.0
version: 3.6.0
'@unocss/reset': '@unocss/reset':
specifier: ^0.61.3 specifier: ^0.61.3
version: 0.61.3 version: 0.61.3
...@@ -20,6 +26,24 @@ importers: ...@@ -20,6 +26,24 @@ importers:
axios: axios:
specifier: ^1.7.2 specifier: ^1.7.2
version: 1.7.2 version: 1.7.2
clipboardy:
specifier: ^4.0.0
version: 4.0.0
dayjs:
specifier: ^1.11.13
version: 1.11.13
highlight.js:
specifier: ^11.10.0
version: 11.10.0
markdown-it:
specifier: ^14.1.0
version: 14.1.0
markdown-it-link-attributes:
specifier: ^4.0.1
version: 4.0.1
mitt:
specifier: ^3.0.1
version: 3.0.1
nanoid: nanoid:
specifier: ^5.0.7 specifier: ^5.0.7
version: 5.0.7 version: 5.0.7
...@@ -48,6 +72,12 @@ importers: ...@@ -48,6 +72,12 @@ importers:
'@intlify/unplugin-vue-i18n': '@intlify/unplugin-vue-i18n':
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0(rollup@4.18.0)(vue-i18n@9.13.1(vue@3.4.31(typescript@5.5.3))) version: 4.0.0(rollup@4.18.0)(vue-i18n@9.13.1(vue@3.4.31(typescript@5.5.3)))
'@types/markdown-it':
specifier: ^14.1.2
version: 14.1.2
'@types/markdown-it-link-attributes':
specifier: ^3.0.5
version: 3.0.5
'@types/node': '@types/node':
specifier: ^20.14.10 specifier: ^20.14.10
version: 20.14.10 version: 20.14.10
...@@ -146,7 +176,7 @@ importers: ...@@ -146,7 +176,7 @@ importers:
version: 5.3.3(@types/node@20.14.10)(sass@1.77.6) version: 5.3.3(@types/node@20.14.10)(sass@1.77.6)
vite-plugin-checker: vite-plugin-checker:
specifier: ^0.7.1 specifier: ^0.7.1
version: 0.7.1(eslint@9.6.0)(optionator@0.9.4)(stylelint@16.6.1(typescript@5.5.3))(typescript@5.5.3)(vite@5.3.3(@types/node@20.14.10)(sass@1.77.6))(vue-tsc@2.0.26(typescript@5.5.3)) version: 0.7.1(eslint@9.6.0)(meow@13.2.0)(optionator@0.9.4)(stylelint@16.6.1(typescript@5.5.3))(typescript@5.5.3)(vite@5.3.3(@types/node@20.14.10)(sass@1.77.6))(vue-tsc@2.0.26(typescript@5.5.3))
vue-eslint-parser: vue-eslint-parser:
specifier: ^9.4.3 specifier: ^9.4.3
version: 9.4.3(eslint@9.6.0) version: 9.4.3(eslint@9.6.0)
...@@ -670,6 +700,9 @@ packages: ...@@ -670,6 +700,9 @@ packages:
'@juggle/resize-observer@3.4.0': '@juggle/resize-observer@3.4.0':
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
'@microsoft/fetch-event-source@2.0.1':
resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==}
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
...@@ -787,6 +820,9 @@ packages: ...@@ -787,6 +820,9 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@traptitech/markdown-it-katex@3.6.0':
resolution: {integrity: sha512-CnJzTWxsgLGXFdSrWRaGz7GZ1kUUi8g3E9HzJmeveX1YwVJavrKYqysktfHZQsujdnRqV5O7g8FPKEA/aeTkOQ==}
'@types/conventional-commits-parser@5.0.0': '@types/conventional-commits-parser@5.0.0':
resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==} resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==}
...@@ -796,12 +832,24 @@ packages: ...@@ -796,12 +832,24 @@ packages:
'@types/katex@0.16.7': '@types/katex@0.16.7':
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
'@types/linkify-it@5.0.0':
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
'@types/lodash-es@4.17.12': '@types/lodash-es@4.17.12':
resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
'@types/lodash@4.17.6': '@types/lodash@4.17.6':
resolution: {integrity: sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA==} resolution: {integrity: sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA==}
'@types/markdown-it-link-attributes@3.0.5':
resolution: {integrity: sha512-VZ2BGN3ywUg7mBD8W6PwR8ChpOxaQSBDbLqPgvNI+uIra3zY2af1eG/3XzWTKjEraTWskMKnZqZd6m1fDF67Bg==}
'@types/markdown-it@14.1.2':
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
'@types/mdurl@2.0.0':
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
'@types/node@20.14.10': '@types/node@20.14.10':
resolution: {integrity: sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==} resolution: {integrity: sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==}
...@@ -1170,6 +1218,10 @@ packages: ...@@ -1170,6 +1218,10 @@ packages:
resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==}
engines: {node: '>=18'} engines: {node: '>=18'}
clipboardy@4.0.0:
resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==}
engines: {node: '>=18'}
cliui@8.0.1: cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
...@@ -1293,6 +1345,9 @@ packages: ...@@ -1293,6 +1345,9 @@ packages:
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
engines: {node: '>=0.11'} engines: {node: '>=0.11'}
dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
de-indent@1.0.2: de-indent@1.0.2:
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
...@@ -1729,6 +1784,11 @@ packages: ...@@ -1729,6 +1784,11 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
hasBin: true hasBin: true
is-docker@3.0.0:
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
hasBin: true
is-extglob@2.1.1: is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
...@@ -1749,6 +1809,11 @@ packages: ...@@ -1749,6 +1809,11 @@ packages:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
is-inside-container@1.0.0:
resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
engines: {node: '>=14.16'}
hasBin: true
is-number@7.0.0: is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'} engines: {node: '>=0.12.0'}
...@@ -1781,6 +1846,14 @@ packages: ...@@ -1781,6 +1846,14 @@ packages:
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
engines: {node: '>=8'} engines: {node: '>=8'}
is-wsl@3.1.0:
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
engines: {node: '>=16'}
is64bit@2.0.0:
resolution: {integrity: sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==}
engines: {node: '>=18'}
isexe@2.0.0: isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
...@@ -1834,6 +1907,10 @@ packages: ...@@ -1834,6 +1907,10 @@ packages:
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
engines: {'0': node >= 0.2.0} engines: {'0': node >= 0.2.0}
katex@0.16.11:
resolution: {integrity: sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==}
hasBin: true
keyv@4.5.4: keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
...@@ -1858,6 +1935,9 @@ packages: ...@@ -1858,6 +1935,9 @@ packages:
lines-and-columns@1.2.4: lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
lint-staged@15.2.7: lint-staged@15.2.7:
resolution: {integrity: sha512-+FdVbbCZ+yoh7E/RosSdqKJyUM2OEjTciH0TFNkawKgvFp1zbGlEC39RADg+xKBG1R4mhoH2j85myBQZ5wR+lw==} resolution: {integrity: sha512-+FdVbbCZ+yoh7E/RosSdqKJyUM2OEjTciH0TFNkawKgvFp1zbGlEC39RADg+xKBG1R4mhoH2j85myBQZ5wR+lw==}
engines: {node: '>=18.12.0'} engines: {node: '>=18.12.0'}
...@@ -1929,12 +2009,22 @@ packages: ...@@ -1929,12 +2009,22 @@ packages:
magic-string@0.30.10: magic-string@0.30.10:
resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==}
markdown-it-link-attributes@4.0.1:
resolution: {integrity: sha512-pg5OK0jPLg62H4k7M9mRJLT61gUp9nvG0XveKYHMOOluASo9OEF13WlXrpAp2aj35LbedAy3QOCgQCw0tkLKAQ==}
markdown-it@14.1.0:
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
hasBin: true
mathml-tag-names@2.1.3: mathml-tag-names@2.1.3:
resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==}
mdn-data@2.0.30: mdn-data@2.0.30:
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
mdurl@2.0.0:
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
meow@12.1.1: meow@12.1.1:
resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==}
engines: {node: '>=16.10'} engines: {node: '>=16.10'}
...@@ -1980,6 +2070,9 @@ packages: ...@@ -1980,6 +2070,9 @@ packages:
minimist@1.2.8: minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
mlly@1.7.1: mlly@1.7.1:
resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==} resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==}
...@@ -2250,6 +2343,10 @@ packages: ...@@ -2250,6 +2343,10 @@ packages:
proxy-from-env@1.1.0: proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
punycode.js@2.3.1:
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
engines: {node: '>=6'}
punycode@2.3.1: punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
...@@ -2513,6 +2610,10 @@ packages: ...@@ -2513,6 +2610,10 @@ packages:
resolution: {integrity: sha512-7RnqIMq572L8PeEzKeBINYEJDDxpcH8JEgLwUqBd3TkofhFRbkq4QLR0u+36avGAhCRbk2nnmjcW9SE531hPDg==} resolution: {integrity: sha512-7RnqIMq572L8PeEzKeBINYEJDDxpcH8JEgLwUqBd3TkofhFRbkq4QLR0u+36avGAhCRbk2nnmjcW9SE531hPDg==}
engines: {node: ^14.18.0 || >=16.0.0} engines: {node: ^14.18.0 || >=16.0.0}
system-architecture@0.1.0:
resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==}
engines: {node: '>=18'}
table@6.8.2: table@6.8.2:
resolution: {integrity: sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==} resolution: {integrity: sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
...@@ -2581,6 +2682,9 @@ packages: ...@@ -2581,6 +2682,9 @@ packages:
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
ufo@1.5.3: ufo@1.5.3:
resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==}
...@@ -3432,6 +3536,8 @@ snapshots: ...@@ -3432,6 +3536,8 @@ snapshots:
'@juggle/resize-observer@3.4.0': {} '@juggle/resize-observer@3.4.0': {}
'@microsoft/fetch-event-source@2.0.1': {}
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
dependencies: dependencies:
'@nodelib/fs.stat': 2.0.5 '@nodelib/fs.stat': 2.0.5
...@@ -3504,6 +3610,10 @@ snapshots: ...@@ -3504,6 +3610,10 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.18.0': '@rollup/rollup-win32-x64-msvc@4.18.0':
optional: true optional: true
'@traptitech/markdown-it-katex@3.6.0':
dependencies:
katex: 0.16.11
'@types/conventional-commits-parser@5.0.0': '@types/conventional-commits-parser@5.0.0':
dependencies: dependencies:
'@types/node': 20.14.10 '@types/node': 20.14.10
...@@ -3512,12 +3622,25 @@ snapshots: ...@@ -3512,12 +3622,25 @@ snapshots:
'@types/katex@0.16.7': {} '@types/katex@0.16.7': {}
'@types/linkify-it@5.0.0': {}
'@types/lodash-es@4.17.12': '@types/lodash-es@4.17.12':
dependencies: dependencies:
'@types/lodash': 4.17.6 '@types/lodash': 4.17.6
'@types/lodash@4.17.6': {} '@types/lodash@4.17.6': {}
'@types/markdown-it-link-attributes@3.0.5':
dependencies:
'@types/markdown-it': 14.1.2
'@types/markdown-it@14.1.2':
dependencies:
'@types/linkify-it': 5.0.0
'@types/mdurl': 2.0.0
'@types/mdurl@2.0.0': {}
'@types/node@20.14.10': '@types/node@20.14.10':
dependencies: dependencies:
undici-types: 5.26.5 undici-types: 5.26.5
...@@ -4031,6 +4154,12 @@ snapshots: ...@@ -4031,6 +4154,12 @@ snapshots:
slice-ansi: 5.0.0 slice-ansi: 5.0.0
string-width: 7.2.0 string-width: 7.2.0
clipboardy@4.0.0:
dependencies:
execa: 8.0.1
is-wsl: 3.1.0
is64bit: 2.0.0
cliui@8.0.1: cliui@8.0.1:
dependencies: dependencies:
string-width: 4.2.3 string-width: 4.2.3
...@@ -4141,6 +4270,8 @@ snapshots: ...@@ -4141,6 +4270,8 @@ snapshots:
dependencies: dependencies:
'@babel/runtime': 7.24.7 '@babel/runtime': 7.24.7
dayjs@1.11.13: {}
de-indent@1.0.2: {} de-indent@1.0.2: {}
debug@4.3.5: debug@4.3.5:
...@@ -4582,6 +4713,8 @@ snapshots: ...@@ -4582,6 +4713,8 @@ snapshots:
is-docker@2.2.1: {} is-docker@2.2.1: {}
is-docker@3.0.0: {}
is-extglob@2.1.1: {} is-extglob@2.1.1: {}
is-fullwidth-code-point@3.0.0: {} is-fullwidth-code-point@3.0.0: {}
...@@ -4596,6 +4729,10 @@ snapshots: ...@@ -4596,6 +4729,10 @@ snapshots:
dependencies: dependencies:
is-extglob: 2.1.1 is-extglob: 2.1.1
is-inside-container@1.0.0:
dependencies:
is-docker: 3.0.0
is-number@7.0.0: {} is-number@7.0.0: {}
is-obj@2.0.0: {} is-obj@2.0.0: {}
...@@ -4616,6 +4753,14 @@ snapshots: ...@@ -4616,6 +4753,14 @@ snapshots:
dependencies: dependencies:
is-docker: 2.2.1 is-docker: 2.2.1
is-wsl@3.1.0:
dependencies:
is-inside-container: 1.0.0
is64bit@2.0.0:
dependencies:
system-architecture: 0.1.0
isexe@2.0.0: {} isexe@2.0.0: {}
jiti@1.21.6: {} jiti@1.21.6: {}
...@@ -4657,6 +4802,10 @@ snapshots: ...@@ -4657,6 +4802,10 @@ snapshots:
jsonparse@1.3.1: {} jsonparse@1.3.1: {}
katex@0.16.11:
dependencies:
commander: 8.3.0
keyv@4.5.4: keyv@4.5.4:
dependencies: dependencies:
json-buffer: 3.0.1 json-buffer: 3.0.1
...@@ -4676,6 +4825,10 @@ snapshots: ...@@ -4676,6 +4825,10 @@ snapshots:
lines-and-columns@1.2.4: {} lines-and-columns@1.2.4: {}
linkify-it@5.0.0:
dependencies:
uc.micro: 2.1.0
lint-staged@15.2.7: lint-staged@15.2.7:
dependencies: dependencies:
chalk: 5.3.0 chalk: 5.3.0
...@@ -4755,10 +4908,23 @@ snapshots: ...@@ -4755,10 +4908,23 @@ snapshots:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15
markdown-it-link-attributes@4.0.1: {}
markdown-it@14.1.0:
dependencies:
argparse: 2.0.1
entities: 4.5.0
linkify-it: 5.0.0
mdurl: 2.0.0
punycode.js: 2.3.1
uc.micro: 2.1.0
mathml-tag-names@2.1.3: {} mathml-tag-names@2.1.3: {}
mdn-data@2.0.30: {} mdn-data@2.0.30: {}
mdurl@2.0.0: {}
meow@12.1.1: {} meow@12.1.1: {}
meow@13.2.0: {} meow@13.2.0: {}
...@@ -4792,6 +4958,8 @@ snapshots: ...@@ -4792,6 +4958,8 @@ snapshots:
minimist@1.2.8: {} minimist@1.2.8: {}
mitt@3.0.1: {}
mlly@1.7.1: mlly@1.7.1:
dependencies: dependencies:
acorn: 8.12.1 acorn: 8.12.1
...@@ -5002,6 +5170,8 @@ snapshots: ...@@ -5002,6 +5170,8 @@ snapshots:
proxy-from-env@1.1.0: {} proxy-from-env@1.1.0: {}
punycode.js@2.3.1: {}
punycode@2.3.1: {} punycode@2.3.1: {}
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
...@@ -5290,6 +5460,8 @@ snapshots: ...@@ -5290,6 +5460,8 @@ snapshots:
'@pkgr/core': 0.1.1 '@pkgr/core': 0.1.1
tslib: 2.6.3 tslib: 2.6.3
system-architecture@0.1.0: {}
table@6.8.2: table@6.8.2:
dependencies: dependencies:
ajv: 8.16.0 ajv: 8.16.0
...@@ -5343,6 +5515,8 @@ snapshots: ...@@ -5343,6 +5515,8 @@ snapshots:
typescript@5.5.3: {} typescript@5.5.3: {}
uc.micro@2.1.0: {}
ufo@1.5.3: {} ufo@1.5.3: {}
unconfig@0.3.13: unconfig@0.3.13:
...@@ -5462,7 +5636,7 @@ snapshots: ...@@ -5462,7 +5636,7 @@ snapshots:
evtd: 0.2.4 evtd: 0.2.4
vue: 3.4.31(typescript@5.5.3) vue: 3.4.31(typescript@5.5.3)
vite-plugin-checker@0.7.1(eslint@9.6.0)(optionator@0.9.4)(stylelint@16.6.1(typescript@5.5.3))(typescript@5.5.3)(vite@5.3.3(@types/node@20.14.10)(sass@1.77.6))(vue-tsc@2.0.26(typescript@5.5.3)): vite-plugin-checker@0.7.1(eslint@9.6.0)(meow@13.2.0)(optionator@0.9.4)(stylelint@16.6.1(typescript@5.5.3))(typescript@5.5.3)(vite@5.3.3(@types/node@20.14.10)(sass@1.77.6))(vue-tsc@2.0.26(typescript@5.5.3)):
dependencies: dependencies:
'@babel/code-frame': 7.24.7 '@babel/code-frame': 7.24.7
ansi-escapes: 4.3.2 ansi-escapes: 4.3.2
...@@ -5481,6 +5655,7 @@ snapshots: ...@@ -5481,6 +5655,7 @@ snapshots:
vscode-uri: 3.0.8 vscode-uri: 3.0.8
optionalDependencies: optionalDependencies:
eslint: 9.6.0 eslint: 9.6.0
meow: 13.2.0
optionator: 0.9.4 optionator: 0.9.4
stylelint: 16.6.1(typescript@5.5.3) stylelint: 16.6.1(typescript@5.5.3)
typescript: 5.5.3 typescript: 5.5.3
......
import { request } from '@/utils/request'
/**
*
* @param payload agentApplicationInfo 应用参数
* @returns 新建或更新应用
*/
export function fetchSaveAgentApplication<T>(payload: object) {
return request.post<T>('/agentApplicationInfoRest/saveOrUpdate.json', payload)
}
/**
*
* @param payload 查询参数
* @returns 获取应用列表
*/
export function fetchGetApplicationList<T>(payload: object) {
return request.post<T>('/agentApplicationInfoRest/getListByMember.json', payload)
}
/**
*
* @param payload agentId 应用Id
* @returns 通过agentId删除应用
*/
export function fetchDeleteApplication<T>(agentId: string) {
return request.post<T>(`/agentApplicationInfoRest/delete.json?agentId=${agentId}`)
}
/**
*
* @param payload agentId 应用Id
* @returns 通过agentId获取调试应用详情
*/
export function fetchGetDebugApplicationInfo<T>(agentId: string) {
return request.post<T>(`/agentApplicationInfoRest/getInfo.json?agentId=${agentId}`)
}
/**
*
* @param payload agentId 应用Id
* @returns 通过agentId获取发布应用详情
*/
export function fetchGetApplicationInfo<T>(agentId: string) {
return request.post<T>(`/agentApplicationRest/getInfo.json?agentId=${agentId}`)
}
/**
*
* @param payload payload 应用参数
* @returns 发布应用
*/
export function fetchPublishApplication<T>(payload: object) {
return request.post<T>('/agentApplicationInfoRest/updateAndPublish.json', payload)
}
/**
* @returns 获取大模型列表
*/
export function fetchGetLargeModelList<T>() {
return request.post<T>('/agentApplicationInfoRest/getLargeModelList.json')
}
/**
* * @param agentId 应用Id
* @returns 创建会话Id
*/
export function fetchCreateDialogues<T>(agentId: string) {
return request.post<T>(`/agentApplicationRest/createDialogues.json?agentId=${agentId}`)
}
import { request } from '@/utils/request'
export function fetchUpload<T>(formdata: FormData) {
return request.post<T>('/bosRest/upload.json', formdata, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 120000,
})
}
<script setup lang="ts"> <script setup lang="ts">
import { zhCN, dateZhCN } from 'naive-ui' import { zhCN, dateZhCN } from 'naive-ui'
import { ref } from 'vue' import { provide, ref } from 'vue'
import mitt from 'mitt'
import { themeOverrides } from '@/config/theme-config' import { themeOverrides } from '@/config/theme-config'
import { useResizeObserver } from '@vueuse/core' import { useResizeObserver } from '@vueuse/core'
import { useDesignSettingStore } from '@/store/modules/design-setting' import { useDesignSettingStore } from '@/store/modules/design-setting'
// import { NThemeEditor } from 'naive-ui'
const designSettingStore = useDesignSettingStore() const designSettingStore = useDesignSettingStore()
const emitter = mitt<MittEvents>()
provide('emitter', emitter)
const currentLocale = ref(zhCN) const currentLocale = ref(zhCN)
const currentDateLocale = ref(dateZhCN) const currentDateLocale = ref(dateZhCN)
...@@ -48,6 +52,4 @@ useResizeObserver(rootContainer, (entries) => { ...@@ -48,6 +52,4 @@ useResizeObserver(rootContainer, (entries) => {
</RouterView> </RouterView>
</NConfigProvider> </NConfigProvider>
</div> </div>
<!-- <NThemeEditor /> -->
</template> </template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><defs><style>.cls-1{fill:#2468f2;}.cls-2{fill:none;}</style></defs><g id="图层_2" data-name="图层 2"><g id="图层_4" data-name="图层 4"><path class="cls-1" d="M10.77,4.87l1.76,3.56a.85.85,0,0,0,.64.47l3.93.57a.86.86,0,0,1,.48,1.46L14.74,13.7a.84.84,0,0,0-.25.76l.67,3.91a.86.86,0,0,1-1.25.91L10.4,17.43a.87.87,0,0,0-.8,0L6.09,19.28a.86.86,0,0,1-1.25-.91l.67-3.91a.84.84,0,0,0-.25-.76L2.42,10.93A.86.86,0,0,1,2.9,9.47L6.83,8.9a.85.85,0,0,0,.64-.47L9.23,4.87A.86.86,0,0,1,10.77,4.87Z"/><rect class="cls-2" width="20" height="20"/></g></g></svg>
<script setup lang="ts">
import { computed } from 'vue'
import { modalHeaderStyle, modalContentStyle, modalFooterStyle } from './modal-style'
interface Props {
title: string // 弹窗标题
isShow: boolean // 是否显示
height?: number // 高度
width?: number // 宽度
borderRadius?: number // 圆角
btnLoading?: boolean // 按钮是否加载中
labelWidth?: number // 标签的宽度
labelPlacement?: 'left' | 'top' // 标签显示的位置
}
interface Emits {
(e: 'update:isShow', value: boolean): void
(e: 'close'): void
(e: 'confirm'): void
}
const props = withDefaults(defineProps<Props>(), {
height: 240,
width: 500,
borderRadius: 6,
labelWidth: 80,
labelPlacement: 'left',
})
const emit = defineEmits<Emits>()
const modalBasicStyle = {
width: props.width + 'px',
height: props.height + 'px',
borderRadius: props.borderRadius + 'px',
}
const showModal = computed({
get() {
return props.isShow
},
set(value: boolean) {
emit('update:isShow', value)
},
})
function handleCloseModal() {
showModal.value = false
emit('close')
}
function handleDetele() {
emit('confirm')
}
</script>
<template>
<NModal
preset="card"
transform-origin="center"
closable
:style="modalBasicStyle"
:show="showModal"
:bordered="false"
:auto-focus="false"
:header-style="modalHeaderStyle"
:content-style="modalContentStyle"
:footer-style="modalFooterStyle"
:on-mask-click="handleCloseModal"
@close="handleCloseModal"
>
<template #header>
<div class="text-xl">{{ title }}</div>
</template>
<div>
<slot name="content" />
</div>
<template #footer>
<div class="flex w-full items-center justify-end">
<NButton class="h-[32px]! w-[80px]! rounded-md!" @click="handleCloseModal"> 取 消 </NButton>
<NButton
:loading="btnLoading"
type="primary"
class="h-[32px]! w-[80px]! rounded-md! ml-4!"
@click="handleDetele"
>
确 定
</NButton>
</div>
</template>
</NModal>
</template>
export const modalHeaderStyle = {
padding: '24px 24px 16px',
fontSize: '20px',
}
export const modalContentStyle = {
padding: '0 24px 24px',
overflow: 'auto',
}
export const modalFooterStyle = {
padding: '0px 24px 24px 24px',
}
<script setup lang="ts">
export interface PaginationInfo {
pageNo: number
pageSize: number
totalPages: number
totalRows: number
}
interface Props {
pagingInfo: PaginationInfo
}
const pageSizes = [
{
label: '10 / 每页',
value: 10,
},
{
label: '20 / 每页',
value: 20,
},
{
label: '30 / 每页',
value: 30,
},
{
label: '40 / 每页',
value: 40,
},
]
interface Emits {
(e: 'update:pagingInfo', value: PaginationInfo): void
(e: 'updatePageNo', value: number): void
(e: 'updatePageSize', value: number): void
}
defineProps<Props>()
const emit = defineEmits<Emits>()
async function handleUpdatePageNo(pageNo: number) {
emit('updatePageNo', pageNo)
}
async function handleUpdatePageSize(pageSize: number) {
emit('updatePageSize', pageSize)
}
</script>
<template>
<div class="flex items-center">
<span class="text-[#999]">{{ pagingInfo.totalRows }}</span>
<NPagination
class="custom-pagination"
:page="pagingInfo.pageNo"
:page-count="pagingInfo.totalPages"
:page-sizes="pageSizes"
size="medium"
show-quick-jumper
show-size-picker
@update:page="handleUpdatePageNo"
@update:page-size="handleUpdatePageSize"
/>
<span class="ml-[10px] text-[#999]"></span>
</div>
</template>
<style lang="scss" scoped>
:deep(.custom-pagination .n-pagination-item) {
padding: 0;
margin-left: 2px;
border-radius: 5px;
}
:deep(.custom-pagination .n-pagination-item--button) {
background-color: white !important;
border: none !important;
}
:deep(.custom-pagination .n-base-selection) {
width: 100px;
border-radius: 6px;
--n-border: 1px solid #999 !important;
}
:deep(.custom-pagination .n-pagination-quick-jumper),
:deep(.custom-pagination .n-input__input-el),
:deep(.custom-pagination .n-base-selection-input__content) {
color: #999;
}
:deep(.custom-pagination .n-input) {
border-radius: 6px;
--n-border: 1px solid #999 !important;
}
</style>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, onUpdated, ref } from 'vue'
import MarkdownIt from 'markdown-it'
import mdKatex from '@traptitech/markdown-it-katex'
import mila from 'markdown-it-link-attributes'
import hljs from 'highlight.js'
import { useClipboard } from '@vueuse/core'
interface Props {
rawTextContent: string
fontSize?: number | string
color?: string
}
const props = withDefaults(defineProps<Props>(), {
rawTextContent: '',
fontSize: '14px',
color: '#333',
})
const mkrContainer = ref()
const copyCodeText = ref('')
const { copy: copyTool } = useClipboard({ source: copyCodeText })
const mdi = new MarkdownIt({
html: true,
linkify: true,
highlight(code: any, language: any) {
const validLang = !!(language && hljs.getLanguage(language))
if (validLang) {
const lang = language ?? ''
return highlightBlock(hljs.highlight(code, { language: lang }).value, lang)
}
return highlightBlock(hljs.highlightAuto(code).value, '')
},
})
mdi.use(mila, { attrs: { target: '_blank', rel: 'noopener' } })
mdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' })
const renderTextContent = computed(() => {
if (props.rawTextContent) {
const urlRegex = /((https?:\/\/[^\s]+))/g
const rawTextContent = props.rawTextContent.replace(urlRegex, '($1)')
return mdi.render(rawTextContent)
}
return ''
})
const wrapStyle = computed(() => {
return {
fontSize: props.fontSize,
color: props.color,
}
})
function highlightBlock(str: string, lang?: string) {
return (
'<pre class="code-block-wrapper">' +
'<div class="code-block-header">' +
`<span class="code-block-header__lang">${lang}</span>` +
'<span class="code-block-header__copy">复制代码</span>' +
'</div>' +
`<code class="hljs code-block-body ${lang}">${str}</code>` +
'</pre>'
)
}
function addCopyEvents() {
if (mkrContainer.value) {
const copyBtn = mkrContainer.value.querySelectorAll('.code-block-header__copy')
copyBtn.forEach((btn: HTMLSpanElement) => {
btn.addEventListener('click', () => {
const code = btn.parentElement?.nextElementSibling?.textContent
if (code) {
copyTool(code).then(() => {
btn.textContent = '复制成功'
setTimeout(() => (btn.textContent = '复制代码'), 1000)
})
}
})
})
}
}
function removeCopyEvents() {
if (mkrContainer.value) {
const copyBtn = mkrContainer.value.querySelectorAll('.code-block-header__copy')
copyBtn.forEach((btn: HTMLSpanElement) => {
btn.removeEventListener('click', () => {})
})
}
}
onMounted(() => {
addCopyEvents()
})
onUpdated(() => {
addCopyEvents()
})
onUnmounted(() => {
removeCopyEvents()
})
</script>
<template>
<div ref="mkrContainer" class="markdown-render-container">
<article class="markdown-body" :style="wrapStyle" v-html="renderTextContent" />
</div>
</template>
<style lang="scss">
@import './style/highlight.scss';
@import './style/github-markdown.scss';
@import './style/custom-style.scss';
</style>
.markdown-body {
background-color: transparent;
font-size: 14px;
p {
white-space: pre-wrap;
}
ol {
list-style-type: decimal;
}
ul {
list-style-type: disc;
}
pre code,
pre tt {
line-height: 1.65;
}
.highlight pre,
pre {
background-color: #f4fafd;
}
code.hljs {
padding: 0;
}
.code-block {
&-wrapper {
position: relative;
padding-top: 24px;
}
&-header {
position: absolute;
top: 5px;
right: 0;
width: 100%;
padding: 0 1rem;
display: flex;
justify-content: flex-end;
align-items: center;
color: #b3b3b3;
&__copy {
cursor: pointer;
margin-left: 0.5rem;
user-select: none;
&:hover {
color: #65a665;
}
}
}
}
}
html.dark {
.message-reply {
.whitespace-pre-wrap {
white-space: pre-wrap;
word-break: break-all;
color: var(--n-text-color);
}
}
.highlight pre,
pre {
background-color: #282c34;
}
}
@media (prefers-color-scheme: dark) {
.markdown-body,
[data-theme='dark'] {
/*dark*/
color-scheme: dark;
--color-prettylights-syntax-comment: #8b949e;
--color-prettylights-syntax-constant: #79c0ff;
--color-prettylights-syntax-entity: #d2a8ff;
--color-prettylights-syntax-storage-modifier-import: #c9d1d9;
--color-prettylights-syntax-entity-tag: #7ee787;
--color-prettylights-syntax-keyword: #ff7b72;
--color-prettylights-syntax-string: #a5d6ff;
--color-prettylights-syntax-variable: #ffa657;
--color-prettylights-syntax-brackethighlighter-unmatched: #f85149;
--color-prettylights-syntax-invalid-illegal-text: #f0f6fc;
--color-prettylights-syntax-invalid-illegal-bg: #8e1519;
--color-prettylights-syntax-carriage-return-text: #f0f6fc;
--color-prettylights-syntax-carriage-return-bg: #b62324;
--color-prettylights-syntax-string-regexp: #7ee787;
--color-prettylights-syntax-markup-list: #f2cc60;
--color-prettylights-syntax-markup-heading: #1f6feb;
--color-prettylights-syntax-markup-italic: #c9d1d9;
--color-prettylights-syntax-markup-bold: #c9d1d9;
--color-prettylights-syntax-markup-deleted-text: #ffdcd7;
--color-prettylights-syntax-markup-deleted-bg: #67060c;
--color-prettylights-syntax-markup-inserted-text: #aff5b4;
--color-prettylights-syntax-markup-inserted-bg: #033a16;
--color-prettylights-syntax-markup-changed-text: #ffdfb6;
--color-prettylights-syntax-markup-changed-bg: #5a1e02;
--color-prettylights-syntax-markup-ignored-text: #c9d1d9;
--color-prettylights-syntax-markup-ignored-bg: #1158c7;
--color-prettylights-syntax-meta-diff-range: #d2a8ff;
--color-prettylights-syntax-brackethighlighter-angle: #8b949e;
--color-prettylights-syntax-sublimelinter-gutter-mark: #484f58;
--color-prettylights-syntax-constant-other-reference-link: #a5d6ff;
--color-fg-default: #e6edf3;
--color-fg-muted: #7d8590;
--color-fg-subtle: #6e7681;
--color-canvas-default: #0d1117;
--color-canvas-subtle: #161b22;
--color-border-default: #30363d;
--color-border-muted: #21262d;
--color-neutral-muted: rgba(110, 118, 129, 0.4);
--color-accent-fg: #2f81f7;
--color-accent-emphasis: #1f6feb;
--color-attention-subtle: rgba(187, 128, 9, 0.15);
--color-danger-fg: #f85149;
}
}
@media (prefers-color-scheme: light) {
.markdown-body,
[data-theme='light'] {
/*light*/
color-scheme: light;
--color-prettylights-syntax-comment: #6e7781;
--color-prettylights-syntax-constant: #0550ae;
--color-prettylights-syntax-entity: #6639ba;
--color-prettylights-syntax-storage-modifier-import: #24292f;
--color-prettylights-syntax-entity-tag: #116329;
--color-prettylights-syntax-keyword: #cf222e;
--color-prettylights-syntax-string: #0a3069;
--color-prettylights-syntax-variable: #953800;
--color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
--color-prettylights-syntax-invalid-illegal-text: #f6f8fa;
--color-prettylights-syntax-invalid-illegal-bg: #82071e;
--color-prettylights-syntax-carriage-return-text: #f6f8fa;
--color-prettylights-syntax-carriage-return-bg: #cf222e;
--color-prettylights-syntax-string-regexp: #116329;
--color-prettylights-syntax-markup-list: #3b2300;
--color-prettylights-syntax-markup-heading: #0550ae;
--color-prettylights-syntax-markup-italic: #24292f;
--color-prettylights-syntax-markup-bold: #24292f;
--color-prettylights-syntax-markup-deleted-text: #82071e;
--color-prettylights-syntax-markup-deleted-bg: #ffebe9;
--color-prettylights-syntax-markup-inserted-text: #116329;
--color-prettylights-syntax-markup-inserted-bg: #dafbe1;
--color-prettylights-syntax-markup-changed-text: #953800;
--color-prettylights-syntax-markup-changed-bg: #ffd8b5;
--color-prettylights-syntax-markup-ignored-text: #eaeef2;
--color-prettylights-syntax-markup-ignored-bg: #0550ae;
--color-prettylights-syntax-meta-diff-range: #8250df;
--color-prettylights-syntax-brackethighlighter-angle: #57606a;
--color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;
--color-prettylights-syntax-constant-other-reference-link: #0a3069;
--color-fg-default: #1f2328;
--color-fg-muted: #656d76;
--color-fg-subtle: #6e7781;
--color-canvas-default: #ffffff;
--color-canvas-subtle: #f6f8fa;
--color-border-default: #d0d7de;
--color-border-muted: hsla(210, 18%, 87%, 1);
--color-neutral-muted: rgba(175, 184, 193, 0.2);
--color-accent-fg: #0969da;
--color-accent-emphasis: #0969da;
--color-attention-subtle: #fff8c5;
--color-danger-fg: #d1242f;
--color-prettylights-syntax-comment: #6e7781;
--color-prettylights-syntax-constant: #0550ae;
--color-prettylights-syntax-entity: #6639ba;
--color-prettylights-syntax-storage-modifier-import: #24292f;
--color-prettylights-syntax-entity-tag: #116329;
--color-prettylights-syntax-keyword: #cf222e;
--color-prettylights-syntax-string: #0a3069;
--color-prettylights-syntax-variable: #953800;
--color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
--color-prettylights-syntax-invalid-illegal-text: #f6f8fa;
--color-prettylights-syntax-invalid-illegal-bg: #82071e;
--color-prettylights-syntax-carriage-return-text: #f6f8fa;
--color-prettylights-syntax-carriage-return-bg: #cf222e;
--color-prettylights-syntax-string-regexp: #116329;
--color-prettylights-syntax-markup-list: #3b2300;
--color-prettylights-syntax-markup-heading: #0550ae;
--color-prettylights-syntax-markup-italic: #24292f;
--color-prettylights-syntax-markup-bold: #24292f;
--color-prettylights-syntax-markup-deleted-text: #82071e;
--color-prettylights-syntax-markup-deleted-bg: #ffebe9;
--color-prettylights-syntax-markup-inserted-text: #116329;
--color-prettylights-syntax-markup-inserted-bg: #dafbe1;
--color-prettylights-syntax-markup-changed-text: #953800;
--color-prettylights-syntax-markup-changed-bg: #ffd8b5;
--color-prettylights-syntax-markup-ignored-text: #eaeef2;
--color-prettylights-syntax-markup-ignored-bg: #0550ae;
--color-prettylights-syntax-meta-diff-range: #8250df;
--color-prettylights-syntax-brackethighlighter-angle: #57606a;
--color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;
--color-prettylights-syntax-constant-other-reference-link: #0a3069;
--color-fg-default: #1f2328;
--color-fg-muted: #656d76;
--color-fg-subtle: #6e7781;
--color-canvas-default: #ffffff;
--color-canvas-subtle: #f6f8fa;
--color-border-default: #d0d7de;
--color-border-muted: hsla(210, 18%, 87%, 1);
--color-neutral-muted: rgba(175, 184, 193, 0.2);
--color-accent-fg: #0969da;
--color-accent-emphasis: #0969da;
--color-attention-subtle: #fff8c5;
--color-danger-fg: #d1242f;
}
}
.markdown-body {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
margin: 0;
color: var(--color-fg-default);
background-color: var(--color-canvas-default);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji';
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
}
.markdown-body .octicon {
display: inline-block;
fill: currentColor;
vertical-align: text-bottom;
}
.markdown-body h1:hover .anchor .octicon-link:before,
.markdown-body h2:hover .anchor .octicon-link:before,
.markdown-body h3:hover .anchor .octicon-link:before,
.markdown-body h4:hover .anchor .octicon-link:before,
.markdown-body h5:hover .anchor .octicon-link:before,
.markdown-body h6:hover .anchor .octicon-link:before {
width: 16px;
height: 16px;
content: ' ';
display: inline-block;
background-color: currentColor;
-webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
}
.markdown-body details,
.markdown-body figcaption,
.markdown-body figure {
display: block;
}
.markdown-body summary {
display: list-item;
}
.markdown-body [hidden] {
display: none !important;
}
.markdown-body a {
background-color: transparent;
color: var(--color-accent-fg);
text-decoration: none;
}
.markdown-body abbr[title] {
border-bottom: none;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
.markdown-body b,
.markdown-body strong {
font-weight: var(--base-text-weight-semibold, 600);
}
.markdown-body dfn {
font-style: italic;
}
.markdown-body h1 {
margin: 0.67em 0;
font-weight: var(--base-text-weight-semibold, 600);
padding-bottom: 0.3em;
font-size: 2em;
border-bottom: 1px solid var(--color-border-muted);
}
.markdown-body mark {
background-color: var(--color-attention-subtle);
color: var(--color-fg-default);
}
.markdown-body small {
font-size: 90%;
}
.markdown-body sub,
.markdown-body sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
.markdown-body sub {
bottom: -0.25em;
}
.markdown-body sup {
top: -0.5em;
}
.markdown-body img {
border-style: none;
max-width: 100%;
box-sizing: content-box;
background-color: var(--color-canvas-default);
}
.markdown-body code,
.markdown-body kbd,
.markdown-body pre,
.markdown-body samp {
font-family: monospace;
font-size: 1em;
}
.markdown-body figure {
margin: 1em 40px;
}
.markdown-body hr {
box-sizing: content-box;
overflow: hidden;
background: transparent;
border-bottom: 1px solid var(--color-border-muted);
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: var(--color-border-default);
border: 0;
}
.markdown-body input {
font: inherit;
margin: 0;
overflow: visible;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.markdown-body [type='button'],
.markdown-body [type='reset'],
.markdown-body [type='submit'] {
-webkit-appearance: button;
}
.markdown-body [type='checkbox'],
.markdown-body [type='radio'] {
box-sizing: border-box;
padding: 0;
}
.markdown-body [type='number']::-webkit-inner-spin-button,
.markdown-body [type='number']::-webkit-outer-spin-button {
height: auto;
}
.markdown-body [type='search']::-webkit-search-cancel-button,
.markdown-body [type='search']::-webkit-search-decoration {
-webkit-appearance: none;
}
.markdown-body ::-webkit-input-placeholder {
color: inherit;
opacity: 0.54;
}
.markdown-body ::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
.markdown-body a:hover {
text-decoration: underline;
}
.markdown-body ::placeholder {
color: var(--color-fg-subtle);
opacity: 1;
}
.markdown-body hr::before {
display: table;
content: '';
}
.markdown-body hr::after {
display: table;
clear: both;
content: '';
}
.markdown-body table {
border-spacing: 0;
border-collapse: collapse;
display: block;
width: max-content;
max-width: 100%;
overflow: auto;
}
.markdown-body td,
.markdown-body th {
padding: 0;
}
.markdown-body details summary {
cursor: pointer;
}
.markdown-body details:not([open]) > *:not(summary) {
display: none !important;
}
.markdown-body a:focus,
.markdown-body [role='button']:focus,
.markdown-body input[type='radio']:focus,
.markdown-body input[type='checkbox']:focus {
outline: 2px solid var(--color-accent-fg);
outline-offset: -2px;
box-shadow: none;
}
.markdown-body a:focus:not(:focus-visible),
.markdown-body [role='button']:focus:not(:focus-visible),
.markdown-body input[type='radio']:focus:not(:focus-visible),
.markdown-body input[type='checkbox']:focus:not(:focus-visible) {
outline: solid 1px transparent;
}
.markdown-body a:focus-visible,
.markdown-body [role='button']:focus-visible,
.markdown-body input[type='radio']:focus-visible,
.markdown-body input[type='checkbox']:focus-visible {
outline: 2px solid var(--color-accent-fg);
outline-offset: -2px;
box-shadow: none;
}
.markdown-body a:not([class]):focus,
.markdown-body a:not([class]):focus-visible,
.markdown-body input[type='radio']:focus,
.markdown-body input[type='radio']:focus-visible,
.markdown-body input[type='checkbox']:focus,
.markdown-body input[type='checkbox']:focus-visible {
outline-offset: 0;
}
.markdown-body kbd {
display: inline-block;
padding: 3px 5px;
font:
11px ui-monospace,
SFMono-Regular,
SF Mono,
Menlo,
Consolas,
Liberation Mono,
monospace;
line-height: 10px;
color: var(--color-fg-default);
vertical-align: middle;
background-color: var(--color-canvas-subtle);
border: solid 1px var(--color-neutral-muted);
border-bottom-color: var(--color-neutral-muted);
border-radius: 6px;
box-shadow: inset 0 -1px 0 var(--color-neutral-muted);
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: var(--base-text-weight-semibold, 600);
line-height: 1.25;
}
.markdown-body h2 {
font-weight: var(--base-text-weight-semibold, 600);
padding-bottom: 0.3em;
font-size: 1.5em;
border-bottom: 1px solid var(--color-border-muted);
}
.markdown-body h3 {
font-weight: var(--base-text-weight-semibold, 600);
font-size: 1.25em;
}
.markdown-body h4 {
font-weight: var(--base-text-weight-semibold, 600);
font-size: 1em;
}
.markdown-body h5 {
font-weight: var(--base-text-weight-semibold, 600);
font-size: 0.875em;
}
.markdown-body h6 {
font-weight: var(--base-text-weight-semibold, 600);
font-size: 0.85em;
color: var(--color-fg-muted);
}
.markdown-body p {
margin-top: 0;
margin-bottom: 10px;
}
.markdown-body blockquote {
margin: 0;
padding: 0 1em;
color: var(--color-fg-muted);
border-left: 0.25em solid var(--color-border-default);
}
.markdown-body ul,
.markdown-body ol {
margin-top: 0;
margin-bottom: 0;
padding-left: 2em;
}
.markdown-body ol ol,
.markdown-body ul ol {
list-style-type: lower-roman;
}
.markdown-body ul ul ol,
.markdown-body ul ol ol,
.markdown-body ol ul ol,
.markdown-body ol ol ol {
list-style-type: lower-alpha;
}
.markdown-body dd {
margin-left: 0;
}
.markdown-body tt,
.markdown-body code,
.markdown-body samp {
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Menlo,
Consolas,
Liberation Mono,
monospace;
font-size: 12px;
}
.markdown-body pre {
margin-top: 0;
margin-bottom: 0;
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Menlo,
Consolas,
Liberation Mono,
monospace;
font-size: 12px;
word-wrap: normal;
}
.markdown-body .octicon {
display: inline-block;
overflow: visible !important;
vertical-align: text-bottom;
fill: currentColor;
}
.markdown-body input::-webkit-outer-spin-button,
.markdown-body input::-webkit-inner-spin-button {
margin: 0;
-webkit-appearance: none;
appearance: none;
}
.markdown-body::before {
display: table;
content: '';
}
.markdown-body::after {
display: table;
clear: both;
content: '';
}
.markdown-body > *:first-child {
margin-top: 0 !important;
}
.markdown-body > *:last-child {
margin-bottom: 0 !important;
}
.markdown-body a:not([href]) {
color: inherit;
text-decoration: none;
}
.markdown-body .absent {
color: var(--color-danger-fg);
}
.markdown-body .anchor {
float: left;
padding-right: 4px;
margin-left: -20px;
line-height: 1;
}
.markdown-body .anchor:focus {
outline: none;
}
.markdown-body p,
.markdown-body blockquote,
.markdown-body ul,
.markdown-body ol,
.markdown-body dl,
.markdown-body table,
.markdown-body pre,
.markdown-body details {
margin-top: 0;
margin-bottom: 16px;
}
.markdown-body blockquote > :first-child {
margin-top: 0;
}
.markdown-body blockquote > :last-child {
margin-bottom: 0;
}
.markdown-body h1 .octicon-link,
.markdown-body h2 .octicon-link,
.markdown-body h3 .octicon-link,
.markdown-body h4 .octicon-link,
.markdown-body h5 .octicon-link,
.markdown-body h6 .octicon-link {
color: var(--color-fg-default);
vertical-align: middle;
visibility: hidden;
}
.markdown-body h1:hover .anchor,
.markdown-body h2:hover .anchor,
.markdown-body h3:hover .anchor,
.markdown-body h4:hover .anchor,
.markdown-body h5:hover .anchor,
.markdown-body h6:hover .anchor {
text-decoration: none;
}
.markdown-body h1:hover .anchor .octicon-link,
.markdown-body h2:hover .anchor .octicon-link,
.markdown-body h3:hover .anchor .octicon-link,
.markdown-body h4:hover .anchor .octicon-link,
.markdown-body h5:hover .anchor .octicon-link,
.markdown-body h6:hover .anchor .octicon-link {
visibility: visible;
}
.markdown-body h1 tt,
.markdown-body h1 code,
.markdown-body h2 tt,
.markdown-body h2 code,
.markdown-body h3 tt,
.markdown-body h3 code,
.markdown-body h4 tt,
.markdown-body h4 code,
.markdown-body h5 tt,
.markdown-body h5 code,
.markdown-body h6 tt,
.markdown-body h6 code {
padding: 0 0.2em;
font-size: inherit;
}
.markdown-body summary h1,
.markdown-body summary h2,
.markdown-body summary h3,
.markdown-body summary h4,
.markdown-body summary h5,
.markdown-body summary h6 {
display: inline-block;
}
.markdown-body summary h1 .anchor,
.markdown-body summary h2 .anchor,
.markdown-body summary h3 .anchor,
.markdown-body summary h4 .anchor,
.markdown-body summary h5 .anchor,
.markdown-body summary h6 .anchor {
margin-left: -40px;
}
.markdown-body summary h1,
.markdown-body summary h2 {
padding-bottom: 0;
border-bottom: 0;
}
.markdown-body ul.no-list,
.markdown-body ol.no-list {
padding: 0;
list-style-type: none;
}
.markdown-body ol[type='a s'] {
list-style-type: lower-alpha;
}
.markdown-body ol[type='A s'] {
list-style-type: upper-alpha;
}
.markdown-body ol[type='i s'] {
list-style-type: lower-roman;
}
.markdown-body ol[type='I s'] {
list-style-type: upper-roman;
}
.markdown-body ol[type='1'] {
list-style-type: decimal;
}
.markdown-body div > ol:not([type]) {
list-style-type: decimal;
}
.markdown-body ul ul,
.markdown-body ul ol,
.markdown-body ol ol,
.markdown-body ol ul {
margin-top: 0;
margin-bottom: 0;
}
.markdown-body li > p {
margin-top: 16px;
}
.markdown-body li + li {
margin-top: 0.25em;
}
.markdown-body dl {
padding: 0;
}
.markdown-body dl dt {
padding: 0;
margin-top: 16px;
font-size: 1em;
font-style: italic;
font-weight: var(--base-text-weight-semibold, 600);
}
.markdown-body dl dd {
padding: 0 16px;
margin-bottom: 16px;
}
.markdown-body table th {
font-weight: var(--base-text-weight-semibold, 600);
}
.markdown-body table th,
.markdown-body table td {
padding: 6px 13px;
border: 1px solid var(--color-border-default);
}
.markdown-body table td > :last-child {
margin-bottom: 0;
}
.markdown-body table tr {
background-color: var(--color-canvas-default);
border-top: 1px solid var(--color-border-muted);
}
.markdown-body table tr:nth-child(2n) {
background-color: var(--color-canvas-subtle);
}
.markdown-body table img {
background-color: transparent;
}
.markdown-body img[align='right'] {
padding-left: 20px;
}
.markdown-body img[align='left'] {
padding-right: 20px;
}
.markdown-body .emoji {
max-width: none;
vertical-align: text-top;
background-color: transparent;
}
.markdown-body span.frame {
display: block;
overflow: hidden;
}
.markdown-body span.frame > span {
display: block;
float: left;
width: auto;
padding: 7px;
margin: 13px 0 0;
overflow: hidden;
border: 1px solid var(--color-border-default);
}
.markdown-body span.frame span img {
display: block;
float: left;
}
.markdown-body span.frame span span {
display: block;
padding: 5px 0 0;
clear: both;
color: var(--color-fg-default);
}
.markdown-body span.align-center {
display: block;
overflow: hidden;
clear: both;
}
.markdown-body span.align-center > span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: center;
}
.markdown-body span.align-center span img {
margin: 0 auto;
text-align: center;
}
.markdown-body span.align-right {
display: block;
overflow: hidden;
clear: both;
}
.markdown-body span.align-right > span {
display: block;
margin: 13px 0 0;
overflow: hidden;
text-align: right;
}
.markdown-body span.align-right span img {
margin: 0;
text-align: right;
}
.markdown-body span.float-left {
display: block;
float: left;
margin-right: 13px;
overflow: hidden;
}
.markdown-body span.float-left span {
margin: 13px 0 0;
}
.markdown-body span.float-right {
display: block;
float: right;
margin-left: 13px;
overflow: hidden;
}
.markdown-body span.float-right > span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: right;
}
.markdown-body code,
.markdown-body tt {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
white-space: break-spaces;
background-color: var(--color-neutral-muted);
border-radius: 6px;
}
.markdown-body code br,
.markdown-body tt br {
display: none;
}
.markdown-body del code {
text-decoration: inherit;
}
.markdown-body samp {
font-size: 85%;
}
.markdown-body pre code {
font-size: 100%;
}
.markdown-body pre > code {
padding: 0;
margin: 0;
word-break: normal;
white-space: pre;
background: transparent;
border: 0;
}
.markdown-body .highlight {
margin-bottom: 16px;
}
.markdown-body .highlight pre {
margin-bottom: 0;
word-break: normal;
}
.markdown-body .highlight pre,
.markdown-body pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
color: var(--color-fg-default);
background-color: var(--color-canvas-subtle);
border-radius: 6px;
}
.markdown-body pre code,
.markdown-body pre tt {
display: inline;
max-width: auto;
padding: 0;
margin: 0;
overflow: visible;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
}
.markdown-body .csv-data td,
.markdown-body .csv-data th {
padding: 5px;
overflow: hidden;
font-size: 12px;
line-height: 1;
text-align: left;
white-space: nowrap;
}
.markdown-body .csv-data .blob-num {
padding: 10px 8px 9px;
text-align: right;
background: var(--color-canvas-default);
border: 0;
}
.markdown-body .csv-data tr {
border-top: 0;
}
.markdown-body .csv-data th {
font-weight: var(--base-text-weight-semibold, 600);
background: var(--color-canvas-subtle);
border-top: 0;
}
.markdown-body [data-footnote-ref]::before {
content: '[';
}
.markdown-body [data-footnote-ref]::after {
content: ']';
}
.markdown-body .footnotes {
font-size: 12px;
color: var(--color-fg-muted);
border-top: 1px solid var(--color-border-default);
}
.markdown-body .footnotes ol {
padding-left: 16px;
}
.markdown-body .footnotes ol ul {
display: inline-block;
padding-left: 16px;
margin-top: 16px;
}
.markdown-body .footnotes li {
position: relative;
}
.markdown-body .footnotes li:target::before {
position: absolute;
top: -8px;
right: -8px;
bottom: -8px;
left: -24px;
pointer-events: none;
content: '';
border: 2px solid var(--color-accent-emphasis);
border-radius: 6px;
}
.markdown-body .footnotes li:target {
color: var(--color-fg-default);
}
.markdown-body .footnotes .data-footnote-backref g-emoji {
font-family: monospace;
}
.markdown-body .pl-c {
color: var(--color-prettylights-syntax-comment);
}
.markdown-body .pl-c1,
.markdown-body .pl-s .pl-v {
color: var(--color-prettylights-syntax-constant);
}
.markdown-body .pl-e,
.markdown-body .pl-en {
color: var(--color-prettylights-syntax-entity);
}
.markdown-body .pl-smi,
.markdown-body .pl-s .pl-s1 {
color: var(--color-prettylights-syntax-storage-modifier-import);
}
.markdown-body .pl-ent {
color: var(--color-prettylights-syntax-entity-tag);
}
.markdown-body .pl-k {
color: var(--color-prettylights-syntax-keyword);
}
.markdown-body .pl-s,
.markdown-body .pl-pds,
.markdown-body .pl-s .pl-pse .pl-s1,
.markdown-body .pl-sr,
.markdown-body .pl-sr .pl-cce,
.markdown-body .pl-sr .pl-sre,
.markdown-body .pl-sr .pl-sra {
color: var(--color-prettylights-syntax-string);
}
.markdown-body .pl-v,
.markdown-body .pl-smw {
color: var(--color-prettylights-syntax-variable);
}
.markdown-body .pl-bu {
color: var(--color-prettylights-syntax-brackethighlighter-unmatched);
}
.markdown-body .pl-ii {
color: var(--color-prettylights-syntax-invalid-illegal-text);
background-color: var(--color-prettylights-syntax-invalid-illegal-bg);
}
.markdown-body .pl-c2 {
color: var(--color-prettylights-syntax-carriage-return-text);
background-color: var(--color-prettylights-syntax-carriage-return-bg);
}
.markdown-body .pl-sr .pl-cce {
font-weight: bold;
color: var(--color-prettylights-syntax-string-regexp);
}
.markdown-body .pl-ml {
color: var(--color-prettylights-syntax-markup-list);
}
.markdown-body .pl-mh,
.markdown-body .pl-mh .pl-en,
.markdown-body .pl-ms {
font-weight: bold;
color: var(--color-prettylights-syntax-markup-heading);
}
.markdown-body .pl-mi {
font-style: italic;
color: var(--color-prettylights-syntax-markup-italic);
}
.markdown-body .pl-mb {
font-weight: bold;
color: var(--color-prettylights-syntax-markup-bold);
}
.markdown-body .pl-md {
color: var(--color-prettylights-syntax-markup-deleted-text);
background-color: var(--color-prettylights-syntax-markup-deleted-bg);
}
.markdown-body .pl-mi1 {
color: var(--color-prettylights-syntax-markup-inserted-text);
background-color: var(--color-prettylights-syntax-markup-inserted-bg);
}
.markdown-body .pl-mc {
color: var(--color-prettylights-syntax-markup-changed-text);
background-color: var(--color-prettylights-syntax-markup-changed-bg);
}
.markdown-body .pl-mi2 {
color: var(--color-prettylights-syntax-markup-ignored-text);
background-color: var(--color-prettylights-syntax-markup-ignored-bg);
}
.markdown-body .pl-mdr {
font-weight: bold;
color: var(--color-prettylights-syntax-meta-diff-range);
}
.markdown-body .pl-ba {
color: var(--color-prettylights-syntax-brackethighlighter-angle);
}
.markdown-body .pl-sg {
color: var(--color-prettylights-syntax-sublimelinter-gutter-mark);
}
.markdown-body .pl-corl {
text-decoration: underline;
color: var(--color-prettylights-syntax-constant-other-reference-link);
}
.markdown-body g-emoji {
display: inline-block;
min-width: 1ch;
font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
font-size: 1em;
font-style: normal !important;
font-weight: var(--base-text-weight-normal, 400);
line-height: 1;
vertical-align: -0.075em;
}
.markdown-body g-emoji img {
width: 1em;
height: 1em;
}
.markdown-body .task-list-item {
list-style-type: none;
}
.markdown-body .task-list-item label {
font-weight: var(--base-text-weight-normal, 400);
}
.markdown-body .task-list-item.enabled label {
cursor: pointer;
}
.markdown-body .task-list-item + .task-list-item {
margin-top: 4px;
}
.markdown-body .task-list-item .handle {
display: none;
}
.markdown-body .task-list-item-checkbox {
margin: 0 0.2em 0.25em -1.4em;
vertical-align: middle;
}
.markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox {
margin: 0 -1.6em 0.25em 0.2em;
}
.markdown-body .contains-task-list {
position: relative;
}
.markdown-body .contains-task-list:hover .task-list-item-convert-container,
.markdown-body .contains-task-list:focus-within .task-list-item-convert-container {
display: block;
width: auto;
height: 24px;
overflow: visible;
clip: auto;
}
.markdown-body ::-webkit-calendar-picker-indicator {
filter: invert(50%);
}
html.dark {
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em;
}
code.hljs {
padding: 3px 5px;
}
.hljs {
color: #abb2bf;
background: #282c34;
}
.hljs-keyword,
.hljs-operator,
.hljs-pattern-match {
color: #f92672;
}
.hljs-function,
.hljs-pattern-match .hljs-constructor {
color: #61aeee;
}
.hljs-function .hljs-params {
color: #a6e22e;
}
.hljs-function .hljs-params .hljs-typing {
color: #fd971f;
}
.hljs-module-access .hljs-module {
color: #7e57c2;
}
.hljs-constructor {
color: #e2b93d;
}
.hljs-constructor .hljs-string {
color: #9ccc65;
}
.hljs-comment,
.hljs-quote {
color: #b18eb1;
font-style: italic;
}
.hljs-doctag,
.hljs-formula {
color: #c678dd;
}
.hljs-deletion,
.hljs-name,
.hljs-section,
.hljs-selector-tag,
.hljs-subst {
color: #e06c75;
}
.hljs-literal {
color: #56b6c2;
}
.hljs-addition,
.hljs-attribute,
.hljs-meta .hljs-string,
.hljs-regexp,
.hljs-string {
color: #98c379;
}
.hljs-built_in,
.hljs-class .hljs-title,
.hljs-title.class_ {
color: #e6c07b;
}
.hljs-attr,
.hljs-number,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-pseudo,
.hljs-template-variable,
.hljs-type,
.hljs-variable {
color: #d19a66;
}
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-symbol,
.hljs-title {
color: #61aeee;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
.hljs-link {
text-decoration: underline;
}
}
html {
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em;
}
code.hljs {
padding: 3px 5px;
&::-webkit-scrollbar {
height: 4px;
}
}
.hljs {
color: #383a42;
background: #fafafa;
}
.hljs-comment,
.hljs-quote {
color: #a0a1a7;
font-style: italic;
}
.hljs-doctag,
.hljs-formula,
.hljs-keyword {
color: #a626a4;
}
.hljs-deletion,
.hljs-name,
.hljs-section,
.hljs-selector-tag,
.hljs-subst {
color: #e45649;
}
.hljs-literal {
color: #0184bb;
}
.hljs-addition,
.hljs-attribute,
.hljs-meta .hljs-string,
.hljs-regexp,
.hljs-string {
color: #50a14f;
}
.hljs-attr,
.hljs-number,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-pseudo,
.hljs-template-variable,
.hljs-type,
.hljs-variable {
color: #986801;
}
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-symbol,
.hljs-title {
color: #4078f2;
}
.hljs-built_in,
.hljs-class .hljs-title,
.hljs-title.class_ {
color: #c18401;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
.hljs-link {
text-decoration: underline;
}
}
<script setup lang="ts">
import { nextTick, ref, watch } from 'vue'
import type { UploadFileInfo } from 'naive-ui'
import { fetchUpload } from '@/apis/upload'
interface Emit {
(e: 'afterUpload', data: UploadFileInfo[]): void
}
const props = defineProps({
saveFileArr: {
type: Array,
default: () => {
return []
},
},
fileType: {
type: String,
default: 'image-card',
},
maxNum: {
type: Number,
default: 1,
},
listType: {
type: String,
default: '.doc,.docx,.pdf,.xls,.xlsx,.zip,.rar,.jpg,.png,.jpeg,.svg,.gif',
},
})
const emit = defineEmits<Emit>()
const fileList = ref<UploadFileInfo[]>([])
const previewImageUrl = ref('')
const uploadPhotoRef = ref()
const imageRef = ref()
const showImage = false
watch(
() => props.saveFileArr,
(newValue) => {
fileList.value = []
if (newValue?.length) {
newValue?.forEach((item) => {
const params = {
id: new Date().getTime().toString(),
name: new Date().getTime().toString(),
status: 'finished' as any,
url: item as string,
}
fileList.value.push(params)
})
}
},
{ deep: true, immediate: true },
)
function handleUpload(file: any) {
if (file.event) {
const formdata = new FormData()
formdata.append('file', file.file.file)
fetchUpload(formdata).then((res) => {
if (res.code === 0) {
const fileData = {
id: file.file.id,
name: file.file.name,
status: 'finished' as any,
url: (res.data as string) || null,
}
fileList.value.push(fileData)
} else {
const fileData = {
id: file.file.id as string,
name: file.file.name,
status: 'error' as any,
}
fileList.value.push(fileData)
}
fileList.value = fileList.value.filter((item) => {
return item.status !== 'pending'
})
emit('afterUpload', fileList.value)
})
} else {
const fileId = file.file.id
fileList.value = fileList.value.filter((item) => {
return item.id !== fileId
})
emit('afterUpload', fileList.value)
}
}
function handlePreview(file: UploadFileInfo) {
const { url } = file
previewImageUrl.value = url as string
nextTick(() => {
imageRef.value.click()
})
}
</script>
<template>
<NUpload
ref="uploadPhotoRef"
v-model:file-list="fileList"
:max="props.maxNum"
:list-type="props.fileType"
@change="handleUpload"
@preview="handlePreview"
/>
<NImage
v-show="showImage"
ref="imageRef"
width="100"
:src="previewImageUrl"
:show-toolbar="false"
:preview-src="previewImageUrl"
/>
</template>
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { BASE_URLS } from '@/config/base-url'
import { useUserStore } from '@/store/modules/user'
const EVENT_SOURCE_BASE_URL = `${BASE_URLS[window.ENV || 'DEV']}`
export function fetchCustomEventSource(config: {
path: string
payload: any
controller: AbortController
onMessage: (data: string) => void
onRequestError: (err: any) => void
onError?: (err: any) => void
onFinally?: () => void
}) {
const userStore = useUserStore()
let responseError = false
fetchEventSource(`${EVENT_SOURCE_BASE_URL}${config.path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Request-Token': userStore.token || '',
},
body: JSON.stringify(config.payload || {}),
signal: config.controller?.signal,
openWhenHidden: true,
onmessage: (e) => {
if (e.data === '[DONE]' && !responseError) {
config.onMessage(e.data)
config.onFinally && config.onFinally()
return
}
try {
const data = JSON.parse(e.data)
if (data.code === -10) {
window.$message.info('身份已过期,请重新登陆')
config.onError && config.onError(data)
userStore.logout()
return
}
if (data.code === -1) {
responseError = true
window.$message.error(data.message)
config.controller?.abort()
config.onFinally && config.onFinally()
config.onError && config.onError(data)
return
}
config.onMessage(data.message)
} catch (err) {
config.onRequestError(err)
config.onFinally && config.onFinally()
}
},
onclose: () => {},
onerror: (err) => {
config.onRequestError(err)
window.$message.error(err.message || '操作失败请重试')
config.onFinally && config.onFinally()
throw err
},
})
}
import { computed } from 'vue'
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
export function useLayoutConfig() {
const breakpoints = useBreakpoints(breakpointsTailwind)
const isMobileLayout = breakpoints.smaller('sm')
const isMobile = computed(() => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
})
return { isMobileLayout, isMobile }
}
import type { Ref } from 'vue'
import { nextTick, ref } from 'vue'
type ScrollElement = HTMLDivElement | null
interface ScrollReturn {
scrollRef: Ref<ScrollElement>
scrollToBottom: () => Promise<void>
scrollToTop: () => Promise<void>
scrollToBottomIfAtBottom: () => Promise<void>
}
export function useScroll(): ScrollReturn {
const scrollRef = ref<ScrollElement>(null)
const scrollToBottom = async () => {
await nextTick()
if (scrollRef.value) scrollRef.value.scrollTop = scrollRef.value.scrollHeight
}
const scrollToTop = async () => {
await nextTick()
if (scrollRef.value) scrollRef.value.scrollTop = 0
}
const scrollToBottomIfAtBottom = async () => {
await nextTick()
if (scrollRef.value) {
const threshold = 100 // 阈值,表示滚动条到底部的距离阈值
const distanceToBottom = scrollRef.value.scrollHeight - scrollRef.value.scrollTop - scrollRef.value.clientHeight
if (distanceToBottom <= threshold) scrollRef.value.scrollTop = scrollRef.value.scrollHeight
}
}
return {
scrollRef,
scrollToBottom,
scrollToTop,
scrollToBottomIfAtBottom,
}
}
import { onMounted, nextTick, onUnmounted, ref, Ref, computed } from 'vue'
function debounce(handler: CallableFunction, delay: number) {
let timer: number | undefined = undefined
return () => {
clearTimeout(timer)
timer = setTimeout(handler, delay || 0)
}
}
export default function useTableScrollY(unavailableHeight = 0) {
const pageContentWrapRef = ref<HTMLElement | null>(null)
const pageContentWrapOffsetHeight = ref(0)
const tableContentY = computed(() => {
// 页面高度 - 不可用的高度
return pageContentWrapOffsetHeight.value - unavailableHeight
})
const updatePageContentWrapOffsetHeightWithDebounce = debounce(() => {
updatePageContentWrapOffsetHeight(pageContentWrapRef, pageContentWrapOffsetHeight)
}, 100)
onMounted(() => {
nextTick(() => {
setTimeout(() => {
updatePageContentWrapOffsetHeight(pageContentWrapRef, pageContentWrapOffsetHeight)
setTimeout(() => {
updatePageContentWrapOffsetHeight(pageContentWrapRef, pageContentWrapOffsetHeight)
setTimeout(() => {
updatePageContentWrapOffsetHeight(pageContentWrapRef, pageContentWrapOffsetHeight)
}, 200)
}, 200)
}, 200)
})
window.addEventListener('resize', updatePageContentWrapOffsetHeightWithDebounce)
})
onUnmounted(() => {
window.removeEventListener('resize', updatePageContentWrapOffsetHeightWithDebounce)
})
function updatePageContentWrapOffsetHeight(
pageContentWrapRef: Ref<HTMLElement | null>,
pageContentWrapOffsetHeight: Ref<number>,
) {
if (pageContentWrapRef.value) {
pageContentWrapOffsetHeight.value = pageContentWrapRef.value.offsetHeight || 0
}
}
return {
tableContentY,
pageContentWrapRef,
}
}
import { Directive } from 'vue'
// 创建遮罩层
function createMask(): HTMLElement {
const mask = document.createElement('div')
mask.className = 'loading-mask'
mask.style.position = 'absolute'
mask.style.top = '0px'
mask.style.left = '0px'
mask.style.width = '100%'
mask.style.height = '100%'
mask.style.backgroundColor = 'rgba(122, 122, 122, 0.8)'
mask.style.display = 'flex'
mask.style.justifyContent = 'center'
mask.style.alignItems = 'center'
const loadingIcon = document.createElement('div')
loadingIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 2A10 10 0 1 0 22 12A10 10 0 0 0 12 2Zm0 18a8 8 0 1 1 8-8A8 8 0 0 1 12 20Z" opacity="0.5" />
<path fill="currentColor" d="M20 12h2A10 10 0 0 0 12 2V4A8 8 0 0 1 20 12Z">
<animateTransform attributeName="transform" dur="1s" from="0 12 12" repeatCount="indefinite" to="360 12 12" type="rotate" />
</path>
</svg>`
loadingIcon.style.color = '#2468f2'
loadingIcon.style.width = '44px'
loadingIcon.style.height = '44px'
mask.appendChild(loadingIcon)
return mask
}
// 自定义指令定义
const loading: Directive<HTMLElement, boolean> = {
mounted(el, binding) {
let isBody = false
if (binding.modifiers.body) {
isBody = true
}
const value = binding.value
if (value) {
if (isBody) {
document.body.appendChild(createMask())
} else {
el.appendChild(createMask())
}
}
},
updated(el, binding) {
let isBody = false
if (binding.modifiers.body) {
isBody = true
}
const value = binding.value
if (value) {
if (isBody) {
const mask = document.querySelector('.loading-mask')
if (!mask) {
document.body.appendChild(createMask())
}
} else {
const mask = el.querySelector('.loading-mask')
if (!mask) {
el.appendChild(createMask())
}
}
} else {
if (isBody) {
const mask = document.querySelector('.loading-mask')
if (mask) {
document.body.removeChild(mask)
}
} else {
const mask = el.querySelector('.loading-mask')
if (mask) {
el.removeChild(mask)
}
}
}
},
unmounted(el, binding) {
let isBody = false
if (binding.modifiers.body) {
isBody = true
}
if (isBody) {
const mask = document.querySelector('.loading-mask')
if (mask) {
document.body.removeChild(mask)
}
} else {
const mask = el.querySelector('.loading-mask')
if (mask) {
el.removeChild(mask)
}
}
},
}
export default loading
...@@ -39,9 +39,9 @@ function handleUpdateValue(_key: string, menuItemOption: MenuOption) { ...@@ -39,9 +39,9 @@ function handleUpdateValue(_key: string, menuItemOption: MenuOption) {
router.push({ name: menuItemOption.routeName }) router.push({ name: menuItemOption.routeName })
} }
// function handleToPersonAppSettingPage() { function handleToPersonAppSettingPage() {
// router.push({ name: 'PersonalAppSetting' }) router.push({ name: 'PersonalAppSetting' })
// } }
function handleDropdownSelect(key: string) { function handleDropdownSelect(key: string) {
if (key === 'logout') { if (key === 'logout') {
...@@ -69,7 +69,7 @@ function handleToLogin() { ...@@ -69,7 +69,7 @@ function handleToLogin() {
class="mx-auto my-[14px] flex h-[23px] w-[90px] bg-[url('@/assets/images/page-logo.png')] bg-contain bg-center bg-no-repeat" class="mx-auto my-[14px] flex h-[23px] w-[90px] bg-[url('@/assets/images/page-logo.png')] bg-contain bg-center bg-no-repeat"
/> />
<!-- <div class="py-5"> <div class="py-5">
<button <button
class="bg-theme-color flex h-[40px] w-[203px] items-center justify-center rounded-md text-white outline-none hover:opacity-80" class="bg-theme-color flex h-[40px] w-[203px] items-center justify-center rounded-md text-white outline-none hover:opacity-80"
@click="handleToPersonAppSettingPage" @click="handleToPersonAppSettingPage"
...@@ -77,7 +77,7 @@ function handleToLogin() { ...@@ -77,7 +77,7 @@ function handleToLogin() {
<CustomIcon icon="ic:outline-add" class="mr-1 h-[18px] w-[18px]" /> <CustomIcon icon="ic:outline-add" class="mr-1 h-[18px] w-[18px]" />
<span>创建应用</span> <span>创建应用</span>
</button> </button>
</div> --> </div>
<ul> <ul>
<li <li
......
...@@ -8,6 +8,7 @@ import '@/styles/reset.scss' ...@@ -8,6 +8,7 @@ import '@/styles/reset.scss'
import 'virtual:uno.css' import 'virtual:uno.css'
import '@unocss/reset/normalize.css' import '@unocss/reset/normalize.css'
import '@unocss/reset/tailwind.css' import '@unocss/reset/tailwind.css'
import LoadingDirective from './directives/loading'
async function bootstrap() { async function bootstrap() {
const app = createApp(App) const app = createApp(App)
...@@ -22,6 +23,8 @@ async function bootstrap() { ...@@ -22,6 +23,8 @@ async function bootstrap() {
meta.name = 'naive-ui-style' meta.name = 'naive-ui-style'
document.head.appendChild(meta) document.head.appendChild(meta)
app.directive('loading', LoadingDirective)
app.mount('#app') app.mount('#app')
} }
......
...@@ -2,7 +2,7 @@ import type { Router } from 'vue-router' ...@@ -2,7 +2,7 @@ import type { Router } from 'vue-router'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
/** 路由白名单 */ /** 路由白名单 */
const whitePathList = ['/login'] const whitePathList = ['/login', '/home']
export function createRouterGuards(router: Router) { export function createRouterGuards(router: Router) {
router.beforeEach((to) => { router.beforeEach((to) => {
...@@ -18,6 +18,10 @@ export function createRouterGuards(router: Router) { ...@@ -18,6 +18,10 @@ export function createRouterGuards(router: Router) {
if (whitePathList.includes(to.path)) { if (whitePathList.includes(to.path)) {
return true return true
} }
//忽略校验直接跳过
if (to.meta.ignoreAuth) {
return true
}
if (!userStore.isLogin && !whitePathList.includes(to.fullPath)) { if (!userStore.isLogin && !whitePathList.includes(to.fullPath)) {
return { path: '/login', query: { redirect: encodeURIComponent(to.fullPath) } } return { path: '/login', query: { redirect: encodeURIComponent(to.fullPath) } }
......
import { type RouteRecordRaw } from 'vue-router'
export default [
{
path: '/personal-space',
name: 'PersonalSpace',
meta: {
rank: 1001,
title: '个人空间',
icon: 'mingcute:user-2-line',
belong: 'personal-space',
},
component: () => import('@/layout/index.vue'),
redirect: '/personalSpaceLayout',
children: [
{
path: '/personalSpaceLayout',
name: 'PersonalSpaceLayout',
meta: {
rank: 1001,
title: '个人空间',
belong: 'PersonalSpace',
},
component: () => import('@/views/personal-space/personal-space.vue'),
redirect: '/personalSpace/app',
children: [
{
path: '/personalSpace/app',
name: 'PersonalSpaceApp',
meta: {
rank: 1001,
title: 'Agent应用',
belong: 'PersonalSpace',
},
component: () => import('@/views/personal-space/personal-app/personal-app.vue'),
},
],
},
],
},
{
path: '/personal-app-setting/:agentId?/:tabKey?',
name: 'PersonalAppSetting',
meta: {
rank: 1001,
title: '应用设置',
icon: 'mingcute:user-2-line',
belong: 'PersonalAppSetting',
hideSideMenItem: true,
},
component: () => import('@/views/personal-space/personal-app-setting/personal-app-setting.vue'),
},
] as RouteRecordRaw[]
import { type RouteRecordRaw } from 'vue-router'
export default [
{
path: '/share/web_source/:agentId?',
name: 'ShareWebApplication',
meta: {
rank: 1001,
title: '我的Agent应用',
hideSideMenItem: true,
ignoreAuth: true,
},
component: () => import('@/views/share/share-application-web.vue'),
},
{
path: '/share/mobile_source/:agentId?',
name: 'ShareMobileApplication',
meta: {
rank: 1001,
title: '我的Agent应用',
hideSideMenItem: true,
ignoreAuth: true,
},
component: () => import('@/views/share/share-application-mobile.vue'),
},
] as RouteRecordRaw[]
import { defineStore } from 'pinia'
import { PersonalAppConfigState } from '../types/personal-app-config'
export function defaultPersonalAppConfigState(): PersonalAppConfigState {
return {
baseInfo: {
agentId: '',
agentTitle: '我的Agent应用',
agentAvatar: 'https://gsst-poe-sit.gz.bcebos.com/data/20240911/1726041369632.webp',
agentDesc: '',
agentSystem: '',
agentPublishStatus: 'draft',
},
commConfig: {
preamble: '',
featuredQuestions: [],
continuousQuestionStatus: 'default',
continuousQuestionSystem: '',
continuousQuestionTurn: 3,
},
knowledgeConfig: {
knowledgeIds: [],
},
commModelConfig: {
largeModel: 'ERNIE-4.0-8K',
topP: 0.0,
communicationTurn: 0,
},
modifiedTime: new Date(),
}
}
export const usePersonalAppConfigStore = defineStore('personal-app-config-store', {
state: (): PersonalAppConfigState => defaultPersonalAppConfigState(),
actions: {
updatePersonalAppConfigState(personalAppConfigState: Partial<PersonalAppConfigState>) {
this.$state = { ...this.$state, ...personalAppConfigState }
},
resetPersonalAppConfigState() {
this.$state = defaultPersonalAppConfigState()
},
},
})
export interface PersonalAppConfigState {
baseInfo: {
agentId: string //应用ID
agentTitle: string //应用标题
agentAvatar: string //应用头像
agentDesc: string //应用描述
agentSystem: string //角色指令
agentPublishStatus: 'draft' | 'publish' //发布状态 draft-草稿 publish-发布
}
commConfig: {
preamble: string //开场白
featuredQuestions: string[] //推荐问
continuousQuestionStatus: 'default' | 'customizable' | 'close' //追问状态
continuousQuestionSystem: string // 追问提示词 customizable时必填
continuousQuestionTurn: number // 追问轮次 1-5 customizable时必填
}
knowledgeConfig: {
knowledgeIds: string[] //知识库ID
}
commModelConfig: {
largeModel: string //大模型
topP: number //多样性 0-1.00
communicationTurn: number //参考对话轮次 0-100
}
modifiedTime: Date
}
...@@ -3,3 +3,24 @@ body { ...@@ -3,3 +3,24 @@ body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
} }
@mixin custom-scrollbar($scrollbarSize: 5px, $scrollbarColor: #ededed) {
::-webkit-scrollbar {
width: $scrollbarSize;
height: $scrollbarSize;
}
::-webkit-scrollbar-corner {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: $scrollbarColor;
border-radius: 5px;
}
::-webkit-scrollbar-track {
background: transparent;
border-radius: 2px;
}
}
import clipboard from 'clipboardy'
export function copyToClip(text: string) {
return clipboard.write(text)
}
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'
import 'dayjs/locale/zh-cn'
dayjs.extend(utc)
dayjs.extend(timezone)
export function formatDateTime(date: string | number | Date, format: string = 'YYYY-MM-DD HH:mm:ss'): string {
return dayjs(date).format(format)
}
import { h } from 'vue'
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import { formatDateTime } from '@/utils/date-formatter'
export function createChannelPublishColumn(
handleChannelPublishTableAction: (actionType: string, linkUrl: string) => void,
) {
return [
{
title: '发布渠道',
key: 'channel',
align: 'left',
width: 540,
render() {
return h(
'div',
{
style: {
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
},
},
{
default: () => [
h(CustomIcon, {
width: '24px',
icon: 'icon-park-solid:computer',
color: '#2468f2',
}),
h(
'div',
{
style: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'flex-start',
fontSize: '14px',
marginLeft: '12px',
},
},
{
default: () => [
h(
'span',
{},
{
default: () => '网页端',
},
),
h(
'span',
{
style: {
color: '#84868c',
},
},
{
default: () => '可通过PC或移动设备立即开始对话',
},
),
],
},
),
],
},
)
},
},
{
title: '状态',
key: 'agentPublishStatus',
align: 'left',
width: 220,
render() {
return h(
'div',
{
style: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'flex-start',
},
},
{
default: () => [
h(
'div',
{
style: {
background: '#34a853',
borderRadius: '4px',
padding: '2px 14px',
color: '#fff',
marginBottom: '4px',
},
},
{
default: () => '已发布',
},
),
h(
'span',
{
style: {
color: '#84868c',
},
},
{
default: () => formatDateTime(new Date()) + '发布',
},
),
],
},
)
},
},
{
title: '操作',
key: 'action',
align: 'left',
width: '460',
render(row: { linkUrl: string }) {
return h(
'div',
{
style: {
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
},
},
{
default: () => [
h(
'div',
{
style: {
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
fontSize: '14px',
padding: '4px 12px',
background: '#f7f7f9',
borderColor: '#2468f2',
color: '#2468f2',
},
className: 'cursor-pointer rounded-md border hover:opacity-80',
onClick: () => handleChannelPublishTableAction('accessPage', row.linkUrl),
},
{
default: () => [
h(CustomIcon, { icon: 'lets-icons:view', style: { marginRight: '6px', fontSize: '16px' } }),
h(
'span',
{},
{
default: () => '立即访问',
},
),
],
},
),
h(
'div',
{
style: {
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
fontSize: '14px',
marginLeft: '16px',
padding: '4px 12px',
background: '#f7f7f9',
},
className: 'cursor-pointer hover:text-theme-color rounded-md border hover:border-theme-color',
onClick: () => handleChannelPublishTableAction('copyLink', row.linkUrl),
},
{
default: () => [
h(CustomIcon, { icon: 'pepicons-pop:share-android-circle', style: { marginRight: '6px' } }),
h(
'span',
{},
{
default: () => '分享链接',
},
),
],
},
),
],
},
)
},
},
]
}
<script setup lang="ts">
import { computed, ref, watchEffect } from 'vue'
import CustomModal from '@/components/custom-modal/custom-modal.vue'
interface Props {
modalTitle: string
isShowModal: boolean
btnLoading: boolean
questionSystem: string
}
interface Emits {
(e: 'update:isShowModal', value: boolean): void
(e: 'comfirm', value: string): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const continuousQuestionSystem = ref('')
watchEffect(() => {
continuousQuestionSystem.value = props.questionSystem
})
const showModal = computed({
get() {
return props.isShowModal
},
set(value: boolean) {
emit('update:isShowModal', value)
},
})
function handleAdditionalPrompt() {
emit('comfirm', continuousQuestionSystem.value)
}
</script>
<template>
<CustomModal
v-model:is-show="showModal"
:title="modalTitle"
:btn-loading="btnLoading"
:height="636"
:width="520"
@confirm="handleAdditionalPrompt"
>
<template #content>
<p class="mb-3 select-none text-[#84868c]">可在追问prompt中指引追问的字数、风格和内容范围。</p>
<NInput
v-model:value="continuousQuestionSystem"
type="textarea"
:rows="19"
maxlength="1000"
show-count
placeholder="请输入追问prompt"
class="rounded-lg!"
/>
</template>
</CustomModal>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Preamble from './preamble.vue'
import MessageList from './message-list.vue'
import FooterInput from './footer-input.vue'
const messageListRef = ref<InstanceType<typeof MessageList> | null>(null)
const messageList = ref<ConversationMessageItem[]>([])
function handleAddMessageItem(messageItem: ConversationMessageItem) {
messageList.value.push(messageItem)
}
function handleUpdateSpecifyMessageItem(messageItemIndex: number, newObj: Partial<ConversationMessageItem>) {
if (messageList.value[messageItemIndex]) {
Object.entries(newObj).forEach(([k, v]) => {
;(messageList.value[messageItemIndex] as any)[k as keyof typeof newObj] = v
})
}
}
function handleDeleteLastMessageItem() {
messageList.value.pop()
}
function handleUpdatePageScroll() {
messageListRef.value?.scrollToBottom()
}
function handleClearAllMessage() {
window.$dialog.warning({
title: '确认要清空对话吗?',
content: '清空对话将清空调试区域所有历史对话内容,确定清空对话吗?',
negativeText: '取消',
positiveText: '确定',
onPositiveClick: () => {
messageList.value = []
window.$message.success('清空成功')
},
})
}
</script>
<template>
<div class="flex h-full min-w-[300px] flex-1 flex-col overflow-hidden bg-[#f2f5f9]">
<p class="mb-[18px] px-5 py-[18px] text-base">预览与调试</p>
<div class="flex w-full flex-1 overflow-hidden">
<div v-show="messageList.length === 0" class="w-full">
<Preamble />
</div>
<div v-show="messageList.length > 0" class="w-full">
<MessageList ref="messageListRef" :message-list="messageList" />
</div>
</div>
<FooterInput
:message-list="messageList"
@add-message-item="handleAddMessageItem"
@update-specify-message-item="handleUpdateSpecifyMessageItem"
@delete-last-message-item="handleDeleteLastMessageItem"
@update-page-scroll="handleUpdatePageScroll"
@clear-all-message="handleClearAllMessage"
/>
</div>
</template>
<style scoped lang="scss">
@include custom-scrollbar(6px);
</style>
<script lang="ts" setup>
import { readonly } from 'vue'
import { useRouter } from 'vue-router'
import { createChannelPublishColumn } from '../columns'
import { INDEX_URLS } from '@/config/base-url'
import useTableScrollY from '@/composables/useTableScrollY'
import { copyToClip } from '@/utils/copy'
const { pageContentWrapRef, tableContentY } = useTableScrollY()
const router = useRouter()
const channelPublishList = readonly([
{
linkUrl: `${INDEX_URLS[window.ENV || 'DEV']}share/web_source/${router.currentRoute.value.params.agentId}`,
},
])
const columns = createChannelPublishColumn(handleClickChannelPublishTableAction)
function handleClickChannelPublishTableAction(actionType: string, channleUrl: string) {
switch (actionType) {
case 'accessPage':
handleAccessPage(channleUrl)
break
case 'copyLink':
handleCopyShareLink(channleUrl)
break
}
}
function handleAccessPage(channleUrl: string) {
location.href = channleUrl
}
function handleCopyShareLink(channleUrl: string) {
copyToClip(channleUrl)
window.$message.success('链接复制成功,快分享给你的好友吧!')
}
</script>
<template>
<div ref="pageContentWrapRef" class="h-full overflow-hidden p-5">
<NDataTable
:bordered="true"
:bottom-bordered="true"
:single-line="false"
:data="channelPublishList"
:columns="columns"
:max-height="tableContentY"
:scroll-x="1220"
/>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { SelectOption, UploadFileInfo } from 'naive-ui'
import { useThrottleFn } from '@vueuse/core'
import AdditionalPromptModal from './additional-prompt-modal.vue'
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import UploadPhoto from '@/components/upload-photo/upload-photo.vue'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import {
fetchGetDebugApplicationInfo,
fetchGetLargeModelList,
fetchSaveAgentApplication,
} from '@/apis/agent-application'
const router = useRouter()
const personalAppConfigStore = usePersonalAppConfigStore()
const defaultPrompt = `#角色设定
作为一个____,你的任务是____。
#组件能力
你具备____能力。
#要求与限制
1.输出内容的风格要求____
2.输出结果的格式为____
3.输出内容的字数限制不超过____
......`
const questionSettingOptions = [
{
label: '默认',
value: 'default',
style: { fontSize: '12px' },
},
{
label: '自定义',
value: 'customizable',
style: { fontSize: '12px' },
},
{
label: '关闭',
value: 'close',
style: { fontSize: '12px' },
},
]
const modalListOptions = reactive<SelectOption[]>([])
const commConfigExpandedNames = ref<string[]>(['continuousQuestion'])
const showAdditionalPromptModal = ref(false)
const isInitGetAgentAppDetail = ref(false)
// const generateAgentAvatarLoading = ref(false) // 是否正在生成头像
const generatePreambleLoading = ref(false) // 是否正在生成开场白
const generateFeaturedQuestionsLoading = ref(false) // 是否正在生成推荐词
const personalAppConfig = computed(() => {
return personalAppConfigStore.$state
})
const continuousQuestionStatusText = computed(() => {
return personalAppConfig.value.commConfig.continuousQuestionStatus === 'default'
? '默认'
: personalAppConfig.value.commConfig.continuousQuestionStatus === 'customizable'
? '自定义'
: '关闭'
})
watch(
() => personalAppConfig.value,
() => {
!isInitGetAgentAppDetail.value && handleUpdatePersonalAppId()
},
{ deep: true, once: true },
)
watch(
() => personalAppConfig.value,
() => {
!isInitGetAgentAppDetail.value && handleSavePersonalAppConfig()
},
{ deep: true },
)
onMounted(async () => {
if (router.currentRoute.value.params.agentId) {
isInitGetAgentAppDetail.value = true
await handleGetAgentApplicationDetail(router.currentRoute.value.params.agentId as string)
isInitGetAgentAppDetail.value = false
}
await handleGetLargeModelList()
})
const handleSavePersonalAppConfig = useThrottleFn(
async () => {
personalAppConfig.value.baseInfo.agentId && (await handleSaveAgentApplication())
},
2000,
true,
)
async function handleGetAgentApplicationDetail(agentId: string) {
const res = await fetchGetDebugApplicationInfo<PersonalAppConfigState>(agentId)
if (res.code === 0) {
personalAppConfigStore.updatePersonalAppConfigState({
...res.data,
baseInfo: {
...res.data.baseInfo,
agentAvatar:
res.data.baseInfo.agentAvatar || 'https://gsst-poe-sit.gz.bcebos.com/data/20240911/1726041369632.webp',
},
})
}
}
async function handleGetLargeModelList() {
const res = await fetchGetLargeModelList<{ modelName: string }[]>()
res.data.forEach((item) => {
modalListOptions.push({
label: item.modelName,
value: item.modelName,
style: { fontSize: '12px' },
})
})
}
// 保存应用配置
async function handleSaveAgentApplication() {
await fetchSaveAgentApplication<PersonalAppConfigState>(personalAppConfigStore.$state)
}
// 更新保存应用ID
async function handleUpdatePersonalAppId() {
const res = await fetchSaveAgentApplication<PersonalAppConfigState>(personalAppConfigStore.$state)
if (res.code === 0) {
personalAppConfigStore.updatePersonalAppConfigState({
baseInfo: { ...personalAppConfigStore.baseInfo, agentId: res.data.baseInfo.agentId },
})
}
}
function handleSettingDefaultPrompt() {
personalAppConfig.value.baseInfo.agentSystem = defaultPrompt
}
function handleUploadAppAvatar(file: UploadFileInfo[]) {
personalAppConfig.value.baseInfo.agentAvatar = file?.[0]?.url || ''
}
function handleQuestionSettingChange(continuousQuestionStatus: string) {
if (continuousQuestionStatus !== 'customizable') {
personalAppConfig.value.commConfig.continuousQuestionSystem = ''
personalAppConfig.value.commConfig.continuousQuestionTurn = 3
}
}
function handleShowAdditionalPromptModal() {
showAdditionalPromptModal.value = true
}
function handleAdditionalPrompt(continuousQuestionSystem: string) {
personalAppConfig.value.commConfig.continuousQuestionSystem = continuousQuestionSystem
showAdditionalPromptModal.value = false
}
function handleUpdateCommConfigExpandedNames(expandedNames: any) {
commConfigExpandedNames.value = expandedNames
}
// function handleGenerateAgentAvatar() {
// personalAppConfig.value.baseInfo.agentDesc = '22'
// generateAgentAvatarLoading.value = true
// setTimeout(() => {
// generateAgentAvatarLoading.value = false
// }, 2000)
// }
// function handleAIGeneratePreamble() {
// !commConfigExpandedNames.value.includes('preamble') && commConfigExpandedNames.value.push('preamble')
// generatePreambleLoading.value = true
// setTimeout(() => {
// generatePreambleLoading.value = false
// }, 2000)
// }
// function handleAIGenerateFeaturedQuestions() {
// !commConfigExpandedNames.value.includes('featuredQuestions') &&
// commConfigExpandedNames.value.push('featuredQuestions')
// generateFeaturedQuestionsLoading.value = true
// setTimeout(() => {
// generateFeaturedQuestionsLoading.value = false
// }, 2000)
// }
</script>
<template>
<div class="flex h-full w-[800px] flex-col xl:w-[1000px]">
<div
class="flex h-[56px] w-full items-center justify-between border-r border-[#e8e9eb] px-5 text-[#333] shadow-[inset_0_-1px_#e8e9eb]"
>
<span class="font-600 mr-4 text-base">应用配置</span>
<div>
<NPopover placement="bottom" trigger="click" style="width: 420px">
<template #trigger>
<div
class="hover:border-theme-color flex cursor-pointer items-center justify-between rounded-md border border-[#d4d6d9] px-3 py-[7px]"
>
<img src="@/assets/images/lingjing-icon.png" class="mr-1 h-4 w-4" />
<span class="line-clamp-1 max-w-[100px] text-xs text-[#5c5f66]">
{{ personalAppConfig.commModelConfig.largeModel }}
</span>
<CustomIcon icon="mingcute:down-line" class="ml-1.5 text-base outline-none" />
</div>
</template>
<div class="mb-2 mt-[6px] flex items-center">
<span class="font-500 mr-3 text-sm text-[#151b26]">问答模型</span>
<span class="rounded bg-[#f2f5f9] px-1 text-xs text-[#5c5f66]">用于总结生成回复结果</span>
</div>
<NSelect
v-model:value="personalAppConfig.commModelConfig.largeModel"
class="model-select"
:options="modalListOptions"
/>
<div class="mt-4 text-xs">
<div class="mb-2.5 flex h-[34px] items-center justify-between">
<div class="flex w-[105px] items-center">
<span>多样性:</span>
<NPopover trigger="hover">
<template #trigger>
<CustomIcon
icon="mingcute:question-line"
class="cursor-pointer text-base text-[#999] outline-none"
/>
</template>
<span class="text-xs">
用于控制模型输出的多样性。推荐值为 0,数值越大,模型每次输出内容的差异性越大
</span>
</NPopover>
</div>
<div class="mx-5 flex flex-1">
<NSlider
v-model:value="personalAppConfig.commModelConfig.topP"
:default-value="0"
:step="0.01"
:min="0"
:max="1"
/>
<span class="ml-4 w-[30px]">{{ personalAppConfig.commModelConfig.topP }}</span>
</div>
<NInputNumber
v-model:value="personalAppConfig.commModelConfig.topP"
placeholder=""
:step="0.01"
:min="0"
:max="1"
size="small"
class="w-[90px]! text-xs!"
/>
</div>
<div class="mb-2.5 flex h-[34px] items-center justify-between">
<div class="flex w-[105px] items-center">
<span>参考对话轮数:</span>
<NPopover trigger="hover">
<template #trigger>
<CustomIcon
icon="mingcute:question-line"
class="cursor-pointer text-base text-[#999] outline-none"
/>
</template>
<span class="text-xs">
传入大模型上下文的最大对话轮数。推荐值为2,数值越大,多轮对话中上下文相关性越强,但Tokens消耗越多
</span>
</NPopover>
</div>
<div class="mx-5 flex flex-1">
<NSlider
v-model:value="personalAppConfig.commModelConfig.communicationTurn"
:default-value="3"
:step="1"
:min="0"
:max="100"
/>
<span class="ml-4 w-[30px]">{{ personalAppConfig.commModelConfig.communicationTurn }}</span>
</div>
<NInputNumber
v-model:value="personalAppConfig.commModelConfig.communicationTurn"
:step="1"
:min="0"
:max="100"
size="small"
class="w-[90px]!"
placeholder=""
/>
</div>
</div>
</NPopover>
</div>
</div>
<div class="flex h-[calc(100vh-56px-56px)]">
<div class="flex h-full flex-1 flex-col overflow-auto border-r border-[#e8e9eb] py-4">
<div class="mb-1 flex h-6 items-center px-5 leading-6">
<CustomIcon icon="mdi:user" class="mr-1.5 text-lg" />
<span>应用设定</span>
</div>
<NCollapse :default-expanded-names="['1']" class="px-5">
<template #arrow>
<CustomIcon icon="gravity-ui:caret-right" />
</template>
<NCollapseItem title="基本信息" name="1" class="my-[13px]!">
<div class="justify-left flex items-start pl-5">
<div class="mr-2 h-[72px] w-[72px]">
<UploadPhoto
:save-file-arr="[personalAppConfig.baseInfo.agentAvatar]"
@after-upload="handleUploadAppAvatar"
/>
<!-- <div
class="text-theme-color mt-3 flex h-[28px] items-center justify-between rounded-md border border-[#d4d6d9] px-2"
:class="
generateAgentAvatarLoading
? 'cursor-not-allowed opacity-50'
: 'cursor-pointer hover:border-[#d4e5ff] hover:bg-[#e6f0ff]'
"
@click="handleGenerateAgentAvatar"
>
<div class="mt-[-2px] h-[14px] w-[14px] bg-[url(@/assets/svgs/star.svg)] bg-[length:100%_100%]" />
<span class="text-xs">AI生成</span>
</div> -->
</div>
<div class="flex flex-1">
<NForm label-placement="left" class="flex-1">
<NFormItem feedback-style="height: 14px">
<NInput
v-model:value="personalAppConfig.baseInfo.agentTitle"
:maxlength="50"
show-count
placeholder="请输入应用名称"
class="h-[32px]! text-xs! rounded-md!"
/>
</NFormItem>
<NFormItem feedback-style="display:none">
<NInput
v-model:value="personalAppConfig.baseInfo.agentDesc"
type="textarea"
:maxlength="100"
:rows="3"
show-count
placeholder="请描述你的应用,该描述将在应用发布后固定展示"
class="not-resize rounded-md! text-xs!"
size="small"
/>
</NFormItem>
</NForm>
</div>
</div>
</NCollapseItem>
</NCollapse>
<div class="flex flex-1 flex-col border-t border-[#e8e9eb] px-5">
<div class="my-[13px] flex items-center justify-between">
<div class="flex items-center pl-6">
<span class="select-none">角色指令</span>
<NPopover style="width: 520px" trigger="hover">
<template #trigger>
<CustomIcon
icon="mingcute:question-line"
class="ml-1 cursor-pointer text-base text-[#999] outline-none"
/>
</template>
<span class="text-xs">
通过角色指令(Instruction)功能,你能够精确设定Agent应用的作用范围。包括指定应用将扮演的角色、能够使用的组件以及输出结果的格式与风格。此外,你还可以规定应用不得执行哪些操作等。
</span>
</NPopover>
</div>
<NPopover style="width: 200px" content-style="padding: 10px 0;" trigger="hover">
<template #trigger>
<div
class="text-theme-color flex cursor-pointer items-center hover:opacity-80"
@click="handleSettingDefaultPrompt"
>
<CustomIcon icon="solar:book-2-broken" class="mr-3 text-base" />
<span class="text-xs">模板</span>
</div>
</template>
<p class="text-xs" style="white-space: pre-wrap">{{ defaultPrompt }}</p>
</NPopover>
</div>
<div class="flex flex-1 p-1 pl-6">
<NInput
v-model:value="personalAppConfig.baseInfo.agentSystem"
type="textarea"
class="prompt-input not-resize text-xs! flex-1"
:maxlength="1400"
show-count
placeholder="请输入希望角色完成的任务目标、具备的组件能力以及对输出答案的要求与限制等"
/>
</div>
</div>
</div>
<div class="h-full flex-1 overflow-auto border-r border-[#e8e9eb] py-4">
<div class="flex h-6 items-center px-5 leading-6">
<CustomIcon icon="streamline:decent-work-and-economic-growth-solid" class="mr-1.5 text-base" />
<span>能力扩展</span>
</div>
<section class="border-b border-[#e8e9eb] px-5">
<div class="pt-4">
<h2 class="my-3 text-[#84868c]">知识</h2>
<NCollapse :trigger-areas="['main', 'arrow']">
<template #arrow>
<CustomIcon icon="gravity-ui:caret-right" />
</template>
<NCollapseItem title="知识库" name="1" class="my-[13px]!">
<span class="text-xs text-[#84868c]">
引用文本数据、表格型知识数据(含FAQ问答,多列索引问答)以及网页数据,实现知识库问答,应用最多可关联5个知识库,请详细填写知识库描述信息以提高问答准确率
</span>
</NCollapseItem>
</NCollapse>
</div>
</section>
<section class="border-b border-[#e8e9eb] px-5">
<div class="pt-4">
<h2 class="my-3 text-[#84868c]">对话</h2>
<NCollapse
:expanded-names="commConfigExpandedNames"
:trigger-areas="['main', 'arrow']"
@update:expanded-names="handleUpdateCommConfigExpandedNames"
>
<template #arrow>
<CustomIcon icon="gravity-ui:caret-right" />
</template>
<NCollapseItem title="开场白" name="preamble" class="my-[13px]!">
<!-- <template #header-extra>
<NTooltip trigger="hover">
<template #trigger>
<div
class="h-4 w-4 bg-[length:100%_100%]"
:class="
generatePreambleLoading
? 'bg-[url(@/assets/images/loading.gif)]'
: 'bg-[url(@/assets/svgs/star.svg)]'
"
@click="handleAIGeneratePreamble"
/>
</template>
AI生成
</NTooltip>
</template> -->
<NInput
v-model:value="personalAppConfig.commConfig.preamble"
type="textarea"
:rows="5"
:disabled="generatePreambleLoading"
class="text-xs! rounded-md!"
placeholder="请输入开场白"
/>
</NCollapseItem>
<NCollapseItem title="推荐词" name="featuredQuestions" class="my-[13px]!">
<!-- <template #header-extra>
<NTooltip trigger="hover">
<template #trigger>
<div
class="h-4 w-4 bg-[length:100%_100%]"
:class="
generateFeaturedQuestionsLoading
? 'bg-[url(@/assets/images/loading.gif)]'
: 'bg-[url(@/assets/svgs/star.svg)]'
"
@click="handleAIGenerateFeaturedQuestions"
/>
</template>
AI生成
</NTooltip>
</template> -->
<NDynamicInput
v-model:value="personalAppConfig.commConfig.featuredQuestions"
placeholder="请输入推荐词"
class="rounded-md"
:min="1"
:max="3"
:disabled="generateFeaturedQuestionsLoading"
/>
</NCollapseItem>
<NCollapseItem name="continuousQuestion" class="my-[13px]!">
<template #header>
<div class="flex items-center">
<span>追问</span>
<NPopover trigger="hover">
<template #trigger>
<CustomIcon
icon="mingcute:question-line"
class="ml-1 cursor-pointer text-base text-[#999] outline-none"
/>
</template>
<span class="text-xs"> 大模型根据对话内容自动生成的追加问题 </span>
</NPopover>
</div>
</template>
<template #header-extra>
<NPopselect
v-model:value="personalAppConfig.commConfig.continuousQuestionStatus"
:options="questionSettingOptions"
trigger="click"
content-class="text-xs"
@update:value="handleQuestionSettingChange"
>
<div class="text-theme-color flex cursor-pointer items-center justify-between text-xs">
<span> {{ continuousQuestionStatusText }}</span>
<CustomIcon icon="mingcute:down-line" class="ml-1 text-base" />
</div>
</NPopselect>
</template>
<div>
<span
v-show="personalAppConfig.commConfig.continuousQuestionStatus === 'default'"
class="text-xs text-[#84868c]"
>
根据用户最近3轮对话,在最后一轮回复后自动提供3个提问建议。
</span>
<div v-show="personalAppConfig.commConfig.continuousQuestionStatus === 'customizable'">
<span class="text-xs text-[#84868c]">
根据最近轮次的参考对话,在最后一轮回复后自动提供3个提问建议。
</span>
<div class="mt-4 text-xs">
<div class="mb-2.5 flex h-[34px] items-center justify-between">
<span>参考对话轮数:</span>
<div class="mx-5 flex flex-1">
<NSlider
v-model:value="personalAppConfig.commConfig.continuousQuestionTurn"
:default-value="3"
:step="1"
:min="1"
:max="5"
/>
<span class="ml-4">{{ personalAppConfig.commConfig.continuousQuestionTurn }}</span>
</div>
<NPopover trigger="hover">
<template #trigger>
<CustomIcon
icon="mingcute:question-line"
class="ml-1 cursor-pointer text-base text-[#999]"
/>
</template>
<span class="text-xs"> 大模型在追问生成过程中可参考1、3、5轮的对话轮数 </span>
</NPopover>
</div>
<div class="mb-2.5 flex h-[34px] items-center justify-between">
<span>追问prompt:</span>
<div class="mx-5 flex flex-1">
<span
class="text-theme-color cursor-pointer hover:opacity-80"
@click="handleShowAdditionalPromptModal"
>
编辑
</span>
</div>
<NPopover trigger="hover">
<template #trigger>
<CustomIcon
icon="mingcute:question-line"
class="ml-1 cursor-pointer text-base text-[#999]"
/>
</template>
<span class="text-xs"> 追问prompt可以控制追问的字数、风格和内容范围 </span>
</NPopover>
</div>
</div>
</div>
<span
v-show="personalAppConfig.commConfig.continuousQuestionStatus === 'close'"
class="text-xs text-[#84868c]"
>
在每轮回复后,不会提供用户任何提问建议
</span>
</div>
</NCollapseItem>
</NCollapse>
</div>
</section>
</div>
</div>
</div>
<AdditionalPromptModal
v-model:isShowModal="showAdditionalPromptModal"
:question-system="personalAppConfig.commConfig.continuousQuestionSystem"
:btn-loading="false"
modal-title="追加Prompt"
@comfirm="handleAdditionalPrompt"
/>
</template>
<style lang="scss" scoped>
:deep(.n-form-item-feedback-wrapper) {
--n-feedback-height: 14px;
}
:deep(.not-resize .n-input-wrapper) {
resize: none !important;
}
:deep(.prompt-input.n-input--textarea) {
--n-border: none !important;
--n-border-focus: none !important;
--n-border-hover: none !important;
--n-box-shadow-focus: none !important;
}
:deep(.prompt-input .n-input-wrapper) {
padding: 0;
}
.model-select {
:deep(.n-base-selection) {
font-size: 12px;
border-radius: 6px;
}
}
:deep(.n-input__suffix),
:deep(.n-input-word-count) {
font-size: 13px !important;
}
:deep(.n-input--stateful) {
--n-border-radius: 6px !important;
font-size: 12px;
}
:deep(.n-upload-trigger.n-upload-trigger--image-card) {
width: 70px;
height: 70px;
border-radius: 12px;
}
:deep(.n-upload-file-list .n-upload-file.n-upload-file--image-card-type) {
width: 72px;
height: 72px;
border-radius: 12px;
}
</style>
<script setup lang="ts"></script>
<template>
<div class="loader" />
</template>
<style lang="scss" scoped>
.loader {
width: 6px;
aspect-ratio: 1;
border-radius: 50%;
animation: l5 1s infinite linear alternate;
}
@keyframes l5 {
0% {
background: #2468f2;
box-shadow:
13px 0 #2468f2,
-13px 0 #0002;
}
33% {
background: #0002;
box-shadow:
13px 0 #2468f2,
-13px 0 #0002;
}
66% {
background: #0002;
box-shadow:
13px 0 #0002,
-13px 0 #2468f2;
}
100% {
background: #2468f2;
box-shadow:
13px 0 #0002,
-13px 0 #2468f2;
}
}
</style>
<script setup lang="ts">
import { computed, inject, onUnmounted, ref } from 'vue'
import { Emitter } from 'mitt'
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import { fetchCustomEventSource } from '@/composables/useEventSource'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
interface Props {
messageList: ConversationMessageItem[]
}
const props = defineProps<Props>()
const emit = defineEmits<{
addMessageItem: [value: ConversationMessageItem]
updateSpecifyMessageItem: [messageItemIndex: number, newObj: Partial<ConversationMessageItem>]
deleteLastMessageItem: []
updatePageScroll: []
clearAllMessage: []
}>()
const personalAppConfigStore = usePersonalAppConfigStore()
const emitter = inject<Emitter<MittEvents>>('emitter')
const inputeMessageContent = ref('')
const isAnswerResponseWait = ref(false)
let controller: AbortController | null = null
const agentId = computed(() => {
return personalAppConfigStore.baseInfo.agentId
})
const isAllowClearMessage = computed(() => {
return props.messageList.length > 0
})
const isSendBtnDisabled = computed(() => {
return !inputeMessageContent.value.trim()
})
onUnmounted(() => {
blockMessageResponse()
emitter?.off('selectFeaturedQuestion')
})
emitter?.on('selectFeaturedQuestion', (featuredQuestion) => {
inputeMessageContent.value = featuredQuestion
handleMessageSend()
})
function messageItemFactory() {
return {
timestamp: Date.now(),
role: 'user',
textContent: '',
isEmptyContent: false,
isTextContentLoading: false,
isAnswerResponseLoading: false,
} as const
}
function handleEnterKeypress(event: KeyboardEvent) {
if (event.code === 'Enter' && !event.shiftKey) {
event.preventDefault()
if (!inputeMessageContent.value.trim() || isAnswerResponseWait.value) return ''
handleMessageSend()
}
}
function handleMessageSend() {
if (!inputeMessageContent.value.trim() || isAnswerResponseWait.value) return ''
const messages: {
content: {
type: string
text: string
image_url: {
url: ''
}
}[]
role: string
}[] = []
emit('addMessageItem', { ...messageItemFactory(), textContent: inputeMessageContent.value })
emit('updatePageScroll')
props.messageList.forEach((messageItem) => {
messages.push({
content: [
{
type: 'text',
text: messageItem.textContent,
image_url: {
url: '',
},
},
],
role: messageItem.role,
})
})
inputeMessageContent.value = ''
isAnswerResponseWait.value = true
emit('addMessageItem', {
...messageItemFactory(),
role: 'assistant',
isTextContentLoading: true,
isAnswerResponseLoading: true,
})
emit('updatePageScroll')
const currentMessageIndex = props.messageList.length - 1
let replyTextContent = ''
controller = new AbortController()
fetchCustomEventSource({
path: '/api/rest/agentApplicationInfoRest/preview.json',
payload: {
agentId: agentId.value,
messages,
},
controller,
onMessage: (data: any) => {
if (data === '[DONE]') {
emit('updateSpecifyMessageItem', currentMessageIndex, {
isEmptyContent: !replyTextContent,
isTextContentLoading: false,
isAnswerResponseLoading: false,
})
emit('updatePageScroll')
blockMessageResponse()
return
}
if (data) {
replyTextContent += data
emit('updateSpecifyMessageItem', currentMessageIndex, {
textContent: replyTextContent,
isTextContentLoading: false,
})
emit('updatePageScroll')
}
},
onRequestError: () => {
errorMessageResponse()
},
onError: () => {
errorMessageResponse()
},
onFinally: () => {
controller = null
},
})
}
function errorMessageResponse() {
emit('updateSpecifyMessageItem', props.messageList.length - 1, {
isTextContentLoading: false,
textContent: '',
})
emit('deleteLastMessageItem')
emit('deleteLastMessageItem')
blockMessageResponse()
}
function handleClearAllMessage() {
if (!isAllowClearMessage.value) return
blockMessageResponse()
emit('clearAllMessage')
}
function blockMessageResponse() {
controller?.abort()
isAnswerResponseWait.value = false
}
</script>
<template>
<div class="mb-3 mt-5 px-5">
<div class="flex">
<div class="mr-2 flex h-8 w-8 items-center justify-center">
<NPopover trigger="hover">
<template #trigger>
<CustomIcon
icon="fluent:delete-12-regular"
class="text-base outline-none"
:class="
isAllowClearMessage
? 'hover:text-theme-color cursor-pointer text-[#5c5f66]'
: 'cursor-not-allowed text-[#b8babf]'
"
@click="handleClearAllMessage"
/>
</template>
<span class="text-xs">清空历史会话</span>
</NPopover>
</div>
<div class="relative flex-1">
<NInput
v-model:value="inputeMessageContent"
placeholder="请输入你的问题进行提问"
class="rounded-xl! shadow-[0_1px_#09122105,0_1px_1px_#09122105,0_3px_3px_#09122103,0_9px_9px_#09122103]! py-[4px] pr-[50px]"
@keypress="handleEnterKeypress"
/>
<div
class="absolute bottom-2 right-[20px] h-[24px] w-[24px] bg-[url('@/assets/images/send.png')] bg-contain"
:class="isSendBtnDisabled || isAnswerResponseWait ? 'opacity-60' : 'cursor-pointer'"
@click="handleMessageSend"
/>
</div>
</div>
<div class="mt-[9px] pl-10">
<span class="text-xs text-[#84868c]">以上内容均由AI生成,仅供参考</span>
</div>
</div>
</template>
<script setup lang="ts">
import CustomLoading from './custom-loading.vue'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
const personalAppConfigStore = usePersonalAppConfigStore()
interface Props {
role: 'user' | 'assistant'
messageItem: ConversationMessageItem
}
defineProps<Props>()
const useAvatar = 'https://mkp-dev.oss-cn-shenzhen.aliyuncs.com/data/upload/20240827/1724728478476.png'
const assistantAvatar = personalAppConfigStore.baseInfo.agentAvatar
</script>
<template>
<div class="mb-5 flex last:mb-0">
<NImage
:src="role === 'user' ? useAvatar : assistantAvatar"
preview-disabled
:width="32"
:height="32"
class="mr-2 mt-1.5 h-8 w-8 rounded-full"
/>
<div
class="min-w-[80px] max-w-[calc(100%-32px-12px)] flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]"
:class="role === 'user' ? 'bg-[#4b87ff] text-white' : 'bg-white text-[#333]'"
>
<div v-if="messageItem.isTextContentLoading" class="py-1.5 pl-4">
<CustomLoading />
</div>
<div v-else>
<p class="break-all">
<MarkdownRender
:raw-text-content="messageItem.isEmptyContent ? '[空内容]' : messageItem.textContent"
:color="role === 'user' ? '#fff' : '#192338'"
/>
</p>
<div v-show="role === 'assistant' && messageItem.isAnswerResponseLoading" class="mb-[5px] mt-4 px-4">
<CustomLoading />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import MessageItem from './message-item.vue'
import { useScroll } from '@/composables/useScroll'
interface Props {
messageList: ConversationMessageItem[]
}
defineProps<Props>()
const { scrollRef, scrollToBottom } = useScroll()
defineExpose({
scrollToBottom,
})
</script>
<template>
<main ref="scrollRef" class="h-full overflow-y-auto px-5">
<MessageItem
v-for="messageItem in messageList"
:key="messageItem.timestamp"
:role="messageItem.role"
:message-item="messageItem"
/>
</main>
</template>
<script setup lang="ts">
import { computed, h, onMounted, readonly, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { DropdownOption } from 'naive-ui'
import { sidebarMenus } from '@/router/index'
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import { useUserStore } from '@/store/modules/user'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import { formatDateTime } from '@/utils/date-formatter'
import { fetchPublishApplication } from '@/apis/agent-application'
const defaultAvatar = 'https://gsst-poe-sit.gz.bcebos.com/data/20240910/1725952917468.png'
interface Emits {
(e: 'changeAgentAppTabKey', value: string): void
}
const emit = defineEmits<Emits>()
const router = useRouter()
const userStore = useUserStore()
const personalAppConfigStore = usePersonalAppConfigStore()
const avatarOptions = readonly([
{
label: '退出登录',
key: 'logout',
icon: () => h(CustomIcon, { icon: 'teenyicons:logout-solid' }),
},
])
const modifiedTime = ref(personalAppConfigStore.modifiedTime)
const isInitGetAgentAppDetail = ref(false)
const isUpdateAgentAppConfig = ref(false)
const currentAgentAppTabKey = ref('config')
const publishBtnloading = ref(false)
const agentAppOptionList = [
{
value: 'config',
label: '配置',
},
{
value: 'publish',
label: '发布',
},
]
const personalAppConfig = computed(() => personalAppConfigStore.$state)
const menuOptions = computed(() => {
return sidebarMenus.map((item) => {
return {
label: item.label,
key: item.key,
routeName: item.routeName,
icon: () => h(CustomIcon, { icon: item.icon || 'icon-home' }),
}
})
})
const isShowModifiedTime = computed(() => {
return isUpdateAgentAppConfig.value && personalAppConfigStore.baseInfo.agentId
})
const publishBtnText = computed(() => {
return personalAppConfigStore.baseInfo.agentPublishStatus === 'publish' ? '更新发布' : '发 布'
})
const isShowPublishBtn = computed(() => {
return currentAgentAppTabKey.value === 'config'
})
const isAllowClickPublish = computed(() => {
return personalAppConfigStore.baseInfo.agentId && personalAppConfigStore.baseInfo.agentPublishStatus === 'publish'
})
watch(
() => personalAppConfig.value,
() => {
if (isInitGetAgentAppDetail.value) {
isInitGetAgentAppDetail.value = false
return
}
modifiedTime.value = new Date()
!isUpdateAgentAppConfig.value && (isUpdateAgentAppConfig.value = true)
},
{ deep: true },
)
onMounted(() => {
if (router.currentRoute.value.params.agentId) {
isInitGetAgentAppDetail.value = true
}
if (router.currentRoute.value.query.tabKey) {
currentAgentAppTabKey.value = router.currentRoute.value.query.tabKey as string
}
})
function handleMenuSelect(_key: number, option: DropdownOption) {
router.replace({ name: option.routeName as string })
}
function handleBackPreviousPage() {
router.go(-1)
}
function handleDropdownSelect(key: number) {
if (key === 1) {
userStore.logout().then(() => {
router.push({ name: 'Login' })
})
}
}
function handleSwtichAgentAppOption(currentTabKey: string) {
if (!isAllowClickPublish.value) return
currentAgentAppTabKey.value = currentTabKey
router.replace({
name: router.currentRoute.value.name as string,
query: { tabKey: currentTabKey },
params: { ...router.currentRoute.value.params },
})
emit('changeAgentAppTabKey', currentTabKey)
}
async function handlePublishApplication() {
publishBtnloading.value = true
const res = await fetchPublishApplication(personalAppConfig.value).finally(() => (publishBtnloading.value = false))
if (res.code === 0) {
window.$message.success('发布成功')
currentAgentAppTabKey.value = 'publish'
router.replace({
name: router.currentRoute.value.name as string,
query: { tabKey: 'publish' },
params: { ...router.currentRoute.value.params },
})
emit('changeAgentAppTabKey', 'publish')
}
}
</script>
<template>
<header class="h-navbar flex w-full items-center justify-between bg-[#f2f5f9] px-5 shadow-[inset_0_-1px_#e8e9eb]">
<div class="flex flex-1 items-center">
<NDropdown trigger="hover" :options="menuOptions" @select="handleMenuSelect">
<CustomIcon
icon="weui:back-outlined"
class="hover:text-theme-color mr-5 outline-none"
@click="handleBackPreviousPage"
/>
</NDropdown>
<div class="flex flex-col items-start justify-center">
<NPopover trigger="hover">
<template #trigger>
<span class="font-500 line-clamp-1 max-w-[200px] break-words text-base text-[#000]">
{{ personalAppConfigStore.baseInfo.agentTitle }}
</span>
</template>
<span>{{ personalAppConfigStore.baseInfo.agentTitle }}</span>
</NPopover>
<div class="flex items-center text-xs">
<NPopover trigger="hover">
<template #trigger>
<span class="line-clamp-1 max-w-[200px] break-words text-[#84868c]">
{{ personalAppConfigStore.baseInfo.agentDesc || '暂无描述' }}
</span>
</template>
<span>{{ personalAppConfigStore.baseInfo.agentDesc || '暂无描述' }}</span>
</NPopover>
<div
v-show="personalAppConfigStore.baseInfo.agentId"
class="ml-3 h-5 rounded bg-white px-2 leading-5 text-[#5c5f66]"
>
已变更
</div>
<div v-show="isShowModifiedTime" class="ml-3 h-5 rounded-md bg-white px-2 leading-5 text-[#151b26]">
自动保存于 {{ formatDateTime(modifiedTime, 'HH:mm:ss') }}
</div>
</div>
</div>
</div>
<div class="flex h-[38px] rounded-lg bg-[#e3e8f0] p-0.5">
<div
v-for="optionItem in agentAppOptionList"
:key="optionItem.value"
:value="optionItem.value"
:label="optionItem.label"
class="flex w-20 items-center justify-center rounded-lg"
:class="[
currentAgentAppTabKey === optionItem.value ? 'text-theme-color bg-white' : 'text-[#84868c]',
isAllowClickPublish ? 'hover:text-theme-color cursor-pointer' : 'cursor-not-allowed',
]"
@click="handleSwtichAgentAppOption(optionItem.value)"
>
<NPopover v-if="!isAllowClickPublish" trigger="hover">
<template #trigger>
{{ optionItem.label }}
</template>
<span>请先完成发布</span>
</NPopover>
<div v-if="isAllowClickPublish">{{ optionItem.label }}</div>
</div>
</div>
<div class="flex flex-1 items-center justify-end">
<NButton
v-show="isShowPublishBtn"
type="primary"
class="h-[32px]! min-w-20! rounded-md!"
:loading="publishBtnloading"
@click="handlePublishApplication"
>
{{ publishBtnText }}
</NButton>
<NDropdown trigger="click" :options="avatarOptions" @select="handleDropdownSelect">
<div class="ml-10 flex h-full cursor-pointer items-center">
<NAvatar round :size="30" object-fit="cover" :src="userStore.userInfo.avatarUrl || defaultAvatar" />
</div>
</NDropdown>
</div>
</header>
</template>
<script setup lang="ts">
import { computed, inject } from 'vue'
import { Emitter } from 'mitt'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
const personalAppConfigStore = usePersonalAppConfigStore()
const emitter = inject<Emitter<MittEvents>>('emitter')
const agentAvatar = computed(() => {
return (
personalAppConfigStore.baseInfo.agentAvatar || 'https://gsst-poe-sit.gz.bcebos.com/data/20240911/1726041369632.webp'
)
})
function handleSelectFeaturedQuestion(featuredQuestion: string) {
emitter?.emit('selectFeaturedQuestion', featuredQuestion)
}
</script>
<template>
<div class="flex w-full flex-1 flex-col px-5">
<div class="mb-5 flex w-full justify-center pt-[50px]">
<img :src="agentAvatar" class="h-[72px] w-[72px] rounded-xl border" />
</div>
<div class="flex flex-col items-center justify-center">
<p class="font-500 mb-4 line-clamp-1 text-2xl text-[#151b26]">
{{ personalAppConfigStore.baseInfo.agentTitle }}
</p>
<div class="flex flex-col items-start justify-center">
<p
v-show="personalAppConfigStore.commConfig.preamble"
class="mb-6 select-none rounded-xl border border-[#e8e9eb] bg-white px-[16px] py-[12px] shadow-[0_2px_2px_#0000000a]"
>
{{ personalAppConfigStore.commConfig.preamble }}
</p>
<ul class="flex max-w-full flex-col items-start justify-center gap-3 overflow-hidden">
<li v-for="(featuredQuestionItem, index) in personalAppConfigStore.commConfig.featuredQuestions" :key="index">
<div
v-show="featuredQuestionItem"
class="w-full cursor-pointer rounded-xl border border-[#d4d6d9] bg-[#ffffff80] px-[14px] py-[11px] hover:opacity-80"
@click="handleSelectFeaturedQuestion(featuredQuestionItem)"
>
<NPopover trigger="hover">
<template #trigger>
<span class="break-all">{{ featuredQuestionItem }}</span>
</template>
<span>{{ featuredQuestionItem }}</span>
</NPopover>
</div>
</li>
</ul>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import PageNarbar from './components/page-narbar.vue'
import AppSetting from './components/app-setting.vue'
import AppPreview from './components/app-preview.vue'
import AppPublish from './components/app-publish.vue'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
const router = useRouter()
const personalAppConfigStore = usePersonalAppConfigStore()
const currentAgentAppTabKey = ref('config')
onMounted(() => {
if (router.currentRoute.value.query.tabKey) {
currentAgentAppTabKey.value = router.currentRoute.value.query.tabKey as string
}
})
onUnmounted(() => {
personalAppConfigStore.resetPersonalAppConfigState()
})
function handleChangeAgentAppTabKey(currentTabKey: string) {
currentAgentAppTabKey.value = currentTabKey
}
</script>
<template>
<main class="h-full min-w-[1000px]">
<PageNarbar @change-agent-app-tab-key="handleChangeAgentAppTabKey" />
<div class="h-content flex w-full flex-1">
<div v-if="currentAgentAppTabKey === 'config'" class="flex h-full w-full flex-1">
<AppSetting />
<AppPreview />
</div>
<div v-if="currentAgentAppTabKey === 'publish'" class="flex h-full w-full flex-1">
<AppPublish />
</div>
</div>
</main>
</template>
import { NImage, NTooltip } from 'naive-ui'
import { h } from 'vue'
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { formatDateTime } from '@/utils/date-formatter'
export function createPersonalAppColumn(handlePersonalAppTableAction: (actionType: string, agentId: string) => void) {
return [
{
title: '应用名称',
key: 'agentTitle',
align: 'left',
ellipsis: {
tooltip: true,
},
width: 210,
fixed: 'left',
render(row: PersonalAppConfigState) {
return h(
'div',
{
style: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
},
{
default: () => [
h(NImage, {
width: '36px',
src: row.baseInfo.agentAvatar || 'https://gsst-poe-sit.gz.bcebos.com/data/20240911/1726041369632.webp',
showToolbar: false,
previewDisabled: true,
style: {
borderRadius: '8px',
height: '36px',
},
}),
h(
'span',
{
style: {
marginLeft: '12px',
fontWeight: '600',
flex: '1',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
className: 'hover:text-theme-color cursor-pointer',
},
{
default: () => [
h(
NTooltip,
{
props: { placement: 'top' },
},
{
trigger: () => h('span', {}, row.baseInfo.agentTitle || '我的Agent应用'),
default: () => h('span', {}, row.baseInfo.agentTitle || '我的Agent应用'),
},
),
],
},
),
],
},
)
},
},
{
title: '模型名称',
key: 'largeModel',
align: 'left',
ellipsis: {
tooltip: true,
},
width: 210,
render(row: PersonalAppConfigState) {
return row.commModelConfig.largeModel
},
},
{
title: '应用Id',
key: 'agentId',
align: 'left',
ellipsis: {
tooltip: true,
},
width: 180,
render(row: PersonalAppConfigState) {
return h(
'div',
{
style: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
},
{
default: () => [
h(
'span',
{
style: {
flex: '1',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
},
{
default: () => [
h(
NTooltip,
{
props: { placement: 'top' },
},
{
trigger: () => h('span', {}, row.baseInfo.agentId || '--'),
default: () => h('span', {}, row.baseInfo.agentId || '--'),
},
),
],
},
),
h(CustomIcon, {
icon: 'ion:copy-outline',
style: {
cursor: 'pointer',
color: '#2468f2',
},
onClick: () => handlePersonalAppTableAction('copyAgentId', row.baseInfo.agentId),
}),
],
},
)
},
},
{
title: '发布状态',
key: 'agentPublishStatus',
align: 'left',
ellipsis: {
tooltip: true,
},
width: 230,
render(row: PersonalAppConfigState) {
let publicText = '----'
let publicIcon = 'ion:close-circle-outline'
let bgColor = '#84868c'
let color = '#f2f5f9'
switch (row.baseInfo.agentPublishStatus) {
case 'draft':
publicText = '未发布'
publicIcon = 'gg:time'
bgColor = '#f2f5f9'
color = '#84868c'
break
case 'publish':
publicText = '已发布'
publicIcon = 'gg:check-o'
bgColor = '#ecffe6'
color = '#30bf13'
break
}
return [
h(
'div',
{ style: { display: 'flex', alignItems: 'center' } },
{
default: () => [
h(
'div',
{
className: 'flex justify-center items-center',
style: {
backgroundColor: bgColor,
fontSize: '12px',
width: '72px',
height: '24px',
borderRadius: '4px',
marginRight: '6px',
},
},
{
default: () => [
h(CustomIcon, {
icon: publicIcon,
style: {
color,
fontSize: '16px',
marginRight: '4px',
},
}),
h(
'span',
{
style: {
color,
},
},
publicText,
),
],
},
),
// row.baseInfo.agentPublishStatus === 'publish' && formatDateTime(row.modifiedTime),
],
},
),
]
},
},
{
title: '最近编辑时间',
key: 'modifiedTime',
align: 'left',
ellipsis: {
tooltip: true,
},
width: 170,
render(row: PersonalAppConfigState) {
return formatDateTime(row.modifiedTime)
},
},
{
title: '发布渠道',
key: 'channel',
align: 'left',
ellipsis: {
tooltip: true,
},
width: 140,
render(row: PersonalAppConfigState) {
return [
row.baseInfo.agentPublishStatus === 'publish'
? h(
NTooltip,
{
props: { placement: 'top' },
},
{
trigger: () =>
h(
'div',
{
className: 'flex justify-center items-center',
style: {
backgroundColor: '#e6f0ff',
borderRadius: '4px',
padding: '6px',
},
onClick: () => handlePersonalAppTableAction('openPublishDetail', row.baseInfo.agentId),
},
{
default: () => [
h(CustomIcon, {
icon: 'icon-park-solid:computer',
style: {
fontSize: '16px',
cursor: 'pointer',
color: '#2468f2',
},
}),
],
},
),
default: () => h('span', {}, '查看发布详情'),
},
)
: '--',
]
},
},
{
title: '操作',
key: 'action',
align: 'left',
ellipsis: {
tooltip: true,
},
width: 190,
fixed: 'right',
render(row: PersonalAppConfigState) {
return [
h(
'span',
{
style: { marginRight: '20px' },
className: 'text-theme-color cursor-pointer hover:opacity-80',
onClick: () => handlePersonalAppTableAction('edit', row.baseInfo.agentId),
},
{ default: () => '编辑' },
),
h(
'span',
{
style: { marginRight: '20px' },
className: 'text-theme-color cursor-pointer hover:opacity-80',
onClick: () => handlePersonalAppTableAction('copy', row.baseInfo.agentId),
},
{ default: () => '复制' },
),
h(
'span',
{
style: { marginRight: '20px' },
className: 'text-theme-color cursor-pointer hover:opacity-80',
onClick: () => handlePersonalAppTableAction('delete', row.baseInfo.agentId),
},
{ default: () => '删除' },
),
]
},
},
]
}
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import CustomPagination, { PaginationInfo } from '@/components/custom-pagination/custom-pagination.vue'
import { createPersonalAppColumn } from './columns.ts'
import useTableScrollY from '@/composables/useTableScrollY.ts'
import {
fetchDeleteApplication,
fetchGetDebugApplicationInfo,
fetchGetApplicationList,
fetchSaveAgentApplication,
} from '@/apis/agent-application.ts'
import { PersonalAppConfigState } from '@/store/types/personal-app-config.ts'
import { copyToClip } from '@/utils/copy.ts'
const router = useRouter()
const { pageContentWrapRef, tableContentY } = useTableScrollY(48 + 32 + 16 + 16 + 28)
const columns = createPersonalAppColumn(handleClickPersonalAppTableAction)
const pagingInfo = ref<PaginationInfo>({
pageNo: 1,
pageSize: 10,
totalPages: 0,
totalRows: 0,
})
const agentAppList = ref<PersonalAppConfigState[]>([])
const agentSearchInputValue = ref('')
const agentAppListTableLoading = ref(false)
onMounted(async () => {
await handleGetApplicationList()
})
async function handleGetApplicationList() {
agentAppListTableLoading.value = true
const res = await fetchGetApplicationList<PersonalAppConfigState[]>({
query: agentSearchInputValue.value,
pagingInfo: pagingInfo.value,
}).finally(() => (agentAppListTableLoading.value = false))
if (res.code === 0) {
agentAppList.value = res.data
pagingInfo.value = res.pagingInfo as PaginationInfo
}
}
function handleClickPersonalAppTableAction(actionType: string, agentId: string) {
switch (actionType) {
case 'copyAgentId':
handleCopyAgentId(agentId)
break
case 'openPublishDetail':
handleOpenPublishDetail(agentId)
break
case 'edit':
handleEditPersonalApp(agentId)
break
case 'copy':
handleCopyPersonalApp(agentId)
break
case 'delete':
handleDeletePersonalApp(agentId)
break
}
}
function handleCopyAgentId(agentId: string) {
copyToClip(agentId)
window.$message.success('复制成功')
}
function handleOpenPublishDetail(agentId: string) {
router.push({
name: 'PersonalAppSetting',
query: {
tabKey: 'publish',
},
params: {
agentId,
},
})
}
function handleEditPersonalApp(agentId: string) {
router.push({
name: 'PersonalAppSetting',
params: {
agentId,
},
})
}
async function handleCopyPersonalApp(agentId: string) {
const res = await fetchGetDebugApplicationInfo<PersonalAppConfigState>(agentId)
if (res.code === 0) {
const payload = res.data
payload.baseInfo.agentId = ''
payload.baseInfo.agentTitle += '的副本'
payload.baseInfo.agentPublishStatus = 'draft'
await fetchSaveAgentApplication(payload)
await handleGetApplicationList()
}
}
function handleDeletePersonalApp(agentId: string) {
window.$dialog.warning({
title: '确定要删除选中的应用吗?',
content: '删除后,如需再次使用,请重新创建',
negativeText: '取消',
positiveText: '确定',
onPositiveClick: async () => {
const res = await fetchDeleteApplication(agentId)
if (res.code === 0) {
window.$message.success('删除成功')
await handleGetApplicationList()
}
},
})
}
function handleToPersonAppSettingPage() {
router.push({ name: 'PersonalAppSetting' })
}
async function handleEnterKeypress(event: KeyboardEvent) {
if (event.code === 'Enter' && !event.shiftKey) {
event.preventDefault()
await handleGetApplicationList()
}
}
async function handleGetApplicationListUpdatePageNo(pageNo: number) {
pagingInfo.value.pageNo = pageNo
await handleGetApplicationList()
}
async function handleGetApplicationListUpdatePageSize(pageSize: number) {
pagingInfo.value.pageNo = 1
pagingInfo.value.pageSize = pageSize
await handleGetApplicationList()
}
</script>
<template>
<div ref="pageContentWrapRef" class="h-full">
<div class="mb-4 flex justify-between">
<NInput
v-model:value="agentSearchInputValue"
placeholder="请输入应用名称或描述"
class="w-[256px]! h-[32px]! rounded-md!"
@keypress="handleEnterKeypress"
>
<template #suffix>
<CustomIcon
icon="tdesign:search"
class="cursor-pointer text-base text-[#999]"
@click="handleGetApplicationList"
/>
</template>
</NInput>
<NButton type="primary" class="h-[32px]! w-[100px]! rounded-md!" @click="handleToPersonAppSettingPage">
创建应用
</NButton>
</div>
<div class="mb-4" :style="{ height: tableContentY + 48 + 'px' }">
<NDataTable
:loading="agentAppListTableLoading"
:bordered="true"
:bottom-bordered="true"
:single-line="false"
:data="agentAppList"
:columns="columns"
:max-height="tableContentY"
:scroll-x="1330"
/>
</div>
<footer class="flex justify-end">
<CustomPagination
:paging-info="pagingInfo"
@update-page-no="handleGetApplicationListUpdatePageNo"
@update-page-size="handleGetApplicationListUpdatePageSize"
/>
</footer>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const currentRoute = useRoute()
const router = useRouter()
const routerNameValue = ref(currentRoute.name)
const personalSpaceModuleList = [
{
routeName: 'PersonalSpaceApp',
label: '应用',
},
]
watch(
() => currentRoute.fullPath,
() => {
routerNameValue.value = currentRoute.name
},
)
function handleChangeRoute(routeName: string) {
router.replace({ name: routeName })
}
</script>
<template>
<div class="flex h-full flex-col">
<ul class="mb-4 flex">
<li
v-for="personalSpaceModuleItem in personalSpaceModuleList"
:key="personalSpaceModuleItem.routeName"
class="hover:text-theme-color ml-[32px] h-9 cursor-pointer select-none text-xl leading-9 first:ml-0"
:class="[routerNameValue === personalSpaceModuleItem.routeName ? 'text-theme-color' : '']"
@click="handleChangeRoute(personalSpaceModuleItem.routeName)"
>
{{ personalSpaceModuleItem.label }}
</li>
</ul>
<div class="h-full overflow-hidden rounded-2xl bg-white p-6 shadow-[0_2px_2px_#0000000a]">
<RouterView v-slot="{ Component }">
<Transition appear name="fade-slide" mode="out-in">
<Component :is="Component" />
</Transition>
</RouterView>
</div>
</div>
</template>
<style lang="scss" scoped>
.fade-slide-leave-active,
.fade-slide-enter-active {
transition: all 0.3s;
}
.fade-slide-enter-from {
opacity: 0;
transform: translateX(-30px);
}
.fade-slide-leave-to {
opacity: 0;
transform: translateX(30px);
}
</style>
<script setup lang="ts"></script>
<template>
<div class="loader" />
</template>
<style lang="scss" scoped>
.loader {
width: 6px;
aspect-ratio: 1;
border-radius: 50%;
animation: l5 1s infinite linear alternate;
}
@keyframes l5 {
0% {
background: #2468f2;
box-shadow:
13px 0 #2468f2,
-13px 0 #0002;
}
33% {
background: #0002;
box-shadow:
13px 0 #2468f2,
-13px 0 #0002;
}
66% {
background: #0002;
box-shadow:
13px 0 #0002,
-13px 0 #2468f2;
}
100% {
background: #2468f2;
box-shadow:
13px 0 #0002,
-13px 0 #2468f2;
}
}
</style>
<script setup lang="ts">
import { computed, inject, onMounted, onUnmounted, ref } from 'vue'
import { Emitter } from 'mitt'
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import { fetchCustomEventSource } from '@/composables/useEventSource'
import { useUserStore } from '@/store/modules/user'
import { useLayoutConfig } from '@/composables/useLayoutConfig'
interface Props {
agentId: string
dialogsId: string
messageList: ConversationMessageItem[]
}
const props = defineProps<Props>()
const emit = defineEmits<{
addMessageItem: [value: ConversationMessageItem]
updateSpecifyMessageItem: [messageItemIndex: number, newObj: Partial<ConversationMessageItem>]
deleteLastMessageItem: []
updatePageScroll: []
clearAllMessage: []
toLogin: []
}>()
const { isMobile } = useLayoutConfig()
const userStore = useUserStore()
const emitter = inject<Emitter<MittEvents>>('emitter')
const inputeMessageContent = ref('')
const isAnswerResponseWait = ref(false)
let controller: AbortController | null = null
const isLogin = computed(() => {
return userStore.isLogin
})
const isAllowClearMessage = computed(() => {
return props.messageList.length > 0
})
const isSendBtnDisabled = computed(() => {
return !inputeMessageContent.value.trim()
})
const inputPlaceholder = computed(() => {
return isLogin.value ? '请输入你的问题进行提问' : ''
})
onMounted(() => {
emitter?.on('selectFeaturedQuestion', (featuredQuestion) => {
if (!isLogin.value) {
window.$message.warning('请先登录')
return
}
inputeMessageContent.value = featuredQuestion
handleMessageSend()
})
})
onUnmounted(() => {
blockMessageResponse()
emitter?.off('selectFeaturedQuestion')
})
function messageItemFactory() {
return {
timestamp: Date.now(),
role: 'user',
textContent: '',
isEmptyContent: false,
isTextContentLoading: false,
isAnswerResponseLoading: false,
} as const
}
function handleEnterKeypress(event: KeyboardEvent) {
if (event.code === 'Enter' && !event.shiftKey) {
event.preventDefault()
if (!inputeMessageContent.value.trim() || isAnswerResponseWait.value) return ''
handleMessageSend()
}
}
function handleMessageSend() {
if (!isLogin.value) {
window.$message.warning('请先登录')
return
}
if (!inputeMessageContent.value.trim() || isAnswerResponseWait.value) return ''
emit('addMessageItem', { ...messageItemFactory(), textContent: inputeMessageContent.value })
emit('updatePageScroll')
emit('addMessageItem', {
...messageItemFactory(),
role: 'assistant',
isTextContentLoading: true,
isAnswerResponseLoading: true,
})
emit('updatePageScroll')
const input = inputeMessageContent.value
const currentMessageIndex = props.messageList.length - 1
let replyTextContent = ''
isAnswerResponseWait.value = true
inputeMessageContent.value = ''
controller = new AbortController()
fetchCustomEventSource({
path: '/api/rest/agentApplicationRest/callAgentApplication.json',
payload: {
agentId: props.agentId,
dialogsId: props.dialogsId,
input,
},
controller,
onMessage: (data: any) => {
if (data === '[DONE]') {
emit('updateSpecifyMessageItem', currentMessageIndex, {
isEmptyContent: !replyTextContent,
isTextContentLoading: false,
isAnswerResponseLoading: false,
})
emit('updatePageScroll')
blockMessageResponse()
return
}
if (data) {
replyTextContent += data
emit('updateSpecifyMessageItem', currentMessageIndex, {
textContent: replyTextContent,
isTextContentLoading: false,
})
emit('updatePageScroll')
}
},
onRequestError: () => {
errorMessageResponse()
},
onError: () => {
errorMessageResponse()
},
onFinally: () => {
controller = null
},
})
}
function errorMessageResponse() {
emit('updateSpecifyMessageItem', props.messageList.length - 1, {
isTextContentLoading: false,
textContent: '',
})
emit('deleteLastMessageItem')
emit('deleteLastMessageItem')
blockMessageResponse()
}
function handleClearAllMessage() {
if (!isAllowClearMessage.value) return
blockMessageResponse()
emit('clearAllMessage')
}
function blockMessageResponse() {
controller?.abort()
isAnswerResponseWait.value = false
}
function handleToLogin() {
emit('toLogin')
}
</script>
<template>
<div class="my-5">
<div class="flex items-center">
<div class="mr-2 flex items-center justify-center" :class="isMobile ? 'h-6 w-6' : 'h-8 w-8'">
<NPopover trigger="hover">
<template #trigger>
<CustomIcon
icon="fluent:delete-12-regular"
class="text-base outline-none"
:class="
isAllowClearMessage
? 'hover:text-theme-color cursor-pointer text-[#5c5f66]'
: 'cursor-not-allowed text-[#b8babf]'
"
@click="handleClearAllMessage"
/>
</template>
<span class="text-xs">清空历史会话</span>
</NPopover>
</div>
<div class="relative flex-1">
<NInput
v-model:value="inputeMessageContent"
:placeholder="inputPlaceholder"
:disabled="!isLogin"
class="rounded-xl! shadow-[0_1px_#09122105,0_1px_1px_#09122105,0_3px_3px_#09122103,0_9px_9px_#09122103]! py-[4px] pr-[50px]"
@keypress="handleEnterKeypress"
/>
<div
class="absolute bottom-2 right-[20px] h-[24px] w-[24px] bg-[url('@/assets/images/send.png')] bg-contain"
:class="isSendBtnDisabled || isAnswerResponseWait || !isLogin ? 'opacity-60' : 'cursor-pointer'"
@click="handleMessageSend"
/>
<div v-show="!isLogin" class="absolute left-3 top-[5px] flex h-[34px] items-center text-[#84868c]">
请先
<span class="text-theme-color cursor-pointer px-1 hover:opacity-80" @click="handleToLogin">登录</span>
后开始提问
</div>
</div>
</div>
<div class="mt-[9px]">
<span class="flex w-full justify-center text-xs text-[#b8babf]">以上内容均由AI生成,仅供参考</span>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import CustomLoading from './custom-loading.vue'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { useLayoutConfig } from '@/composables/useLayoutConfig'
interface Props {
role: 'user' | 'assistant'
messageItem: ConversationMessageItem
agentApplicationConfig: PersonalAppConfigState
}
const props = defineProps<Props>()
const { isMobile } = useLayoutConfig()
const useAvatar = 'https://mkp-dev.oss-cn-shenzhen.aliyuncs.com/data/upload/20240827/1724728478476.png'
const assistantAvatar = computed(() => {
return (
props.agentApplicationConfig.baseInfo.agentAvatar ||
'https://gsst-poe-sit.gz.bcebos.com/data/20240911/1726041369632.webp'
)
})
</script>
<template>
<div
class="mb-5 flex last:mb-0"
:class="[
isMobile ? 'flex-row-reverse' : 'flex-row',
role === 'assistant' && isMobile ? 'justify-end' : 'justify-start',
]"
>
<NImage
v-show="!isMobile"
:src="role === 'user' ? useAvatar : assistantAvatar"
preview-disabled
:width="32"
:height="32"
class="mr-2 mt-1.5 h-8 w-8 rounded-full"
/>
<div
class="min-w-[80px] flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]"
:class="[
role === 'user' ? 'bg-theme-color text-white' : 'bg-white text-[#333]',
isMobile ? 'max-w-[calc(100%-20px)]' : 'max-w-[calc(100%-32px-12px)]',
]"
>
<div v-if="messageItem.isTextContentLoading" class="py-1.5 pl-4">
<CustomLoading />
</div>
<div v-else>
<p class="break-all">
<MarkdownRender
:raw-text-content="messageItem.isEmptyContent ? '[空内容]' : messageItem.textContent"
:color="role === 'user' ? '#fff' : '#192338'"
/>
</p>
<div v-show="role === 'assistant' && messageItem.isAnswerResponseLoading" class="mb-[5px] mt-4 px-4">
<CustomLoading />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import MessageItem from './message-item.vue'
import { useScroll } from '@/composables/useScroll'
import { PersonalAppConfigState } from '@/store/types/personal-app-config'
interface Props {
messageList: ConversationMessageItem[]
agentApplicationConfig: PersonalAppConfigState
}
defineProps<Props>()
const { scrollRef, scrollToBottom } = useScroll()
defineExpose({
scrollToBottom,
})
</script>
<template>
<main ref="scrollRef" class="h-full overflow-y-auto px-5">
<MessageItem
v-for="messageItem in messageList"
:key="messageItem.timestamp"
:role="messageItem.role"
:message-item="messageItem"
:agent-application-config="agentApplicationConfig"
/>
</main>
</template>
<script setup lang="ts">
import { useUserStore } from '@/store/modules/user'
import { computed } from 'vue'
interface Props {
agentTitle: string
}
defineProps<Props>()
const emit = defineEmits<{
toCreateApplication: []
toLogin: []
}>()
const userStore = useUserStore()
const isLogin = computed(() => {
return userStore.isLogin
})
function handleToCreateApplication() {
emit('toCreateApplication')
}
function handleToLogin() {
emit('toLogin')
}
</script>
<template>
<header class="flex h-[48px] w-full items-center justify-between border-b border-[#e8e9eb] bg-white px-4">
<div class="h-[23px] w-[90px] bg-[url('@/assets/images/page-logo.png')] bg-contain bg-center bg-no-repeat" />
<div>
<NButton
v-show="isLogin"
type="primary"
class="rounded-md! h-[32px]! text-xs! w-[80px]!"
@click="handleToCreateApplication"
>
创建应用
</NButton>
<NButton v-show="!isLogin" type="primary" class="rounded-md! h-[32px]! text-xs! w-[80px]!" @click="handleToLogin">
<span class="text-xs">立即登录</span>
</NButton>
</div>
</header>
</template>
<script setup lang="ts">
import { computed, inject } from 'vue'
import { Emitter } from 'mitt'
import { PersonalAppConfigState } from '@/store/types/personal-app-config'
interface Props {
agentApplicationConfig: PersonalAppConfigState
}
const props = defineProps<Props>()
const agentApplicationConfig = computed(() => props.agentApplicationConfig)
const emitter = inject<Emitter<MittEvents>>('emitter')
const agentAvatar = computed(() => {
return (
agentApplicationConfig.value.baseInfo.agentAvatar ||
'https://gsst-poe-sit.gz.bcebos.com/data/20240911/1726041369632.webp'
)
})
function handleSelectFeaturedQuestion(featuredQuestion: string) {
emitter?.emit('selectFeaturedQuestion', featuredQuestion)
}
</script>
<template>
<div class="flex w-full flex-1 flex-col px-5">
<div class="mb-5 mt-[60px] flex w-full justify-center sm:pt-[120px]">
<img :src="agentAvatar" class="h-[72px] w-[72px] rounded-xl border" />
</div>
<div class="flex flex-col items-center justify-center">
<p class="font-500 mb-4 line-clamp-1 text-2xl text-[#151b26]">
{{ agentApplicationConfig.baseInfo.agentTitle }}
</p>
<div class="flex flex-col items-start justify-center">
<p
v-show="agentApplicationConfig.commConfig.preamble"
class="mb-6 select-none rounded-xl bg-[#e7ecf2] px-[16px] py-[12px]"
>
{{ agentApplicationConfig.commConfig.preamble }}
</p>
<ul class="flex max-w-full flex-col items-start justify-center gap-3 overflow-hidden">
<li v-for="(featuredQuestionItem, index) in agentApplicationConfig.commConfig.featuredQuestions" :key="index">
<div
v-show="featuredQuestionItem"
class="w-full cursor-pointer rounded-xl border border-[#d4d6d9] bg-[#ffffff80] px-[14px] py-[11px] hover:opacity-80"
@click="handleSelectFeaturedQuestion(featuredQuestionItem)"
>
<span class="break-all">{{ featuredQuestionItem }}</span>
</div>
</li>
</ul>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import { useUserStore } from '@/store/modules/user'
import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { formatDateTime } from '@/utils/date-formatter'
interface Props {
agentApplicationConfig: PersonalAppConfigState
}
defineProps<Props>()
const emit = defineEmits<{
backHomePage: []
toCreateApplication: []
toLogin: []
}>()
const userStore = useUserStore()
const useAvatar = computed(() => {
return userStore.userInfo.avatarUrl || 'https://gsst-poe-sit.gz.bcebos.com/data/20240910/1725952917468.png'
})
const isLogin = computed(() => {
return userStore.isLogin
})
function handleBackHomePage() {
emit('backHomePage')
}
function handleToCreateApplication() {
emit('toCreateApplication')
}
function handleToLogin() {
emit('toLogin')
}
</script>
<template>
<header class="flex h-[68px] w-full items-center justify-between border-b border-[#e8e9eb] bg-white px-5">
<div class="flex items-center">
<CustomIcon
icon="weui:back-outlined"
class="hover:text-theme-color mr-5 outline-none"
@click="handleBackHomePage"
/>
<img :src="agentApplicationConfig.baseInfo.agentAvatar" class="mr-5 h-10 w-10 rounded-md" />
<div class="flex flex-col items-start justify-center">
<span class="mb-1 line-clamp-1 max-w-[200px] break-all">{{ agentApplicationConfig.baseInfo.agentTitle }}</span>
<div class="flex items-center text-xs text-[#84868c]">
<img v-show="isLogin" :src="useAvatar" class="mr-2 h-5 w-5 rounded-full" />
<p v-show="isLogin" class="mr-4 line-clamp-1 max-w-[120px] break-words break-all">
{{ userStore.userInfo.nickName }}
</p>
<span>发布于 {{ formatDateTime(agentApplicationConfig.modifiedTime) }}</span>
</div>
</div>
</div>
<div>
<NButton
v-show="isLogin"
type="primary"
class="rounded-md! h-[32px]! w-[100px]!"
@click="handleToCreateApplication"
>创建应用</NButton
>
<NButton v-show="!isLogin" type="primary" class="rounded-md! h-[32px]! w-[100px]!" @click="handleToLogin">
立即登录
</NButton>
</div>
</header>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import PageHeader from './components/mobile-page-header.vue'
import Preamble from './components/preamble.vue'
import MessageList from './components/message-list.vue'
import FooterInput from './components/footer-input.vue'
import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { useUserStore } from '@/store/modules/user'
import { defaultPersonalAppConfigState } from '@/store/modules/personal-app-config'
import { fetchCreateDialogues, fetchGetApplicationInfo } from '@/apis/agent-application'
import { useLayoutConfig } from '@/composables/useLayoutConfig'
const router = useRouter()
const userStore = useUserStore()
const { isMobile } = useLayoutConfig()
const fullScreenLoading = ref(false)
const agentId = ref('')
const dialogsId = ref('')
const agentApplicationConfig = ref<PersonalAppConfigState>(defaultPersonalAppConfigState())
const messageListRef = ref<InstanceType<typeof MessageList> | null>(null)
const messageList = ref<ConversationMessageItem[]>([])
onMounted(async () => {
if (router.currentRoute.value.params.agentId) {
agentId.value = router.currentRoute.value.params.agentId as string
}
if (!isMobile.value) {
router.replace({
name: 'ShareWebApplication',
params: { agentId: agentId.value },
})
}
if (agentId.value) {
fullScreenLoading.value = true
await handleGetApplicationDetail()
userStore.isLogin && (await handleCreateDialogues())
fullScreenLoading.value = false
return
}
router.replace({ name: 'Home' })
})
async function handleGetApplicationDetail() {
fetchGetApplicationInfo<PersonalAppConfigState>(agentId.value)
.then((res) => {
agentApplicationConfig.value = res.data
document.title = agentApplicationConfig.value.baseInfo.agentTitle
})
.catch(() => {
router.replace({ name: 'Home' })
})
}
async function handleCreateDialogues() {
const res = await fetchCreateDialogues(agentId.value)
if (res.code === 0) {
dialogsId.value = res.data as string
}
}
function handleToLoginPage() {
router.push({
name: 'Login',
query: { redirect: encodeURIComponent(router.currentRoute.value.fullPath) },
})
}
function handleCreateApplicationPage() {
window.$dialog.info({
title: '温馨提示',
content: '为保证您的体验,请通过pc端访问',
positiveText: '我知道啦',
})
}
function handleAddMessageItem(messageItem: ConversationMessageItem) {
messageList.value.push(messageItem)
}
function handleUpdateSpecifyMessageItem(messageItemIndex: number, newObj: Partial<ConversationMessageItem>) {
if (messageList.value[messageItemIndex]) {
Object.entries(newObj).forEach(([k, v]) => {
;(messageList.value[messageItemIndex] as any)[k as keyof typeof newObj] = v
})
}
}
function handleDeleteLastMessageItem() {
messageList.value.pop()
}
function handleUpdatePageScroll() {
messageListRef.value?.scrollToBottom()
}
function handleClearAllMessage() {
window.$dialog.warning({
title: '确认要清空对话吗?',
content: '清空对话将清空区域所有历史对话内容,确定清空对话吗?',
negativeText: '取消',
positiveText: '确定',
onPositiveClick: () => {
messageList.value = []
window.$message.success('清空成功')
},
})
}
</script>
<template>
<div v-loading="fullScreenLoading" class="h-full w-full">
<PageHeader
:agent-title="agentApplicationConfig.baseInfo.agentTitle"
@to-login="handleToLoginPage"
@to-create-application="handleCreateApplicationPage"
/>
<div class="flex h-[calc(100%-48px)] w-full flex-col bg-[#f2f5f9]">
<div v-if="messageList.length === 0" class="w-full flex-1 overflow-auto px-4">
<Preamble :agent-application-config="agentApplicationConfig" />
</div>
<div v-if="messageList.length > 0" class="flex w-full flex-1 flex-col overflow-hidden pt-5">
<div class="flex-1 overflow-auto">
<MessageList
ref="messageListRef"
:agent-application-config="agentApplicationConfig"
:message-list="messageList"
/>
</div>
</div>
<div class="footer-operation px-4">
<FooterInput
:message-list="messageList"
:dialogs-id="dialogsId"
:agent-id="agentApplicationConfig.baseInfo.agentId"
@add-message-item="handleAddMessageItem"
@update-specify-message-item="handleUpdateSpecifyMessageItem"
@delete-last-message-item="handleDeleteLastMessageItem"
@update-page-scroll="handleUpdatePageScroll"
@clear-all-message="handleClearAllMessage"
@to-login="handleToLoginPage"
/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.footer-operation {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
@include custom-scrollbar(4px);
</style>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import PageHeader from './components/web-page-header.vue'
import Preamble from './components/preamble.vue'
import MessageList from './components/message-list.vue'
import FooterInput from './components/footer-input.vue'
import { fetchCreateDialogues, fetchGetApplicationInfo } from '@/apis/agent-application'
import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { defaultPersonalAppConfigState } from '@/store/modules/personal-app-config'
import { useUserStore } from '@/store/modules/user'
import { useLayoutConfig } from '@/composables/useLayoutConfig'
const router = useRouter()
const userStore = useUserStore()
const { isMobile } = useLayoutConfig()
const fullScreenLoading = ref(false)
const agentId = ref('')
const dialogsId = ref('')
const agentApplicationConfig = ref<PersonalAppConfigState>(defaultPersonalAppConfigState())
const messageListRef = ref<InstanceType<typeof MessageList> | null>(null)
const messageList = ref<ConversationMessageItem[]>([])
onMounted(async () => {
if (router.currentRoute.value.params.agentId) {
agentId.value = router.currentRoute.value.params.agentId as string
}
if (isMobile.value) {
router.replace({
name: 'ShareMobileApplication',
params: { agentId: agentId.value },
})
}
if (agentId.value) {
fullScreenLoading.value = true
await handleGetApplicationDetail()
userStore.isLogin && (await handleCreateDialogues())
fullScreenLoading.value = false
return
}
router.replace({ name: 'Home' })
})
async function handleGetApplicationDetail() {
fetchGetApplicationInfo<PersonalAppConfigState>(agentId.value)
.then((res) => {
agentApplicationConfig.value = res.data
document.title = agentApplicationConfig.value.baseInfo.agentTitle
})
.catch(() => {
router.replace({ name: 'Home' })
})
}
async function handleCreateDialogues() {
const res = await fetchCreateDialogues(agentId.value)
if (res.code === 0) {
dialogsId.value = res.data as string
}
}
function handleBackHomePage() {
router.push({
name: 'Home',
})
}
function handleToLoginPage() {
router.replace({
name: 'Login',
query: { redirect: encodeURIComponent(router.currentRoute.value.fullPath) },
})
}
function handleCreateApplicationPage() {
router.push({
name: 'PersonalAppSetting',
})
}
function handleAddMessageItem(messageItem: ConversationMessageItem) {
messageList.value.push(messageItem)
}
function handleUpdateSpecifyMessageItem(messageItemIndex: number, newObj: Partial<ConversationMessageItem>) {
if (messageList.value[messageItemIndex]) {
Object.entries(newObj).forEach(([k, v]) => {
;(messageList.value[messageItemIndex] as any)[k as keyof typeof newObj] = v
})
}
}
function handleDeleteLastMessageItem() {
messageList.value.pop()
}
function handleUpdatePageScroll() {
messageListRef.value?.scrollToBottom()
}
function handleClearAllMessage() {
window.$dialog.warning({
title: '确认要清空对话吗?',
content: '清空对话将清空区域所有历史对话内容,确定清空对话吗?',
negativeText: '取消',
positiveText: '确定',
onPositiveClick: () => {
messageList.value = []
window.$message.success('清空成功')
},
})
}
</script>
<template>
<div v-loading="fullScreenLoading" class="flex h-screen min-h-[500px] w-full flex-col overflow-y-hidden">
<main class="h-full min-w-[1100px]">
<PageHeader
:agent-application-config="agentApplicationConfig"
@back-home-page="handleBackHomePage"
@to-login="handleToLoginPage"
@to-create-application="handleCreateApplicationPage"
/>
<div class="h-[calc(100%-68px)] w-full bg-[#f2f5f9]">
<div class="mx-auto flex h-full w-[1000px] flex-col overflow-hidden">
<div v-if="messageList.length === 0" class="w-full flex-1 overflow-auto px-5">
<Preamble :agent-application-config="agentApplicationConfig" />
</div>
<div v-if="messageList.length > 0" class="flex w-full flex-1 flex-col overflow-hidden">
<div class="my-5 ml-16 mr-5 border-b border-[#e8e9eb] py-6">
<div class="flex items-start">
<img :src="agentApplicationConfig.baseInfo.agentAvatar" class="h-14 w-14 rounded-xl" />
<p class="font-500 ml-4 line-clamp-1 h-8 max-w-[90%] break-all text-2xl leading-8 text-[#151b26]">
{{ agentApplicationConfig.baseInfo.agentTitle }}
</p>
</div>
<div class="mt-3 rounded-xl border border-[#e8e9eb] bg-white px-4 py-3 shadow-[0_2px_2px_#0000000a]">
{{ agentApplicationConfig.commConfig.preamble }}
</div>
</div>
<div class="flex-1 overflow-auto">
<MessageList
ref="messageListRef"
:agent-application-config="agentApplicationConfig"
:message-list="messageList"
/>
</div>
</div>
<div class="px-5">
<FooterInput
:message-list="messageList"
:dialogs-id="dialogsId"
:agent-id="agentApplicationConfig.baseInfo.agentId"
@add-message-item="handleAddMessageItem"
@update-specify-message-item="handleUpdateSpecifyMessageItem"
@delete-last-message-item="handleDeleteLastMessageItem"
@update-page-scroll="handleUpdatePageScroll"
@clear-all-message="handleClearAllMessage"
@to-login="handleToLoginPage"
/>
</div>
</div>
</div>
</main>
</div>
</template>
<style scoped lang="scss">
@include custom-scrollbar(6px);
</style>
...@@ -30,5 +30,6 @@ export default { ...@@ -30,5 +30,6 @@ export default {
'font-family-no-missing-generic-family-keyword': null, 'font-family-no-missing-generic-family-keyword': null,
'scss/at-import-partial-extension': 'always', 'scss/at-import-partial-extension': 'always',
'alpha-value-notation': 'number', 'alpha-value-notation': 'number',
'selector-class-pattern': null,
}, },
} }
declare interface ConversationMessageItem {
timestamp: number
role: 'user' | 'assistant'
textContent: string
isEmptyContent: boolean
isTextContentLoading: boolean
isAnswerResponseLoading: boolean
}
declare interface Window { declare interface Window {
ENV: 'DEV' | 'PROD'
$loadingBar: import('naive-ui').LoadingBarProviderInst $loadingBar: import('naive-ui').LoadingBarProviderInst
$dialog: import('naive-ui').DialogProviderInst $dialog: import('naive-ui').DialogProviderInst
$message: import('naive-ui').MessageProviderInst $message: import('naive-ui').MessageProviderInst
......
declare type MittEvents = {
selectFeaturedQuestion: string
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment