refactor: 优化智能分页 DOM 检测机制并添加密钥名称复制功能

- useSmartPagination 使用 ResizeObserver 替代 nextTick 确保 DOM 稳定后检测
- 添加防抖机制避免频繁触发检测
- 组件卸载时清理 ResizeObserver 资源
- ProviderDetailDrawer 密钥名称支持点击复制
This commit is contained in:
AAEE86
2026-02-09 19:01:14 +08:00
parent 7f7f569148
commit 8e120e2665
2 changed files with 91 additions and 7 deletions

View File

@@ -1,10 +1,11 @@
import { ref, computed, watch, nextTick, type Ref, type ComputedRef } from 'vue' import { ref, computed, watch, nextTick, onUnmounted, type Ref, type ComputedRef } from 'vue'
/** /**
* 智能分页 composable * 智能分页 composable
* *
* 根据列表容器的实际渲染高度自动决定是否分页以及每页条数。 * 根据列表容器的实际渲染高度自动决定是否分页以及每页条数。
* 当列表总高度超过阈值时自动启用分页,低于阈值时恢复全量显示。 * 当列表总高度超过阈值时自动启用分页,低于阈值时恢复全量显示。
* 使用 ResizeObserver 确保 DOM 稳定后再进行检测。
* *
* @param items 响应式数据源(全量列表) * @param items 响应式数据源(全量列表)
* @param listRef 列表容器 DOM 引用 * @param listRef 列表容器 DOM 引用
@@ -19,6 +20,10 @@ export function useSmartPagination<T>(
const itemsPerPage = ref(0) // 0 = 不分页 const itemsPerPage = ref(0) // 0 = 不分页
const cachedAvgItemHeight = ref(0) const cachedAvgItemHeight = ref(0)
let resizeObserver: ResizeObserver | null = null
let pendingDetect = false
let detectDebounceTimer: ReturnType<typeof setTimeout> | null = null
const shouldPaginate = computed(() => { const shouldPaginate = computed(() => {
return itemsPerPage.value > 0 && items.value.length > itemsPerPage.value return itemsPerPage.value > 0 && items.value.length > itemsPerPage.value
}) })
@@ -51,10 +56,13 @@ export function useSmartPagination<T>(
const scrollHeight = el.scrollHeight const scrollHeight = el.scrollHeight
const renderedCount = paginatedItems.value.length const renderedCount = paginatedItems.value.length
if (renderedCount > 0 && scrollHeight > 0) { // 确保 DOM 已渲染且有有效高度
cachedAvgItemHeight.value = scrollHeight / renderedCount if (scrollHeight === 0 || renderedCount === 0) {
return
} }
cachedAvgItemHeight.value = scrollHeight / renderedCount
const estimatedTotalHeight = cachedAvgItemHeight.value * items.value.length const estimatedTotalHeight = cachedAvgItemHeight.value * items.value.length
if (estimatedTotalHeight > maxHeight && cachedAvgItemHeight.value > 0) { if (estimatedTotalHeight > maxHeight && cachedAvgItemHeight.value > 0) {
itemsPerPage.value = Math.max(Math.floor(maxHeight / cachedAvgItemHeight.value), 3) itemsPerPage.value = Math.max(Math.floor(maxHeight / cachedAvgItemHeight.value), 3)
@@ -67,6 +75,47 @@ export function useSmartPagination<T>(
} }
} }
/** 防抖检测,避免频繁触发 */
function debouncedDetect() {
if (detectDebounceTimer) {
clearTimeout(detectDebounceTimer)
}
detectDebounceTimer = setTimeout(() => {
detect()
detectDebounceTimer = null
}, 50)
}
/** 设置 ResizeObserver 监听 DOM 变化 */
function setupResizeObserver() {
cleanupResizeObserver()
const el = listRef.value
if (!el) return
resizeObserver = new ResizeObserver(() => {
// DOM 尺寸变化时重新检测
if (pendingDetect) {
pendingDetect = false
debouncedDetect()
}
})
resizeObserver.observe(el)
}
/** 清理 ResizeObserver */
function cleanupResizeObserver() {
if (resizeObserver) {
resizeObserver.disconnect()
resizeObserver = null
}
if (detectDebounceTimer) {
clearTimeout(detectDebounceTimer)
detectDebounceTimer = null
}
}
/** 重置分页状态(数据源切换时调用) */ /** 重置分页状态(数据源切换时调用) */
function reset() { function reset() {
currentPage.value = 1 currentPage.value = 1
@@ -74,11 +123,42 @@ export function useSmartPagination<T>(
cachedAvgItemHeight.value = 0 cachedAvgItemHeight.value = 0
} }
// 数据源变化时自动重新检测immediate 确保首次挂载时也检测) /** 请求检测(会等待 DOM 稳定后执行) */
function requestDetect() {
pendingDetect = true
// 先尝试立即检测
nextTick(() => {
const el = listRef.value
if (el && el.scrollHeight > 0) {
// DOM 已就绪,直接检测
pendingDetect = false
detect()
}
// 否则等待 ResizeObserver 回调
})
}
// 监听 listRef 变化,设置/更新 ResizeObserver
watch(listRef, (newEl) => {
if (newEl) {
setupResizeObserver()
requestDetect()
} else {
cleanupResizeObserver()
}
}, { immediate: true })
// 数据源变化时重置页码并请求检测
watch(items, () => { watch(items, () => {
currentPage.value = 1 currentPage.value = 1
nextTick(detect) requestDetect()
}, { immediate: true, flush: 'post' }) }, { flush: 'post' })
// 组件卸载时清理
onUnmounted(() => {
cleanupResizeObserver()
})
return { return {
currentPage, currentPage,

View File

@@ -234,7 +234,11 @@
</div> </div>
<div class="flex flex-col min-w-0"> <div class="flex flex-col min-w-0">
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<span class="text-sm font-medium truncate">{{ key.name || '未命名密钥' }}</span> <span
class="text-sm font-medium truncate cursor-pointer hover:text-primary transition-colors"
title="点击复制"
@click.stop="copyToClipboard(key.name || '未命名密钥')"
>{{ key.name || '未命名密钥' }}</span>
<!-- OAuth 订阅类型标签 (Codex) --> <!-- OAuth 订阅类型标签 (Codex) -->
<Badge <Badge
v-if="key.oauth_plan_type" v-if="key.oauth_plan_type"