mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-26 01:33:05 +00:00
feat: Export conversation page to PDF (#3941)
This commit is contained in:
parent
39fd66e84b
commit
7948e4f78b
|
|
@ -32,6 +32,7 @@
|
|||
"element-plus": "^2.10.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"highlight.js": "^11.11.1",
|
||||
"html-to-image": "^1.11.13",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^3.0.1",
|
||||
"katex": "^0.16.10",
|
||||
|
|
@ -45,6 +46,7 @@
|
|||
"recorder-core": "^1.3.25011100",
|
||||
"screenfull": "^6.0.2",
|
||||
"sortablejs": "^1.15.6",
|
||||
"svg2pdf.js": "^2.5.0",
|
||||
"use-element-plus-theme": "^0.0.5",
|
||||
"vite-plugin-html": "^3.2.2",
|
||||
"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',
|
||||
question_count: 'Questions',
|
||||
exportRecords: 'Export Chat History',
|
||||
exportPDF: 'Export PDF',
|
||||
exportImg: 'Exporting images',
|
||||
preview: 'Preview',
|
||||
chatId: 'Chat ID',
|
||||
userInput: 'User Input',
|
||||
quote: 'Quote',
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ export default {
|
|||
only20history: '仅显示最近 20 条对话',
|
||||
question_count: '条提问',
|
||||
exportRecords: '导出聊天记录',
|
||||
exportPDF: '导出PDF',
|
||||
exportImg: '导出图片',
|
||||
preview: '预览',
|
||||
chatId: '对话 ID',
|
||||
chatUserId: '对话用户 ID',
|
||||
chatUserType: '对话用户类型',
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ export default {
|
|||
only20history: '僅顯示最近 20 條對話',
|
||||
question_count: '條提問',
|
||||
exportRecords: '導出聊天記錄',
|
||||
exportPDF: '匯出PDF',
|
||||
exportImg: '匯出圖片',
|
||||
preview: '預覽',
|
||||
chatId: '對話 ID',
|
||||
userInput: '用戶輸入',
|
||||
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"
|
||||
>{{ $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
|
||||
>
|
||||
</el-dropdown-menu>
|
||||
|
|
@ -169,7 +169,7 @@
|
|||
<div class="execution-detail-panel" :resizable="false" collapsible>
|
||||
<div class="p-16 flex-between border-b">
|
||||
<h4 class="medium ellipsis" :title="rightPanelTitle">{{ rightPanelTitle }}</h4>
|
||||
|
||||
|
||||
<div class="flex align-center">
|
||||
<span v-if="rightPanelType === 'paragraphDocument'" class="mr-4">
|
||||
<a
|
||||
|
|
@ -217,6 +217,7 @@
|
|||
emitConfirm
|
||||
@confirm="handleResetPassword"
|
||||
></ResetPassword>
|
||||
<PdfExport ref="pdfExportRef"></PdfExport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -238,12 +239,14 @@ import ParagraphDocumentContent from '@/components/ai-chat/component/knowledge-s
|
|||
import HistoryPanel from '@/views/chat/component/HistoryPanel.vue'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { getFileUrl } from '@/utils/common'
|
||||
import { exportToPDF } from '@/utils/htmlToPdf'
|
||||
import PdfExport from '@/components/pdf-export/index.vue'
|
||||
useResize()
|
||||
|
||||
const pdfExportRef = ref<InstanceType<typeof PdfExport>>()
|
||||
const { common, chatUser } = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
const openPDFExport = () => {
|
||||
pdfExportRef.value?.open(document.getElementById('chatListId'))
|
||||
}
|
||||
const isCollapse = ref(false)
|
||||
const isPcCollapse = ref(false)
|
||||
watch(
|
||||
|
|
|
|||
Loading…
Reference in New Issue