fix: PDF export cross-domain (#3945)
parent
79b590d94f
commit
d0266de89f
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@
|
||||||
@date:2025/6/6 11:18
|
@date:2025/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]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue