fix: PDF export cross-domain (#3945)

v3.2
shaohuzhang1 2025-08-27 14:23:58 +08:00 committed by GitHub
parent 79b590d94f
commit d0266de89f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 101 additions and 24 deletions

View File

@ -22,7 +22,8 @@ def get_default_option(option_list, _type, value_field):
if option_list is not None and isinstance(option_list, list) and len(option_list) > 0: if option_list is not None and isinstance(option_list, list) and len(option_list) > 0:
default_value_list = [o.get(value_field) for o in option_list if o.get('default')] default_value_list = [o.get(value_field) for o in option_list if o.get('default')]
if len(default_value_list) == 0: if len(default_value_list) == 0:
return option_list[0].get(value_field) return [o.get(value_field) for o in option_list] if _type == 'MultiSelect' else option_list[0].get(
value_field)
else: else:
if _type == 'MultiSelect': if _type == 'MultiSelect':
return default_value_list return default_value_list

View File

@ -8,6 +8,7 @@ urlpatterns = [
path('embed', views.ChatEmbedView.as_view()), path('embed', views.ChatEmbedView.as_view()),
path('auth/anonymous', views.AnonymousAuthentication.as_view()), path('auth/anonymous', views.AnonymousAuthentication.as_view()),
path('profile', views.AuthProfile.as_view()), path('profile', views.AuthProfile.as_view()),
path('resource_proxy',views.ResourceProxy.as_view()),
path('application/profile', views.ApplicationProfile.as_view(), name='profile'), path('application/profile', views.ApplicationProfile.as_view(), name='profile'),
path('chat_message/<str:chat_id>', views.ChatView.as_view(), name='chat'), path('chat_message/<str:chat_id>', views.ChatView.as_view(), name='chat'),
path('open', views.OpenView.as_view(), name='open'), path('open', views.OpenView.as_view(), name='open'),

View File

@ -6,7 +6,8 @@
@date2025/6/6 11:18 @date2025/6/6 11:18
@desc: @desc:
""" """
from django.http import HttpResponse import requests
from django.http import HttpResponse, StreamingHttpResponse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema from drf_spectacular.utils import extend_schema
from rest_framework.parsers import MultiPartParser from rest_framework.parsers import MultiPartParser
@ -30,6 +31,39 @@ from users.api import CaptchaAPI
from users.serializers.login import CaptchaSerializer from users.serializers.login import CaptchaSerializer
def stream_image(response):
"""生成器函数,用于流式传输图片数据"""
for chunk in response.iter_content(chunk_size=4096):
if chunk: # 过滤掉保持连接的空块
yield chunk
class ResourceProxy(APIView):
def get(self, request: Request):
image_url = request.query_params.get("url")
if not image_url:
return result.error("Missing 'url' parameter")
try:
# 发送GET请求流式获取图片内容
response = requests.get(
image_url,
stream=True, # 启用流式响应
allow_redirects=True,
timeout=10
)
content_type = response.headers.get('Content-Type', '').split(';')[0]
# 创建Django流式响应
django_response = StreamingHttpResponse(
stream_image(response), # 使用生成器
content_type=content_type
)
return django_response
except Exception as e:
return result.error(f"Image request failed: {str(e)}")
class OpenAIView(APIView): class OpenAIView(APIView):
authentication_classes = [TokenAuth] authentication_classes = [TokenAuth]

View File

@ -55,7 +55,7 @@ config({
} }
tokens[idx].attrSet( tokens[idx].attrSet(
'onerror', 'onerror',
'this.src="/${window.MaxKB.prefix}/assets/load_error.png";this.onerror=null;this.height="33px"', `this.src="./assets/load_error.png";this.onerror=null;this.height="33px"`,
) )
return md.renderer.renderToken(tokens, idx, options) return md.renderer.renderToken(tokens, idx, options)
} }

View File

@ -11,6 +11,7 @@
v-loading="loading" v-loading="loading"
style="height: calc(70vh - 150px); overflow-y: auto; display: flex; justify-content: center" style="height: calc(70vh - 150px); overflow-y: auto; display: flex; justify-content: center"
> >
<div ref="cloneContainerRef" style="width: 100%"></div>
<div ref="svgContainerRef"></div> <div ref="svgContainerRef"></div>
</div> </div>
<template #footer> <template #footer>
@ -39,6 +40,7 @@ import html2Canvas from 'html2canvas'
import { jsPDF } from 'jspdf' import { jsPDF } from 'jspdf'
const loading = ref<boolean>(false) const loading = ref<boolean>(false)
const svgContainerRef = ref() const svgContainerRef = ref()
const cloneContainerRef = ref()
const dialogVisible = ref<boolean>(false) const dialogVisible = ref<boolean>(false)
const open = (element: HTMLElement | null) => { const open = (element: HTMLElement | null) => {
dialogVisible.value = true dialogVisible.value = true
@ -46,28 +48,66 @@ const open = (element: HTMLElement | null) => {
if (!element) { if (!element) {
return return
} }
setTimeout(() => { const cElement = element.cloneNode(true) as HTMLElement
nextTick(() => { const images = cElement.querySelectorAll('img')
htmlToImage const loadPromises = Array.from(images).map((img) => {
.toSvg(element, { pixelRatio: 1, quality: 1 }) if (!img.src.startsWith(window.origin) && img.src.startsWith('http')) {
.then((dataUrl) => { img.src = `${window.MaxKB.prefix}/api/resource_proxy?url=${encodeURIComponent(img.src)}`
return fetch(dataUrl) }
.then((response) => { img.setAttribute('onerror', '')
return response.text() return new Promise((resolve) => {
}) // resolve
.then((text) => { if (img.complete) {
const parser = new DOMParser() resolve({ img, success: img.naturalWidth > 0 })
const svgDoc = parser.parseFromString(text, 'image/svg+xml') return
const svgElement = svgDoc.documentElement }
svgContainerRef.value.appendChild(svgElement)
svgContainerRef.value.style.height = svgElement.scrollHeight + 'px' //
}) img.onload = () => resolve({ img, success: true })
}) img.onerror = () => resolve({ img, success: false })
.finally(() => {
loading.value = false
})
}) })
}, 1) })
Promise.all(loadPromises).finally(() => {
setTimeout(() => {
nextTick(() => {
cloneContainerRef.value.appendChild(cElement)
htmlToImage
.toSvg(cElement, {
pixelRatio: 1,
quality: 1,
onImageErrorHandler: (
event: Event | string,
source?: string,
lineno?: number,
colno?: number,
error?: Error,
) => {
console.log(event, source, lineno, colno, error)
},
})
.then((dataUrl) => {
return fetch(dataUrl)
.then((response) => {
return response.text()
})
.then((text) => {
const parser = new DOMParser()
const svgDoc = parser.parseFromString(text, 'image/svg+xml')
cloneContainerRef.value.style.display = 'none'
const svgElement = svgDoc.documentElement
svgContainerRef.value.appendChild(svgElement)
svgContainerRef.value.style.height = svgElement.scrollHeight + 'px'
})
})
.finally(() => {
loading.value = false
})
.catch((e) => {
loading.value = false
})
})
}, 1)
})
} }
const exportPDF = () => { const exportPDF = () => {
@ -75,6 +115,7 @@ const exportPDF = () => {
setTimeout(() => { setTimeout(() => {
nextTick(() => { nextTick(() => {
html2Canvas(svgContainerRef.value, { html2Canvas(svgContainerRef.value, {
scale: 2,
logging: false, logging: false,
}) })
.then((canvas) => { .then((canvas) => {