mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-26 01:33:05 +00:00
fix: PDF export cross-domain (#3945)
This commit is contained in:
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:
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
@date:2025/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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue