mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-21 05:40:23 +08:00
feat: 截屏功能适配移动端
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user