feat: 增加保存会话为图片的功能 (#374)
* feat: 增加保存会话为图片的功能 * feat: 异常处理和增加导出动画 --------- Co-authored-by: ChenZhaoYu <790348264@qq.com>
This commit is contained in:
parent
ecc2afd164
commit
a2ffa3cb3a
|
@ -26,6 +26,7 @@
|
||||||
"@traptitech/markdown-it-katex": "^3.6.0",
|
"@traptitech/markdown-it-katex": "^3.6.0",
|
||||||
"@vueuse/core": "^9.13.0",
|
"@vueuse/core": "^9.13.0",
|
||||||
"highlight.js": "^11.7.0",
|
"highlight.js": "^11.7.0",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"katex": "^0.16.4",
|
"katex": "^0.16.4",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"naive-ui": "^2.34.3",
|
"naive-ui": "^2.34.3",
|
||||||
|
|
|
@ -17,6 +17,7 @@ specifiers:
|
||||||
crypto-js: ^4.1.1
|
crypto-js: ^4.1.1
|
||||||
eslint: ^8.35.0
|
eslint: ^8.35.0
|
||||||
highlight.js: ^11.7.0
|
highlight.js: ^11.7.0
|
||||||
|
html2canvas: ^1.4.1
|
||||||
husky: ^8.0.3
|
husky: ^8.0.3
|
||||||
katex: ^0.16.4
|
katex: ^0.16.4
|
||||||
less: ^4.1.3
|
less: ^4.1.3
|
||||||
|
@ -39,6 +40,7 @@ dependencies:
|
||||||
'@traptitech/markdown-it-katex': 3.6.0
|
'@traptitech/markdown-it-katex': 3.6.0
|
||||||
'@vueuse/core': 9.13.0_vue@3.2.47
|
'@vueuse/core': 9.13.0_vue@3.2.47
|
||||||
highlight.js: 11.7.0
|
highlight.js: 11.7.0
|
||||||
|
html2canvas: 1.4.1
|
||||||
katex: 0.16.4
|
katex: 0.16.4
|
||||||
markdown-it: 13.0.1
|
markdown-it: 13.0.1
|
||||||
naive-ui: 2.34.3_vue@3.2.47
|
naive-ui: 2.34.3_vue@3.2.47
|
||||||
|
@ -1345,6 +1347,11 @@ packages:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/base64-arraybuffer/1.0.2:
|
||||||
|
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
|
||||||
|
engines: {node: '>= 0.6.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/binary-extensions/2.2.0:
|
/binary-extensions/2.2.0:
|
||||||
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@ -1666,6 +1673,12 @@ packages:
|
||||||
resolution: {integrity: sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==}
|
resolution: {integrity: sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/css-line-break/2.1.0:
|
||||||
|
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
|
||||||
|
dependencies:
|
||||||
|
utrie: 1.0.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/css-render/0.15.12:
|
/css-render/0.15.12:
|
||||||
resolution: {integrity: sha512-eWzS66patiGkTTik+ipO9qNGZ+uNuGyTmnz6/+EJIiFg8+3yZRpnMwgFo8YdXhQRsiePzehnusrxVvugNjXzbw==}
|
resolution: {integrity: sha512-eWzS66patiGkTTik+ipO9qNGZ+uNuGyTmnz6/+EJIiFg8+3yZRpnMwgFo8YdXhQRsiePzehnusrxVvugNjXzbw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2757,6 +2770,14 @@ packages:
|
||||||
lru-cache: 6.0.0
|
lru-cache: 6.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/html2canvas/1.4.1:
|
||||||
|
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
dependencies:
|
||||||
|
css-line-break: 2.1.0
|
||||||
|
text-segmentation: 1.0.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/htmlparser2/8.0.1:
|
/htmlparser2/8.0.1:
|
||||||
resolution: {integrity: sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==}
|
resolution: {integrity: sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -4488,6 +4509,12 @@ packages:
|
||||||
engines: {node: '>=0.10'}
|
engines: {node: '>=0.10'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/text-segmentation/1.0.3:
|
||||||
|
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
|
||||||
|
dependencies:
|
||||||
|
utrie: 1.0.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/text-table/0.2.0:
|
/text-table/0.2.0:
|
||||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -4670,6 +4697,12 @@ packages:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/utrie/1.0.2:
|
||||||
|
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
|
||||||
|
dependencies:
|
||||||
|
base64-arraybuffer: 1.0.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/v8-compile-cache-lib/3.0.1:
|
/v8-compile-cache-lib/3.0.1:
|
||||||
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
|
@ -21,6 +21,10 @@ export default {
|
||||||
copyCode: 'Copy Code',
|
copyCode: 'Copy Code',
|
||||||
clearChat: 'Clear Chat',
|
clearChat: 'Clear Chat',
|
||||||
clearChatConfirm: 'Are you sure to clear this chat?',
|
clearChatConfirm: 'Are you sure to clear this chat?',
|
||||||
|
exportImage: 'Export Image',
|
||||||
|
exportImageConfirm: 'Are you sure to export this chat to png?',
|
||||||
|
exportSuccess: 'Export Success',
|
||||||
|
exportFailed: 'Export Failed',
|
||||||
deleteMessage: 'Delete Message',
|
deleteMessage: 'Delete Message',
|
||||||
deleteMessageConfirm: 'Are you sure to delete this message?',
|
deleteMessageConfirm: 'Are you sure to delete this message?',
|
||||||
deleteHistoryConfirm: 'Are you sure to clear this history?',
|
deleteHistoryConfirm: 'Are you sure to clear this history?',
|
||||||
|
|
|
@ -21,6 +21,10 @@ export default {
|
||||||
copyCode: '复制代码',
|
copyCode: '复制代码',
|
||||||
clearChat: '清空会话',
|
clearChat: '清空会话',
|
||||||
clearChatConfirm: '是否清空会话?',
|
clearChatConfirm: '是否清空会话?',
|
||||||
|
exportImage: '保存会话到图片',
|
||||||
|
exportImageConfirm: '是否将会话保存为图片?',
|
||||||
|
exportSuccess: '保存成功',
|
||||||
|
exportFailed: '保存失败',
|
||||||
deleteMessage: '删除消息',
|
deleteMessage: '删除消息',
|
||||||
deleteMessageConfirm: '是否删除此消息?',
|
deleteMessageConfirm: '是否删除此消息?',
|
||||||
deleteHistoryConfirm: '确定删除此记录?',
|
deleteHistoryConfirm: '确定删除此记录?',
|
||||||
|
|
|
@ -21,6 +21,10 @@ export default {
|
||||||
copyCode: '複製代碼',
|
copyCode: '複製代碼',
|
||||||
clearChat: '清空對話',
|
clearChat: '清空對話',
|
||||||
clearChatConfirm: '是否清空對話?',
|
clearChatConfirm: '是否清空對話?',
|
||||||
|
exportImage: '儲存對話為圖片',
|
||||||
|
exportImageConfirm: '是否將對話儲存為圖片?',
|
||||||
|
exportSuccess: '儲存成功',
|
||||||
|
exportFailed: '儲存失敗',
|
||||||
deleteMessage: '刪除訊息',
|
deleteMessage: '刪除訊息',
|
||||||
deleteMessageConfirm: '是否刪除此訊息?',
|
deleteMessageConfirm: '是否刪除此訊息?',
|
||||||
deleteHistoryConfirm: '確定刪除此紀錄?',
|
deleteHistoryConfirm: '確定刪除此紀錄?',
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<script setup lang='ts'>
|
<script setup lang='ts'>
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { NButton, NInput, useDialog } from 'naive-ui'
|
import { NButton, NInput, useDialog, useMessage } from 'naive-ui'
|
||||||
|
import html2canvas from 'html2canvas'
|
||||||
import { Message } from './components'
|
import { Message } from './components'
|
||||||
import { useScroll } from './hooks/useScroll'
|
import { useScroll } from './hooks/useScroll'
|
||||||
import { useChat } from './hooks/useChat'
|
import { useChat } from './hooks/useChat'
|
||||||
|
@ -16,6 +17,7 @@ let controller = new AbortController()
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
const ms = useMessage()
|
||||||
|
|
||||||
const chatStore = useChatStore()
|
const chatStore = useChatStore()
|
||||||
|
|
||||||
|
@ -268,6 +270,46 @@ async function onRegenerate(index: number) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleExport() {
|
||||||
|
if (loading.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
const d = dialog.warning({
|
||||||
|
title: t('chat.exportImage'),
|
||||||
|
content: t('chat.exportImageConfirm'),
|
||||||
|
positiveText: t('common.yes'),
|
||||||
|
negativeText: t('common.no'),
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
d.loading = true
|
||||||
|
const ele = document.getElementById('image-wrapper')
|
||||||
|
const canvas = await html2canvas(ele as HTMLDivElement)
|
||||||
|
const imgUrl = canvas.toDataURL('image/png')
|
||||||
|
const tempLink = document.createElement('a')
|
||||||
|
tempLink.style.display = 'none'
|
||||||
|
tempLink.href = imgUrl
|
||||||
|
tempLink.setAttribute('download', 'chat-shot.png')
|
||||||
|
if (typeof tempLink.download === 'undefined')
|
||||||
|
tempLink.setAttribute('target', '_blank')
|
||||||
|
|
||||||
|
document.body.appendChild(tempLink)
|
||||||
|
tempLink.click()
|
||||||
|
document.body.removeChild(tempLink)
|
||||||
|
window.URL.revokeObjectURL(imgUrl)
|
||||||
|
d.loading = false
|
||||||
|
ms.success(t('chat.exportSuccess'))
|
||||||
|
Promise.resolve()
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
ms.error(t('chat.exportFailed'))
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
d.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function handleDelete(index: number) {
|
function handleDelete(index: number) {
|
||||||
if (loading.value)
|
if (loading.value)
|
||||||
return
|
return
|
||||||
|
@ -360,9 +402,8 @@ onUnmounted(() => {
|
||||||
id="scrollRef"
|
id="scrollRef"
|
||||||
ref="scrollRef"
|
ref="scrollRef"
|
||||||
class="h-full overflow-hidden overflow-y-auto"
|
class="h-full overflow-hidden overflow-y-auto"
|
||||||
:class="[isMobile ? 'p-2' : 'p-4']"
|
|
||||||
>
|
>
|
||||||
<div class="w-full max-w-screen-xl m-auto">
|
<div id="image-wrapper" class="w-full max-w-screen-xl m-auto" :class="[isMobile ? 'p-2' : 'p-4']">
|
||||||
<template v-if="!dataSources.length">
|
<template v-if="!dataSources.length">
|
||||||
<div class="flex items-center justify-center mt-4 text-center text-neutral-300">
|
<div class="flex items-center justify-center mt-4 text-center text-neutral-300">
|
||||||
<SvgIcon icon="ri:bubble-chart-fill" class="mr-2 text-3xl" />
|
<SvgIcon icon="ri:bubble-chart-fill" class="mr-2 text-3xl" />
|
||||||
|
@ -403,6 +444,11 @@ onUnmounted(() => {
|
||||||
<SvgIcon icon="ri:delete-bin-line" />
|
<SvgIcon icon="ri:delete-bin-line" />
|
||||||
</span>
|
</span>
|
||||||
</HoverButton>
|
</HoverButton>
|
||||||
|
<HoverButton @click="handleExport">
|
||||||
|
<span class="text-xl text-[#4f555e] dark:text-white">
|
||||||
|
<SvgIcon icon="ri:download-2-line" />
|
||||||
|
</span>
|
||||||
|
</HoverButton>
|
||||||
<NInput
|
<NInput
|
||||||
v-model:value="prompt"
|
v-model:value="prompt"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
|
|
Loading…
Reference in New Issue