feat: chat export pdf (#3931)

Co-authored-by: wangdan-fit2cloud <dan.wang@fit2cloud.com>
This commit is contained in:
shaohuzhang1 2025-08-25 18:13:07 +08:00 committed by GitHub
parent deb7333750
commit 23c9bc545c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 99 additions and 4 deletions

View File

@ -32,6 +32,8 @@
"element-plus": "^2.10.2",
"file-saver": "^2.0.5",
"highlight.js": "^11.11.1",
"html2canvas": "^1.4.1",
"jspdf": "^3.0.1",
"katex": "^0.16.10",
"marked": "^12.0.2",
"md-editor-v3": "^5.8.2",

BIN
ui/src/assets/logo/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@ -1 +1,35 @@
<svg id="图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 232.4409 232.4409"><defs><style>.cls-1{fill:url(#未命名的渐变_7);}.cls-2{fill:url(#未命名的渐变_7-2);}.cls-3{fill:url(#未命名的渐变_7-3);}.cls-4{fill:url(#未命名的渐变_7-4);}.cls-5{fill:url(#未命名的渐变_7-5);}.cls-6{fill:url(#未命名的渐变_7-6);}</style><linearGradient id="未命名的渐变_7" x1="113.6159" y1="176.9998" x2="113.6159" y2="195.8629" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#3370FF"/><stop offset="1" stop-color="#7f3bf5"/></linearGradient><linearGradient id="未命名的渐变_7-2" x1="209.3027" y1="90.7042" x2="209.3027" y2="131.8553" xlink:href="#未命名的渐变_7"/><linearGradient id="未命名的渐变_7-3" x1="23.1384" y1="90.7042" x2="23.1384" y2="131.8553" xlink:href="#未命名的渐变_7"/><linearGradient id="未命名的渐变_7-4" x1="138.8087" y1="96.1512" x2="138.8087" y2="118.7847" xlink:href="#未命名的渐变_7"/><linearGradient id="未命名的渐变_7-5" x1="95.3622" y1="96.1512" x2="95.3622" y2="118.7847" xlink:href="#未命名的渐变_7"/><linearGradient id="未命名的渐变_7-6" x1="116.2206" y1="48.8968" x2="116.2206" y2="173.4002" xlink:href="#未命名的渐变_7"/></defs><title>MaxKB</title><path class="cls-1" d="M128.4532,177H98.7785L87.78,187.9985a4.6069,4.6069,0,0,0,3.2576,7.8644h45.1569a4.6069,4.6069,0,0,0,3.2575-7.8644Z"/><path class="cls-2" d="M210.0008,90.7042h-5.85v41.1511h5.85a4.4537,4.4537,0,0,0,4.4537-4.4537V95.1579A4.4537,4.4537,0,0,0,210.0008,90.7042Z"/><path class="cls-3" d="M28.29,90.7042H22.44a4.4538,4.4538,0,0,0-4.4538,4.4537v32.2437a4.4538,4.4538,0,0,0,4.4538,4.4537h5.85Z"/><path class="cls-4" d="M138.8087,96.1512a8.33,8.33,0,0,0-8.33,8.33v5.9727a8.33,8.33,0,1,0,16.6607,0v-5.9727A8.33,8.33,0,0,0,138.8087,96.1512Z"/><path class="cls-5" d="M95.3622,96.1512a8.33,8.33,0,0,0-8.33,8.33v5.9727a8.33,8.33,0,1,0,16.6607,0v-5.9727A8.33,8.33,0,0,0,95.3622,96.1512Z"/><path class="cls-6" d="M166.8344,48.8968H65.6064A33.7544,33.7544,0,0,0,31.89,82.6131v57.07A33.7548,33.7548,0,0,0,65.6064,173.4h101.228a33.7549,33.7549,0,0,0,33.7168-33.7168v-57.07A33.7545,33.7545,0,0,0,166.8344,48.8968Zm2.831,90.4457a6.0733,6.0733,0,0,1-6.0732,6.0733H114.2168a43.5922,43.5922,0,0,0-21.3313,5.5757l-16.5647,9.2946v-14.87h-7.472a6.0733,6.0733,0,0,1-6.0733-6.0733v-60.5a6.0733,6.0733,0,0,1,6.0733-6.0733h94.7434a6.0733,6.0733,0,0,1,6.0732,6.0733Z"/></svg>
<svg id="图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 232.4409 232.4409">
<defs>
<style>
.cls-1{fill:url(#未命名的渐变_7);}.cls-2{fill:url(#未命名的渐变_7-2);}.cls-3{fill:url(#未命名的渐变_7-3);}.cls-4{fill:url(#未命名的渐变_7-4);}.cls-5{fill:url(#未命名的渐变_7-5);}.cls-6{fill:url(#未命名的渐变_7-6);}</style>
<linearGradient id="未命名的渐变_7" x1="113.6159" y1="176.9998" x2="113.6159" y2="195.8629"
gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#3370FF" />
<stop offset="1" stop-color="#7f3bf5" />
</linearGradient>
<linearGradient id="未命名的渐变_7-2" x1="209.3027" y1="90.7042" x2="209.3027" y2="131.8553"
xlink:href="#未命名的渐变_7" />
<linearGradient id="未命名的渐变_7-3" x1="23.1384" y1="90.7042" x2="23.1384" y2="131.8553"
xlink:href="#未命名的渐变_7" />
<linearGradient id="未命名的渐变_7-4" x1="138.8087" y1="96.1512" x2="138.8087" y2="118.7847"
xlink:href="#未命名的渐变_7" />
<linearGradient id="未命名的渐变_7-5" x1="95.3622" y1="96.1512" x2="95.3622" y2="118.7847"
xlink:href="#未命名的渐变_7" />
<linearGradient id="未命名的渐变_7-6" x1="116.2206" y1="48.8968" x2="116.2206" y2="173.4002"
xlink:href="#未命名的渐变_7" />
</defs>
<title>MaxKB</title>
<path class="cls-1"
d="M128.4532,177H98.7785L87.78,187.9985a4.6069,4.6069,0,0,0,3.2576,7.8644h45.1569a4.6069,4.6069,0,0,0,3.2575-7.8644Z" />
<path class="cls-2"
d="M210.0008,90.7042h-5.85v41.1511h5.85a4.4537,4.4537,0,0,0,4.4537-4.4537V95.1579A4.4537,4.4537,0,0,0,210.0008,90.7042Z" />
<path class="cls-3"
d="M28.29,90.7042H22.44a4.4538,4.4538,0,0,0-4.4538,4.4537v32.2437a4.4538,4.4538,0,0,0,4.4538,4.4537h5.85Z" />
<path class="cls-4"
d="M138.8087,96.1512a8.33,8.33,0,0,0-8.33,8.33v5.9727a8.33,8.33,0,1,0,16.6607,0v-5.9727A8.33,8.33,0,0,0,138.8087,96.1512Z" />
<path class="cls-5"
d="M95.3622,96.1512a8.33,8.33,0,0,0-8.33,8.33v5.9727a8.33,8.33,0,1,0,16.6607,0v-5.9727A8.33,8.33,0,0,0,95.3622,96.1512Z" />
<path class="cls-6"
d="M166.8344,48.8968H65.6064A33.7544,33.7544,0,0,0,31.89,82.6131v57.07A33.7548,33.7548,0,0,0,65.6064,173.4h101.228a33.7549,33.7549,0,0,0,33.7168-33.7168v-57.07A33.7545,33.7545,0,0,0,166.8344,48.8968Zm2.831,90.4457a6.0733,6.0733,0,0,1-6.0732,6.0733H114.2168a43.5922,43.5922,0,0,0-21.3313,5.5757l-16.5647,9.2946v-14.87h-7.472a6.0733,6.0733,0,0,1-6.0733-6.0733v-60.5a6.0733,6.0733,0,0,1,6.0733-6.0733h94.7434a6.0733,6.0733,0,0,1,6.0732,6.0733Z" />
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -26,7 +26,7 @@
</div>
<template v-if="!(isUserInput || isAPIInput) || !firsUserInput || type === 'log'">
<el-scrollbar ref="scrollDiv" @scroll="handleScrollTop">
<div ref="dialogScrollbar" class="ai-chat__content p-16">
<div ref="dialogScrollbar" class="ai-chat__content p-16" id="chatListId">
<PrologueContent
:type="type"
:application="applicationDetails"

View File

@ -32,7 +32,7 @@
d="M166.8344,48.8968H65.6064A33.7544,33.7544,0,0,0,31.89,82.6131v57.07A33.7548,33.7548,0,0,0,65.6064,173.4h101.228a33.7549,33.7549,0,0,0,33.7168-33.7168v-57.07A33.7545,33.7545,0,0,0,166.8344,48.8968Zm2.831,90.4457a6.0733,6.0733,0,0,1-6.0732,6.0733H114.2168a43.5922,43.5922,0,0,0-21.3313,5.5757l-16.5647,9.2946v-14.87h-7.472a6.0733,6.0733,0,0,1-6.0733-6.0733v-60.5a6.0733,6.0733,0,0,1,6.0733-6.0733h94.7434a6.0733,6.0733,0,0,1,6.0732,6.0733Z"
/>
</svg>
<img v-else src="@/assets/logo/logo.svg" :height="height" />
<img v-else src="@/assets/logo/logo.png" :height="height" />
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'

56
ui/src/utils/htmlToPdf.ts Normal file
View File

@ -0,0 +1,56 @@
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)
})
}

View File

@ -139,6 +139,9 @@
<el-dropdown-item @click="exportHTML"
>{{ $t('common.export') }} HTML</el-dropdown-item
>
<el-dropdown-item @click="exportToPDF('chatListId', currentChatName + '.pdf')"
>{{ $t('common.export') }} PDF</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
@ -222,7 +225,6 @@ import { ref, onMounted, nextTick, computed, watch } from 'vue'
import { marked } from 'marked'
import { saveAs } from 'file-saver'
import chatAPI from '@/api/chat/chat'
import useStore from '@/stores'
import useResize from '@/layout/hooks/useResize'
import { hexToRgba } from '@/utils/theme'
@ -236,6 +238,7 @@ 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'
useResize()
const { common, chatUser } = useStore()