Skip to content

Commit 76a4b79

Browse files
committed
fix: PDF export cross-domain
1 parent a05573b commit 76a4b79

File tree

5 files changed

+101
-24
lines changed

5 files changed

+101
-24
lines changed

apps/application/flow/step_node/form_node/impl/base_form_node.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ def get_default_option(option_list, _type, value_field):
2222
if option_list is not None and isinstance(option_list, list) and len(option_list) > 0:
2323
default_value_list = [o.get(value_field) for o in option_list if o.get('default')]
2424
if len(default_value_list) == 0:
25-
return option_list[0].get(value_field)
25+
return [o.get(value_field) for o in option_list] if _type == 'MultiSelect' else option_list[0].get(
26+
value_field)
2627
else:
2728
if _type == 'MultiSelect':
2829
return default_value_list

apps/chat/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
path('embed', views.ChatEmbedView.as_view()),
99
path('auth/anonymous', views.AnonymousAuthentication.as_view()),
1010
path('profile', views.AuthProfile.as_view()),
11+
path('resource_proxy',views.ResourceProxy.as_view()),
1112
path('application/profile', views.ApplicationProfile.as_view(), name='profile'),
1213
path('chat_message/<str:chat_id>', views.ChatView.as_view(), name='chat'),
1314
path('open', views.OpenView.as_view(), name='open'),

apps/chat/views/chat.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
@date:2025/6/6 11:18
77
@desc:
88
"""
9-
from django.http import HttpResponse
9+
import requests
10+
from django.http import HttpResponse, StreamingHttpResponse
1011
from django.utils.translation import gettext_lazy as _
1112
from drf_spectacular.utils import extend_schema
1213
from rest_framework.parsers import MultiPartParser
@@ -30,6 +31,39 @@
3031
from users.serializers.login import CaptchaSerializer
3132

3233

34+
def stream_image(response):
35+
"""生成器函数,用于流式传输图片数据"""
36+
for chunk in response.iter_content(chunk_size=4096):
37+
if chunk: # 过滤掉保持连接的空块
38+
yield chunk
39+
40+
41+
class ResourceProxy(APIView):
42+
def get(self, request: Request):
43+
image_url = request.query_params.get("url")
44+
if not image_url:
45+
return result.error("Missing 'url' parameter")
46+
try:
47+
48+
# 发送GET请求,流式获取图片内容
49+
response = requests.get(
50+
image_url,
51+
stream=True, # 启用流式响应
52+
allow_redirects=True,
53+
timeout=10
54+
)
55+
content_type = response.headers.get('Content-Type', '').split(';')[0]
56+
# 创建Django流式响应
57+
django_response = StreamingHttpResponse(
58+
stream_image(response), # 使用生成器
59+
content_type=content_type
60+
)
61+
62+
return django_response
63+
except Exception as e:
64+
return result.error(f"Image request failed: {str(e)}")
65+
66+
3367
class OpenAIView(APIView):
3468
authentication_classes = [TokenAuth]
3569

ui/src/components/markdown/MdRenderer.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ config({
5555
}
5656
tokens[idx].attrSet(
5757
'onerror',
58-
'this.src="/${window.MaxKB.prefix}/assets/load_error.png";this.onerror=null;this.height="33px"',
58+
`this.src="./assets/load_error.png";this.onerror=null;this.height="33px"`,
5959
)
6060
return md.renderer.renderToken(tokens, idx, options)
6161
}

ui/src/components/pdf-export/index.vue

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
v-loading="loading"
1212
style="height: calc(70vh - 150px); overflow-y: auto; display: flex; justify-content: center"
1313
>
14+
<div ref="cloneContainerRef" style="width: 100%"></div>
1415
<div ref="svgContainerRef"></div>
1516
</div>
1617
<template #footer>
@@ -39,42 +40,82 @@ import html2Canvas from 'html2canvas'
3940
import { jsPDF } from 'jspdf'
4041
const loading = ref<boolean>(false)
4142
const svgContainerRef = ref()
43+
const cloneContainerRef = ref()
4244
const dialogVisible = ref<boolean>(false)
4345
const open = (element: HTMLElement | null) => {
4446
dialogVisible.value = true
4547
loading.value = true
4648
if (!element) {
4749
return
4850
}
49-
setTimeout(() => {
50-
nextTick(() => {
51-
htmlToImage
52-
.toSvg(element, { pixelRatio: 1, quality: 1 })
53-
.then((dataUrl) => {
54-
return fetch(dataUrl)
55-
.then((response) => {
56-
return response.text()
57-
})
58-
.then((text) => {
59-
const parser = new DOMParser()
60-
const svgDoc = parser.parseFromString(text, 'image/svg+xml')
61-
const svgElement = svgDoc.documentElement
62-
svgContainerRef.value.appendChild(svgElement)
63-
svgContainerRef.value.style.height = svgElement.scrollHeight + 'px'
64-
})
65-
})
66-
.finally(() => {
67-
loading.value = false
68-
})
51+
const cElement = element.cloneNode(true) as HTMLElement
52+
const images = cElement.querySelectorAll('img')
53+
const loadPromises = Array.from(images).map((img) => {
54+
if (!img.src.startsWith(window.origin) && img.src.startsWith('http')) {
55+
img.src = `${window.MaxKB.prefix}/api/resource_proxy?url=${encodeURIComponent(img.src)}`
56+
}
57+
img.setAttribute('onerror', '')
58+
return new Promise((resolve) => {
59+
// 已加载完成的图片直接 resolve
60+
if (img.complete) {
61+
resolve({ img, success: img.naturalWidth > 0 })
62+
return
63+
}
64+
65+
// 未加载完成的图片监听事件
66+
img.onload = () => resolve({ img, success: true })
67+
img.onerror = () => resolve({ img, success: false })
6968
})
70-
}, 1)
69+
})
70+
Promise.all(loadPromises).finally(() => {
71+
setTimeout(() => {
72+
nextTick(() => {
73+
cloneContainerRef.value.appendChild(cElement)
74+
htmlToImage
75+
.toSvg(cElement, {
76+
pixelRatio: 1,
77+
quality: 1,
78+
onImageErrorHandler: (
79+
event: Event | string,
80+
source?: string,
81+
lineno?: number,
82+
colno?: number,
83+
error?: Error,
84+
) => {
85+
console.log(event, source, lineno, colno, error)
86+
},
87+
})
88+
.then((dataUrl) => {
89+
return fetch(dataUrl)
90+
.then((response) => {
91+
return response.text()
92+
})
93+
.then((text) => {
94+
const parser = new DOMParser()
95+
const svgDoc = parser.parseFromString(text, 'image/svg+xml')
96+
cloneContainerRef.value.style.display = 'none'
97+
const svgElement = svgDoc.documentElement
98+
svgContainerRef.value.appendChild(svgElement)
99+
svgContainerRef.value.style.height = svgElement.scrollHeight + 'px'
100+
})
101+
})
102+
.finally(() => {
103+
loading.value = false
104+
})
105+
.catch((e) => {
106+
loading.value = false
107+
})
108+
})
109+
}, 1)
110+
})
71111
}
72112
73113
const exportPDF = () => {
74114
loading.value = true
75115
setTimeout(() => {
76116
nextTick(() => {
77117
html2Canvas(svgContainerRef.value, {
118+
scale: 2,
78119
logging: false,
79120
})
80121
.then((canvas) => {

0 commit comments

Comments
 (0)