Skip to content

Commit f6da0eb

Browse files
committed
feat: Export conversation page to PDF
1 parent e28c858 commit f6da0eb

File tree

7 files changed

+165
-61
lines changed

7 files changed

+165
-61
lines changed

ui/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"element-plus": "^2.10.2",
3333
"file-saver": "^2.0.5",
3434
"highlight.js": "^11.11.1",
35+
"html-to-image": "^1.11.13",
3536
"html2canvas": "^1.4.1",
3637
"jspdf": "^3.0.1",
3738
"katex": "^0.16.10",
@@ -45,6 +46,7 @@
4546
"recorder-core": "^1.3.25011100",
4647
"screenfull": "^6.0.2",
4748
"sortablejs": "^1.15.6",
49+
"svg2pdf.js": "^2.5.0",
4850
"use-element-plus-theme": "^0.0.5",
4951
"vite-plugin-html": "^3.2.2",
5052
"vue": "^3.5.13",
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<template>
2+
<el-dialog
3+
v-model="dialogVisible"
4+
:title="$t('chat.preview')"
5+
style="overflow: auto"
6+
width="80%"
7+
:before-close="close"
8+
destroy-on-close
9+
>
10+
<div
11+
v-loading="loading"
12+
style="height: calc(70vh - 150px); overflow-y: auto; display: flex; justify-content: center"
13+
>
14+
<div ref="svgContainerRef"></div>
15+
</div>
16+
<template #footer>
17+
<span class="dialog-footer">
18+
<el-button :loading="loading" @click="exportPDF">{{ $t('chat.exportPDF') }}</el-button>
19+
<el-button
20+
:loading="loading"
21+
type="primary"
22+
@click="
23+
() => {
24+
loading = true
25+
exportJepg()
26+
}
27+
"
28+
>
29+
{{ $t('chat.exportImg') }}
30+
</el-button>
31+
</span>
32+
</template>
33+
</el-dialog>
34+
</template>
35+
<script setup lang="ts">
36+
import * as htmlToImage from 'html-to-image'
37+
import { ref, nextTick } from 'vue'
38+
import html2Canvas from 'html2canvas'
39+
import { jsPDF } from 'jspdf'
40+
const loading = ref<boolean>(false)
41+
const svgContainerRef = ref()
42+
const dialogVisible = ref<boolean>(false)
43+
const open = (element: HTMLElement | null) => {
44+
dialogVisible.value = true
45+
loading.value = true
46+
if (!element) {
47+
return
48+
}
49+
setTimeout(() => {
50+
nextTick(() => {
51+
htmlToImage
52+
.toSvg(element, { pixelRatio: 1, quality: 1 })
53+
.then((dataUrl) => {
54+
return fetch(dataUrl)
55+
.then((response) => {
56+
return response.text()
57+
})
58+
.then((text) => {
59+
const parser = new DOMParser()
60+
const svgDoc = parser.parseFromString(text, 'image/svg+xml')
61+
const svgElement = svgDoc.documentElement
62+
svgContainerRef.value.appendChild(svgElement)
63+
svgContainerRef.value.style.height = svgElement.scrollHeight + 'px'
64+
})
65+
})
66+
.finally(() => {
67+
loading.value = false
68+
})
69+
})
70+
}, 1)
71+
}
72+
73+
const exportPDF = () => {
74+
loading.value = true
75+
setTimeout(() => {
76+
nextTick(() => {
77+
html2Canvas(svgContainerRef.value, {
78+
logging: false,
79+
})
80+
.then((canvas) => {
81+
const doc = new jsPDF('p', 'mm', 'a4')
82+
// 将canvas转换为图片
83+
const imgData = canvas.toDataURL(`image/jpeg`, 1)
84+
// 获取PDF页面尺寸
85+
const pageWidth = doc.internal.pageSize.getWidth()
86+
const pageHeight = doc.internal.pageSize.getHeight()
87+
// 计算图像在PDF中的尺寸
88+
const imgWidth = pageWidth
89+
const imgHeight = (canvas.height * imgWidth) / canvas.width
90+
// 添加图像到PDF
91+
doc.addImage(imgData, 'jpeg', 0, 0, imgWidth, imgHeight)
92+
93+
// 如果内容超过一页,自动添加新页面
94+
let heightLeft = imgHeight
95+
let position = 0
96+
97+
// 第一页已经添加
98+
heightLeft -= pageHeight
99+
100+
// 当内容超过一页时
101+
while (heightLeft >= 0) {
102+
position = heightLeft - imgHeight
103+
doc.addPage()
104+
doc.addImage(imgData, 'jpeg', 0, position, imgWidth, imgHeight)
105+
heightLeft -= pageHeight
106+
}
107+
108+
// 保存PDF
109+
doc.save('导出文档.pdf')
110+
return 'ok'
111+
})
112+
.finally(() => {
113+
loading.value = false
114+
})
115+
})
116+
})
117+
}
118+
const exportJepg = () => {
119+
loading.value = true
120+
setTimeout(() => {
121+
nextTick(() => {
122+
html2Canvas(svgContainerRef.value, {
123+
logging: false,
124+
})
125+
.then((canvas) => {
126+
// 将canvas转换为图片
127+
const imgData = canvas.toDataURL(`image/jpeg`, 1)
128+
const link = document.createElement('a')
129+
link.download = `webpage-screenshot.jpeg`
130+
link.href = imgData
131+
document.body.appendChild(link)
132+
link.click()
133+
return 'ok'
134+
})
135+
.finally(() => {
136+
loading.value = false
137+
})
138+
})
139+
}, 1)
140+
}
141+
const close = () => {
142+
dialogVisible.value = false
143+
}
144+
defineExpose({ open, close })
145+
</script>
146+
<style lang="scss" scoped></style>

