mirror of
https://github.com/KwiTsukasa/kt-template-online-playground.git
synced 2026-05-27 16:45:45 +08:00
fix: 裁剪预览截图内容区
This commit is contained in:
parent
526dcfb6cd
commit
99236ba861
@ -32,6 +32,7 @@ const componentTypeList = ref<DictItem[]>([])
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const message = ref('')
|
const message = ref('')
|
||||||
const messageType = ref<'info' | 'success' | 'error'>('info')
|
const messageType = ref<'info' | 'success' | 'error'>('info')
|
||||||
|
const screenshotPadding = 6
|
||||||
|
|
||||||
const isEdit = computed(() => !!form.id)
|
const isEdit = computed(() => !!form.id)
|
||||||
const canSave = computed(
|
const canSave = computed(
|
||||||
@ -78,6 +79,169 @@ function isCanvasSecurityError(err: unknown) {
|
|||||||
return err instanceof DOMException && err.name === 'SecurityError'
|
return err instanceof DOMException && err.name === 'SecurityError'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ScreenshotBounds = {
|
||||||
|
left: number
|
||||||
|
top: number
|
||||||
|
right: number
|
||||||
|
bottom: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScreenshotArea = {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
windowWidth: number
|
||||||
|
windowHeight: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeBounds(
|
||||||
|
bounds: ScreenshotBounds | undefined,
|
||||||
|
rect: Pick<DOMRect, 'left' | 'top' | 'right' | 'bottom' | 'width' | 'height'>,
|
||||||
|
) {
|
||||||
|
if (rect.width <= 0 || rect.height <= 0) {
|
||||||
|
return bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bounds) {
|
||||||
|
return {
|
||||||
|
left: rect.left,
|
||||||
|
top: rect.top,
|
||||||
|
right: rect.right,
|
||||||
|
bottom: rect.bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: Math.min(bounds.left, rect.left),
|
||||||
|
top: Math.min(bounds.top, rect.top),
|
||||||
|
right: Math.max(bounds.right, rect.right),
|
||||||
|
bottom: Math.max(bounds.bottom, rect.bottom),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTransparentColor(value: string) {
|
||||||
|
return (
|
||||||
|
!value ||
|
||||||
|
value === 'transparent' ||
|
||||||
|
value === 'rgba(0, 0, 0, 0)' ||
|
||||||
|
value === 'rgb(0 0 0 / 0)'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPaintedBox(style: CSSStyleDeclaration) {
|
||||||
|
const borderWidth =
|
||||||
|
Number.parseFloat(style.borderTopWidth) +
|
||||||
|
Number.parseFloat(style.borderRightWidth) +
|
||||||
|
Number.parseFloat(style.borderBottomWidth) +
|
||||||
|
Number.parseFloat(style.borderLeftWidth)
|
||||||
|
|
||||||
|
return (
|
||||||
|
style.backgroundImage !== 'none' ||
|
||||||
|
!isTransparentColor(style.backgroundColor) ||
|
||||||
|
borderWidth > 0 ||
|
||||||
|
style.boxShadow !== 'none' ||
|
||||||
|
style.outlineStyle !== 'none'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVisibleElement(element: Element, win: Window) {
|
||||||
|
const style = win.getComputedStyle(element)
|
||||||
|
|
||||||
|
return (
|
||||||
|
style.display !== 'none' &&
|
||||||
|
style.visibility !== 'hidden' &&
|
||||||
|
Number(style.opacity) !== 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRenderableElement(element: Element, win: Window) {
|
||||||
|
const tagName = element.tagName.toLowerCase()
|
||||||
|
|
||||||
|
return (
|
||||||
|
[
|
||||||
|
'button',
|
||||||
|
'canvas',
|
||||||
|
'img',
|
||||||
|
'input',
|
||||||
|
'select',
|
||||||
|
'svg',
|
||||||
|
'table',
|
||||||
|
'textarea',
|
||||||
|
'video',
|
||||||
|
].includes(tagName) || hasPaintedBox(win.getComputedStyle(element))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContentBounds(root: HTMLElement, doc: Document) {
|
||||||
|
const win = doc.defaultView || window
|
||||||
|
let bounds: ScreenshotBounds | undefined
|
||||||
|
|
||||||
|
root.querySelectorAll('*').forEach((element) => {
|
||||||
|
if (!isVisibleElement(element, win) || !isRenderableElement(element, win)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bounds = mergeBounds(bounds, element.getBoundingClientRect())
|
||||||
|
})
|
||||||
|
|
||||||
|
const walker = doc.createTreeWalker(root, NodeFilter.SHOW_TEXT)
|
||||||
|
let node = walker.nextNode()
|
||||||
|
|
||||||
|
while (node) {
|
||||||
|
const text = node.textContent?.trim()
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
const range = doc.createRange()
|
||||||
|
range.selectNodeContents(node)
|
||||||
|
Array.from(range.getClientRects()).forEach((rect) => {
|
||||||
|
bounds = mergeBounds(bounds, rect)
|
||||||
|
})
|
||||||
|
range.detach()
|
||||||
|
}
|
||||||
|
|
||||||
|
node = walker.nextNode()
|
||||||
|
}
|
||||||
|
|
||||||
|
return bounds || mergeBounds(undefined, root.getBoundingClientRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPreviewContentArea(doc: Document, iframe: HTMLIFrameElement) {
|
||||||
|
const root = (doc.querySelector('#app') as HTMLElement | null) || doc.body
|
||||||
|
const bounds = getContentBounds(root, doc)
|
||||||
|
|
||||||
|
if (!bounds) {
|
||||||
|
throw new Error('预览内容为空,无法生成截图')
|
||||||
|
}
|
||||||
|
|
||||||
|
const win = doc.defaultView
|
||||||
|
const scrollX = win?.scrollX || 0
|
||||||
|
const scrollY = win?.scrollY || 0
|
||||||
|
const x = Math.max(0, Math.floor(bounds.left + scrollX - screenshotPadding))
|
||||||
|
const y = Math.max(0, Math.floor(bounds.top + scrollY - screenshotPadding))
|
||||||
|
const right = Math.ceil(bounds.right + scrollX + screenshotPadding)
|
||||||
|
const bottom = Math.ceil(bounds.bottom + scrollY + screenshotPadding)
|
||||||
|
const width = Math.max(1, right - x)
|
||||||
|
const height = Math.max(1, bottom - y)
|
||||||
|
|
||||||
|
return {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
windowWidth: Math.max(
|
||||||
|
iframe.clientWidth,
|
||||||
|
doc.documentElement.scrollWidth,
|
||||||
|
right,
|
||||||
|
),
|
||||||
|
windowHeight: Math.max(
|
||||||
|
iframe.clientHeight,
|
||||||
|
doc.documentElement.scrollHeight,
|
||||||
|
bottom,
|
||||||
|
),
|
||||||
|
} satisfies ScreenshotArea
|
||||||
|
}
|
||||||
|
|
||||||
async function capturePreviewImage() {
|
async function capturePreviewImage() {
|
||||||
await nextFrame()
|
await nextFrame()
|
||||||
|
|
||||||
@ -88,33 +252,28 @@ async function capturePreviewImage() {
|
|||||||
throw new Error('未找到预览区域,无法生成截图')
|
throw new Error('未找到预览区域,无法生成截图')
|
||||||
}
|
}
|
||||||
|
|
||||||
const width = Math.max(iframe.clientWidth, 1)
|
const area = getPreviewContentArea(doc, iframe)
|
||||||
const height = Math.max(iframe.clientHeight, 1)
|
|
||||||
const canvas = await html2canvas(doc.body, {
|
const canvas = await html2canvas(doc.body, {
|
||||||
allowTaint: false,
|
allowTaint: false,
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
height,
|
height: area.height,
|
||||||
imageTimeout: 15000,
|
imageTimeout: 15000,
|
||||||
logging: false,
|
logging: false,
|
||||||
proxy: getResourceProxyEndpoint(),
|
proxy: getResourceProxyEndpoint(),
|
||||||
scale: Math.min(window.devicePixelRatio || 1, 2),
|
scale: Math.min(window.devicePixelRatio || 1, 2),
|
||||||
useCORS: false,
|
useCORS: false,
|
||||||
width,
|
width: area.width,
|
||||||
windowHeight: height,
|
windowHeight: area.windowHeight,
|
||||||
windowWidth: width,
|
windowWidth: area.windowWidth,
|
||||||
x: 0,
|
x: area.x,
|
||||||
y: 0,
|
y: area.y,
|
||||||
scrollX: 0,
|
scrollX: 0,
|
||||||
scrollY: 0,
|
scrollY: 0,
|
||||||
onclone: (clonedDoc) => {
|
onclone: (clonedDoc) => {
|
||||||
const clonedBody = clonedDoc.body
|
const clonedBody = clonedDoc.body
|
||||||
|
|
||||||
// 交给 html2canvas 处理资源加载,只在克隆文档里固定截图视口,避免内容因 body 尺寸漂移。
|
// 交给 html2canvas 处理资源加载,只清理脚本并固定背景,截图范围由真实内容边界裁剪。
|
||||||
clonedBody.querySelectorAll('script').forEach((item) => item.remove())
|
clonedBody.querySelectorAll('script').forEach((item) => item.remove())
|
||||||
clonedBody.style.width = `${width}px`
|
|
||||||
clonedBody.style.height = `${height}px`
|
|
||||||
clonedBody.style.margin = '0'
|
|
||||||
clonedBody.style.overflow = 'hidden'
|
|
||||||
clonedBody.style.background = '#ffffff'
|
clonedBody.style.background = '#ffffff'
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user