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
*
* 根据列表容器的实际渲染高度自动决定是否分页以及每页条数。
* 当列表总高度超过阈值时自动启用分页,低于阈值时恢复全量显示。
* 使用 ResizeObserver 确保 DOM 稳定后再进行检测。
*
* @param items 响应式数据源(全量列表)
* @param listRef 列表容器 DOM 引用
@@ -19,6 +20,10 @@ export function useSmartPagination<T>(
const itemsPerPage = ref(0) // 0 = 不分页
const cachedAvgItemHeight = ref(0)
let resizeObserver: ResizeObserver | null = null
let pendingDetect = false
let detectDebounceTimer: ReturnType<typeof setTimeout> | null = null
const shouldPaginate = computed(() => {
return itemsPerPage.value > 0 && items.value.length > itemsPerPage.value
})
@@ -51,10 +56,13 @@ export function useSmartPagination<T>(
const scrollHeight = el.scrollHeight
const renderedCount = paginatedItems.value.length
if (renderedCount > 0 && scrollHeight > 0) {
cachedAvgItemHeight.value = scrollHeight / renderedCount
// 确保 DOM 已渲染且有有效高度
if (scrollHeight === 0 || renderedCount === 0) {
return
}
cachedAvgItemHeight.value = scrollHeight / renderedCount
const estimatedTotalHeight = cachedAvgItemHeight.value * items.value.length
if (estimatedTotalHeight > maxHeight && cachedAvgItemHeight.value > 0) {
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() {
currentPage.value = 1
@@ -74,11 +123,42 @@ export function useSmartPagination<T>(
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, () => {
currentPage.value = 1
nextTick(detect)
}, { immediate: true, flush: 'post' })
requestDetect()
}, { flush: 'post' })
// 组件卸载时清理
onUnmounted(() => {
cleanupResizeObserver()
})
return {
currentPage,

View File

@@ -234,7 +234,11 @@
</div>
<div class="flex flex-col min-w-0">
<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) -->
<Badge
v-if="key.oauth_plan_type"