fix: PDF export cross-domain (#3945)

This commit is contained in:
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:
default_value_list = [o.get(value_field) for o in option_list if o.get('default')]
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:
if _type == 'MultiSelect':
return default_value_list

View File

@ -8,6 +8,7 @@ urlpatterns = [
path('embed', views.ChatEmbedView.as_view()),
path('auth/anonymous', views.AnonymousAuthentication.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('chat_message/<str:chat_id>', views.ChatView.as_view(), name='chat'),
path('open', views.OpenView.as_view(), name='open'),

View File

@ -6,7 +6,8 @@
@date2025/6/6 11:18
@desc:
"""
from django.http import HttpResponse
import requests
from django.http import HttpResponse, StreamingHttpResponse
from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema
from rest_framework.parsers import MultiPartParser
@ -30,6 +31,39 @@ from users.api import CaptchaAPI
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):
authentication_classes = [TokenAuth]

View File

@ -55,7 +55,7 @@ config({
}
tokens[idx].attrSet(
'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)
}

View File

@ -11,6 +11,7 @@
v-loading="loading"
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>
<template #footer>
@ -39,6 +40,7 @@ import html2Canvas from 'html2canvas'
import { jsPDF } from 'jspdf'
const loading = ref<boolean>(false)
const svgContainerRef = ref()
const cloneContainerRef = ref()
const dialogVisible = ref<boolean>(false)
const open = (element: HTMLElement | null) => {
dialogVisible.value = true
@ -46,28 +48,66 @@ const open = (element: HTMLElement | null) => {
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
})
const cElement = element.cloneNode(true) as HTMLElement
const images = cElement.querySelectorAll('img')
const loadPromises = Array.from(images).map((img) => {
if (!img.src.startsWith(window.origin) && img.src.startsWith('http')) {
img.src = `${window.MaxKB.prefix}/api/resource_proxy?url=${encodeURIComponent(img.src)}`
}
img.setAttribute('onerror', '')
return new Promise((resolve) => {
// resolve
if (img.complete) {
resolve({ img, success: img.naturalWidth > 0 })
return
}
//
img.onload = () => resolve({ img, success: true })
img.onerror = () => resolve({ img, success: 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 = () => {
@ -75,6 +115,7 @@ const exportPDF = () => {
setTimeout(() => {
nextTick(() => {
html2Canvas(svgContainerRef.value, {
scale: 2,
logging: false,
})
.then((canvas) => {