mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
refactor: 优化智能分页 DOM 检测机制并添加密钥名称复制功能
- useSmartPagination 使用 ResizeObserver 替代 nextTick 确保 DOM 稳定后检测 - 添加防抖机制避免频繁触发检测 - 组件卸载时清理 ResizeObserver 资源 - ProviderDetailDrawer 密钥名称支持点击复制
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user