feat: Export conversation page to PDF (#3941)
parent
39fd66e84b
commit
7948e4f78b
|
|
@ -32,6 +32,7 @@
|
||||||
"element-plus": "^2.10.2",
|
"element-plus": "^2.10.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
|
"html-to-image": "^1.11.13",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"jspdf": "^3.0.1",
|
"jspdf": "^3.0.1",
|
||||||
"katex": "^0.16.10",
|
"katex": "^0.16.10",
|
||||||
|
|
@ -45,6 +46,7 @@
|
||||||
"recorder-core": "^1.3.25011100",
|
"recorder-core": "^1.3.25011100",
|
||||||
"screenfull": "^6.0.2",
|
"screenfull": "^6.0.2",
|
||||||
"sortablejs": "^1.15.6",
|
"sortablejs": "^1.15.6",
|
||||||
|
"svg2pdf.js": "^2.5.0",
|
||||||
"use-element-plus-theme": "^0.0.5",
|
"use-element-plus-theme": "^0.0.5",
|
||||||
"vite-plugin-html": "^3.2.2",
|
"vite-plugin-html": "^3.2.2",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="$t('chat.preview')"
|
||||||
|
style="overflow: auto"
|
||||||
|
width="80%"
|
||||||
|
:before-close="close"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-loading="loading"
|
||||||
|
style="height: calc(70vh - 150px); overflow-y: auto; display: flex; justify-content: center"
|
||||||
|
>
|
||||||
|
<div ref="svgContainerRef"></div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button :loading="loading" @click="exportPDF">{{ $t('chat.exportPDF') }}</el-button>
|
||||||
|
<el-button
|
||||||
|
:loading="loading"
|
||||||
|
type="primary"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
loading = true
|
||||||
|
exportJepg()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ $t('chat.exportImg') }}
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import * as htmlToImage from 'html-to-image'
|
||||||
|
import { ref, nextTick } from 'vue'
|
||||||
|
import html2Canvas from 'html2canvas'
|
||||||
|
import { jsPDF } from 'jspdf'
|
||||||
|
const loading = ref<boolean>(false)
|
||||||
|
const svgContainerRef = ref()
|
||||||
|
const dialogVisible = ref<boolean>(false)
|
||||||
|
const open = (element: HTMLElement | null) => {
|
||||||
|
dialogVisible.value = true
|
||||||
|
loading.value = true
|
||||||
|
if (!element) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
htmlToImage
|
||||||
|
.toSvg(element, { pixelRatio: 1, quality: 1 })
|
||||||
|
.then((dataUrl) => {
|
||||||
|
return fetch(dataUrl)
|
||||||
|
.then((response) => {
|
||||||
|
return response.text()
|
||||||
|
})
|
||||||
|
.then((text) => {
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const svgDoc = parser.parseFromString(text, 'image/svg+xml')
|
||||||
|
const svgElement = svgDoc.documentElement
|
||||||
|
svgContainerRef.value.appendChild(svgElement)
|
||||||
|
svgContainerRef.value.style.height = svgElement.scrollHeight + 'px'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportPDF = () => {
|
||||||
|
loading.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
html2Canvas(svgContainerRef.value, {
|
||||||
|
logging: false,
|
||||||
|
})
|
||||||
|
.then((canvas) => {
|
||||||
|
const doc = new jsPDF('p', 'mm', 'a4')
|
||||||
|
// 将canvas转换为图片
|
||||||
|
const imgData = canvas.toDataURL(`image/jpeg`, 1)
|
||||||
|
// 获取PDF页面尺寸
|
||||||
|
const pageWidth = doc.internal.pageSize.getWidth()
|
||||||
|
const pageHeight = doc.internal.pageSize.getHeight()
|
||||||
|
// 计算图像在PDF中的尺寸
|
||||||
|
const imgWidth = pageWidth
|
||||||
|
const imgHeight = (canvas.height * imgWidth) / canvas.width
|
||||||
|
// 添加图像到PDF
|
||||||
|
doc.addImage(imgData, 'jpeg', 0, 0, imgWidth, imgHeight)
|
||||||
|
|
||||||
|
// 如果内容超过一页,自动添加新页面
|
||||||
|
let heightLeft = imgHeight
|
||||||
|
let position = 0
|
||||||
|
|
||||||
|
// 第一页已经添加
|
||||||
|
heightLeft -= pageHeight
|
||||||
|
|
||||||
|
// 当内容超过一页时
|
||||||
|
while (heightLeft >= 0) {
|
||||||
|
position = heightLeft - imgHeight
|
||||||
|
doc.addPage()
|
||||||
|
doc.addImage(imgData, 'jpeg', 0, position, imgWidth, imgHeight)
|
||||||
|
heightLeft -= pageHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存PDF
|
||||||
|
doc.save('导出文档.pdf')
|
||||||
|
return 'ok'
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const exportJepg = () => {
|
||||||
|
loading.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
html2Canvas(svgContainerRef.value, {
|
||||||
|
logging: false,
|
||||||
|
})
|
||||||
|
.then((canvas) => {
|
||||||
|
// 将canvas转换为图片
|
||||||
|
const imgData = canvas.toDataURL(`image/jpeg`, 1)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.download = `webpage-screenshot.jpeg`
|
||||||
|
link.href = imgData
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
return 'ok'
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, 1)
|
||||||
|
}
|
||||||
|
const close = () => {
|
||||||
|
dialogVisible.value = false
|
||||||
|
}
|
||||||
|
defineExpose({ open, close })
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
|
|
@ -9,6 +9,9 @@ export default {
|
||||||
only20history: 'Showing only the last 20 chats',
|
only20history: 'Showing only the last 20 chats',
|
||||||
question_count: 'Questions',
|
question_count: 'Questions',
|
||||||
exportRecords: 'Export Chat History',
|
exportRecords: 'Export Chat History',
|
||||||
|
exportPDF: 'Export PDF',
|
||||||
|
exportImg: 'Exporting images',
|
||||||
|
preview: 'Preview',
|
||||||
chatId: 'Chat ID',
|
chatId: 'Chat ID',
|
||||||
userInput: 'User Input',
|
userInput: 'User Input',
|
||||||
quote: 'Quote',
|
quote: 'Quote',
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ export default {
|
||||||
only20history: '仅显示最近 20 条对话',
|
only20history: '仅显示最近 20 条对话',
|
||||||
question_count: '条提问',
|
question_count: '条提问',
|
||||||
exportRecords: '导出聊天记录',
|
exportRecords: '导出聊天记录',
|
||||||
|
exportPDF: '导出PDF',
|
||||||
|
exportImg: '导出图片',
|
||||||
|
preview: '预览',
|
||||||
chatId: '对话 ID',
|
chatId: '对话 ID',
|
||||||
chatUserId: '对话用户 ID',
|
chatUserId: '对话用户 ID',
|
||||||
chatUserType: '对话用户类型',
|
chatUserType: '对话用户类型',
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ export default {
|
||||||
only20history: '僅顯示最近 20 條對話',
|
only20history: '僅顯示最近 20 條對話',
|
||||||
question_count: '條提問',
|
question_count: '條提問',
|
||||||
exportRecords: '導出聊天記錄',
|
exportRecords: '導出聊天記錄',
|
||||||
|
exportPDF: '匯出PDF',
|
||||||
|
exportImg: '匯出圖片',
|
||||||
|
preview: '預覽',
|
||||||
chatId: '對話 ID',
|
chatId: '對話 ID',
|
||||||
userInput: '用戶輸入',
|
userInput: '用戶輸入',
|
||||||
quote: '引用',
|
quote: '引用',
|
||||||
|
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
import html2Canvas from 'html2canvas'
|
|
||||||
import jsPDF from 'jspdf'
|
|
||||||
|
|
||||||
export const exportToPDF = async (elementId: string, filename = 'document.pdf') => {
|
|
||||||
const element = document.getElementById(elementId)
|
|
||||||
if (!element) return
|
|
||||||
await html2Canvas(element, {
|
|
||||||
useCORS: true,
|
|
||||||
allowTaint: true,
|
|
||||||
logging: false,
|
|
||||||
scale: 2,
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
}).then((canvas: any) => {
|
|
||||||
const pdf = new jsPDF('p', 'mm', 'a4')
|
|
||||||
const pageWidth = 190 // 保留边距后的有效宽度
|
|
||||||
const pageHeight = 277 // 保留边距后的有效高度 //A4大小,210mm x 297mm,四边各保留10mm的边距,显示区域190x277
|
|
||||||
const imgHeight = (pageHeight * canvas.width) / pageWidth
|
|
||||||
|
|
||||||
let renderedHeight = 0
|
|
||||||
while (renderedHeight < canvas.height) {
|
|
||||||
const pageCanvas = document.createElement('canvas')
|
|
||||||
pageCanvas.width = canvas.width
|
|
||||||
pageCanvas.height = Math.min(imgHeight, canvas.height - renderedHeight)
|
|
||||||
|
|
||||||
pageCanvas
|
|
||||||
.getContext('2d')!
|
|
||||||
.putImageData(
|
|
||||||
canvas
|
|
||||||
.getContext('2d')!
|
|
||||||
.getImageData(
|
|
||||||
0,
|
|
||||||
renderedHeight,
|
|
||||||
canvas.width,
|
|
||||||
Math.min(imgHeight, canvas.height - renderedHeight),
|
|
||||||
),
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
|
|
||||||
pdf.addImage(
|
|
||||||
pageCanvas.toDataURL('image/jpeg', 1.0),
|
|
||||||
'JPEG',
|
|
||||||
10,
|
|
||||||
10, // 左边距和上边距
|
|
||||||
pageWidth,
|
|
||||||
Math.min(pageHeight, (pageWidth * pageCanvas.height) / pageCanvas.width),
|
|
||||||
)
|
|
||||||
|
|
||||||
renderedHeight += imgHeight
|
|
||||||
if (renderedHeight < canvas.height) {
|
|
||||||
pdf.addPage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pdf.save(filename)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -139,7 +139,7 @@
|
||||||
<el-dropdown-item @click="exportHTML"
|
<el-dropdown-item @click="exportHTML"
|
||||||
>{{ $t('common.export') }} HTML</el-dropdown-item
|
>{{ $t('common.export') }} HTML</el-dropdown-item
|
||||||
>
|
>
|
||||||
<el-dropdown-item @click="exportToPDF('chatListId', currentChatName + '.pdf')"
|
<el-dropdown-item @click="openPDFExport"
|
||||||
>{{ $t('common.export') }} PDF</el-dropdown-item
|
>{{ $t('common.export') }} PDF</el-dropdown-item
|
||||||
>
|
>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
|
|
@ -169,7 +169,7 @@
|
||||||
<div class="execution-detail-panel" :resizable="false" collapsible>
|
<div class="execution-detail-panel" :resizable="false" collapsible>
|
||||||
<div class="p-16 flex-between border-b">
|
<div class="p-16 flex-between border-b">
|
||||||
<h4 class="medium ellipsis" :title="rightPanelTitle">{{ rightPanelTitle }}</h4>
|
<h4 class="medium ellipsis" :title="rightPanelTitle">{{ rightPanelTitle }}</h4>
|
||||||
|
|
||||||
<div class="flex align-center">
|
<div class="flex align-center">
|
||||||
<span v-if="rightPanelType === 'paragraphDocument'" class="mr-4">
|
<span v-if="rightPanelType === 'paragraphDocument'" class="mr-4">
|
||||||
<a
|
<a
|
||||||
|
|
@ -217,6 +217,7 @@
|
||||||
emitConfirm
|
emitConfirm
|
||||||
@confirm="handleResetPassword"
|
@confirm="handleResetPassword"
|
||||||
></ResetPassword>
|
></ResetPassword>
|
||||||
|
<PdfExport ref="pdfExportRef"></PdfExport>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -238,12 +239,14 @@ import ParagraphDocumentContent from '@/components/ai-chat/component/knowledge-s
|
||||||
import HistoryPanel from '@/views/chat/component/HistoryPanel.vue'
|
import HistoryPanel from '@/views/chat/component/HistoryPanel.vue'
|
||||||
import { cloneDeep } from 'lodash'
|
import { cloneDeep } from 'lodash'
|
||||||
import { getFileUrl } from '@/utils/common'
|
import { getFileUrl } from '@/utils/common'
|
||||||
import { exportToPDF } from '@/utils/htmlToPdf'
|
import PdfExport from '@/components/pdf-export/index.vue'
|
||||||
useResize()
|
useResize()
|
||||||
|
const pdfExportRef = ref<InstanceType<typeof PdfExport>>()
|
||||||
const { common, chatUser } = useStore()
|
const { common, chatUser } = useStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const openPDFExport = () => {
|
||||||
|
pdfExportRef.value?.open(document.getElementById('chatListId'))
|
||||||
|
}
|
||||||
const isCollapse = ref(false)
|
const isCollapse = ref(false)
|
||||||
const isPcCollapse = ref(false)
|
const isPcCollapse = ref(false)
|
||||||
watch(
|
watch(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue