feat: 截屏功能适配移动端

This commit is contained in:
digua
2025-12-22 22:57:26 +08:00
parent cf04e6f607
commit 30834dd007
4 changed files with 98 additions and 5 deletions
+12 -2
View File
@@ -1,6 +1,8 @@
<script setup lang="ts">
import { useScreenCapture } from '@/composables'
import { ref, onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useLayoutStore } from '@/stores/layout'
/**
* 通用截屏按钮组件
@@ -30,6 +32,8 @@ const props = withDefaults(
)
const { isCapturing, capturePage, captureElement } = useScreenCapture()
const layoutStore = useLayoutStore()
const { screenshotMobileAdapt } = storeToRefs(layoutStore)
// 生成唯一 ID 用于隐藏按钮自身
const buttonId = ref('')
@@ -40,8 +44,14 @@ onMounted(() => {
async function handleCapture(event: Event) {
const btn = event.currentTarget as HTMLElement
// 根据用户设置决定是否启用移动端适配
const defaultOptions = {
hideSelectors: [`#${buttonId.value}`],
mobileWidth: screenshotMobileAdapt.value ? true : undefined,
}
if (props.type === 'page') {
await capturePage({ hideSelectors: [`#${buttonId.value}`] })
await capturePage(defaultOptions)
} else if (props.type === 'element') {
let target: HTMLElement | null = null
@@ -52,7 +62,7 @@ async function handleCapture(event: Event) {
}
if (target) {
await captureElement(target, { hideSelectors: [`#${buttonId.value}`] })
await captureElement(target, defaultOptions)
}
}
}
@@ -1,5 +1,11 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useLayoutStore } from '@/stores/layout'
// Store
const layoutStore = useLayoutStore()
const { screenshotMobileAdapt } = storeToRefs(layoutStore)
// 缓存目录信息类型
interface CacheDirectoryInfo {
@@ -187,6 +193,23 @@ defineExpose({
</div>
</div>
<!-- 截图设置 -->
<div>
<h3 class="mb-3 flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
<UIcon name="i-heroicons-camera" class="h-4 w-4 text-blue-500" />
截图设置
</h3>
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
<div class="flex items-center justify-between">
<div class="flex-1 pr-4">
<p class="text-sm font-medium text-gray-900 dark:text-white">移动端适配</p>
<p class="text-xs text-gray-500 dark:text-gray-400">截图时自动缩放宽度适合移动端查看</p>
</div>
<USwitch v-model="screenshotMobileAdapt" />
</div>
</div>
</div>
<!-- 提示信息 -->
<div class="rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-800/50 dark:bg-amber-900/20">
<div class="flex items-start gap-2">
+55 -3
View File
@@ -7,6 +7,9 @@ import { captureAsImageData } from '@/utils/snapCapture'
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
import { useLayoutStore } from '@/stores/layout'
/** 默认移动端最大宽度 */
const DEFAULT_MOBILE_MAX_WIDTH = 525
export interface ScreenCaptureOptions {
/** 截屏时要隐藏的元素选择器列表 */
hideSelectors?: string[]
@@ -16,6 +19,13 @@ export interface ScreenCaptureOptions {
backgroundColor?: string
/** 是否捕获完整的可滚动内容(默认 true) */
fullContent?: boolean
/**
* 移动端适配宽度,设置后会临时改变元素宽度以适配移动端布局
* - 传入数字:使用指定宽度
* - 传入 true:使用默认值 525px(自动适配,仅当原始宽度超过时才缩放)
* - 传入 false 或不传:不进行移动端适配
*/
mobileWidth?: number | boolean
}
/**
@@ -133,6 +143,9 @@ export function useScreenCapture() {
const originalPadding = element.style.padding
const originalPaddingBottom = element.style.paddingBottom
const originalPosition = element.style.position
const originalWidth = element.style.width
const originalMinWidth = element.style.minWidth
const originalMaxWidth = element.style.maxWidth
element.style.padding = '16px'
element.style.paddingBottom = '48px' // 为水印留出空间
@@ -141,6 +154,28 @@ export function useScreenCapture() {
element.style.position = 'relative'
}
// 移动端宽度适配(渐进式缩放)
let appliedMobileWidth = false
if (options?.mobileWidth) {
const baseWidth = typeof options.mobileWidth === 'number' ? options.mobileWidth : DEFAULT_MOBILE_MAX_WIDTH
// 获取元素当前的实际宽度
const currentWidth = element.getBoundingClientRect().width
// 只有当原始宽度大于基准宽度时才缩放
if (currentWidth > baseWidth) {
// 渐进式缩放:目标宽度 = 基准宽度 + (原始宽度 - 基准宽度) × 缩放因子
// 缩放因子 0.3 表示超出部分保留 30%
const scaleFactor = 0.3
const targetWidth = Math.round(baseWidth + (currentWidth - baseWidth) * scaleFactor)
element.style.width = `${targetWidth}px`
element.style.minWidth = `${targetWidth}px`
element.style.maxWidth = `${targetWidth}px`
appliedMobileWidth = true
}
}
// 添加底部水印标识(绝对定位)
const watermark = document.createElement('div')
watermark.className = '__capture-watermark__'
@@ -148,9 +183,9 @@ export function useScreenCapture() {
position: absolute;
left: 0;
right: 0;
bottom: 8px;
bottom: 16px;
text-align: center;
font-size: 12px;
font-size: 14px;
color: #9ca3af;
`
watermark.textContent = '聊天分析实验室 · chatlab.fun'
@@ -191,7 +226,12 @@ export function useScreenCapture() {
// 如果需要捕获完整内容,临时移除 overflow 限制
const fullContent = options?.fullContent !== false
const overflowElements: { el: HTMLElement; originalOverflow: string; originalHeight: string; originalMaxHeight: string }[] = []
const overflowElements: {
el: HTMLElement
originalOverflow: string
originalHeight: string
originalMaxHeight: string
}[] = []
if (fullContent) {
// 处理目标元素及其所有子元素的 overflow 和 max-height 限制
@@ -367,6 +407,15 @@ export function useScreenCapture() {
}
try {
// 如果应用了移动端宽度,等待 DOM 重新布局
if (appliedMobileWidth) {
await new Promise<void>((resolve) => {
requestAnimationFrame(() => {
requestAnimationFrame(() => resolve())
})
})
}
const imageData = await captureAsImageData(element, {
maxExportWidth: options?.maxExportWidth,
backgroundColor: options?.backgroundColor,
@@ -418,6 +467,9 @@ export function useScreenCapture() {
element.style.padding = originalPadding
element.style.paddingBottom = originalPaddingBottom
element.style.position = originalPosition
element.style.width = originalWidth
element.style.minWidth = originalMinWidth
element.style.maxWidth = originalMaxWidth
// 恢复文本节点的原始内容
for (const { node, originalText } of textNodesBackup) {
+8
View File
@@ -15,6 +15,9 @@ export const useLayoutStore = defineStore(
const showChatRecordDrawer = ref(false)
const chatRecordQuery = ref<ChatRecordQuery | null>(null)
// 截图设置
const screenshotMobileAdapt = ref(true) // 截图时开启移动端适配,默认开启
/**
* 切换侧边栏展开/折叠状态
*/
@@ -65,6 +68,7 @@ export const useLayoutStore = defineStore(
screenCaptureImage,
showChatRecordDrawer,
chatRecordQuery,
screenshotMobileAdapt,
toggleSidebar,
openScreenCaptureModal,
closeScreenCaptureModal,
@@ -78,6 +82,10 @@ export const useLayoutStore = defineStore(
pick: ['isSidebarCollapsed'],
storage: sessionStorage,
},
{
pick: ['screenshotMobileAdapt'],
storage: localStorage,
},
],
}
)