feat: 增加保存会话为图片的功能 (#374)

* feat: 增加保存会话为图片的功能

* feat: 异常处理和增加导出动画

---------

Co-authored-by: ChenZhaoYu <790348264@qq.com>
main
CornerSkyless 2 years ago committed by GitHub
parent ecc2afd164
commit a2ffa3cb3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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…
Cancel
Save