-
Notifications
You must be signed in to change notification settings - Fork 2.6k
feat: Export conversation page to PDF #3941
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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', | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The provided code snippet appears to be part of an object exported as Potential Issues:
Optimization Suggestions:
Overall, the code looks clean and self-contained, which is good for its purpose in managing localization data. Make sure the translations are up-to-date for accuracy and completeness. |
||
|
|
||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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( | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
import type { Ref } from 'vue';
import { ref } from 'vue';
const pdfExportRef: Ref<InstanceType<typeof PdfExport>> | null = ref(null);The rest of your code looks well-written except for those issues mentioned above. |
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The provided Vue component appears to be designed to create a dialog with SVG content that allows users to export it as PDF or image. Here is a few points of consideration for optimization:
SVG Loading: The current approach involves converting an SVG string into an HTML element, which can lead to some overhead due to parsing and appending. Consider using a more direct method to handle inline SVG files directly.
Error Handling: Implement robust error handling in the
fetchoperations and other asynchronous calls to improve user experience. This includes providing meaningful messages when exports fail.Memory Management: Ensure that SVG elements are removed from memory after they are no longer needed to prevent excessive memory usage.
PDF Export Optimization:
jspdf-autotableto manage text wrapping and formatting automatically.Code Readability: Add comments and structure the code better to make it easier to understand at a glance.
Here's a revised version with these considerations:
Key Changes:
<img>tag within a hidden div (ref="content"), which simplifies rendering.convertBlobToDataUrlis created to asynchronously read the blob content as a data URL.