ui/src/locales/lang/en-US/ai-chat.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ export default {
99
only20history: 'Showing only the last 20 chats',
1010
question_count: 'Questions',
1111
exportRecords: 'Export Chat History',
12+
exportPDF: 'Export PDF',
13+
exportImg: 'Exporting images',
14+
preview: 'Preview',
1215
chatId: 'Chat ID',
1316
userInput: 'User Input',
1417
quote: 'Quote',

ui/src/locales/lang/zh-CN/ai-chat.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ export default {
99
only20history: '仅显示最近 20 条对话',
1010
question_count: '条提问',
1111
exportRecords: '导出聊天记录',
12+
exportPDF: '导出PDF',
13+
exportImg: '导出图片',
14+
preview: '预览',
1215
chatId: '对话 ID',
1316
chatUserId: '对话用户 ID',
1417
chatUserType: '对话用户类型',

ui/src/locales/lang/zh-Hant/ai-chat.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ export default {
99
only20history: '僅顯示最近 20 條對話',
1010
question_count: '條提問',
1111
exportRecords: '導出聊天記錄',
12+
exportPDF: '匯出PDF',
13+
exportImg: '匯出圖片',
14+
preview: '預覽',
1215
chatId: '對話 ID',
1316
userInput: '用戶輸入',
1417
quote: '引用',

ui/src/utils/htmlToPdf.ts

Lines changed: 0 additions & 56 deletions
This file was deleted.

ui/src/views/chat/pc/index.vue

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@
139139
<el-dropdown-item @click="exportHTML"
140140
>{{ $t('common.export') }} HTML</el-dropdown-item
141141
>
142-
<el-dropdown-item @click="exportToPDF('chatListId', currentChatName + '.pdf')"
142+
<el-dropdown-item @click="openPDFExport"
143143
>{{ $t('common.export') }} PDF</el-dropdown-item
144144
>
145145
</el-dropdown-menu>
@@ -169,7 +169,7 @@
169169
<div class="execution-detail-panel" :resizable="false" collapsible>
170170
<div class="p-16 flex-between border-b">
171171
<h4 class="medium ellipsis" :title="rightPanelTitle">{{ rightPanelTitle }}</h4>
172-
 
172+
173173
<div class="flex align-center">
174174
<span v-if="rightPanelType === 'paragraphDocument'" class="mr-4">
175175
<a
@@ -217,6 +217,7 @@
217217
emitConfirm
218218
@confirm="handleResetPassword"
219219
></ResetPassword>
220+
<PdfExport ref="pdfExportRef"></PdfExport>
220221
</div>
221222
</template>
222223

@@ -238,12 +239,14 @@ import ParagraphDocumentContent from '@/components/ai-chat/component/knowledge-s
238239
import HistoryPanel from '@/views/chat/component/HistoryPanel.vue'
239240
import { cloneDeep } from 'lodash'
240241
import { getFileUrl } from '@/utils/common'
241-
import { exportToPDF } from '@/utils/htmlToPdf'
242+
import PdfExport from '@/components/pdf-export/index.vue'
242243
useResize()
243-
244+
const pdfExportRef = ref<InstanceType<typeof PdfExport>>()
244245
const { common, chatUser } = useStore()
245246
const router = useRouter()
246-
247+
const openPDFExport = () => {
248+
pdfExportRef.value?.open(document.getElementById('chatListId'))
249+
}
247250
const isCollapse = ref(false)
248251
const isPcCollapse = ref(false)
249252
watch(

0 commit comments

Comments
 (0)