A:完整虚拟列表加载

This commit is contained in:
jimmy committed 2025-07-31 14:50:30 +08:00
1 parent a9c58b4bb0
commit e377835c9c
1 file changed
+153 -88
+153 -88
View File
@@ -169,18 +169,14 @@
<span>暂无数据</span>
</div>
<div v-else class="data-info">
<span>已加载 {{ firewallRule.length }} / {{ totalCount }} 条数据</span>
<n-button v-if="hasMore" size="small" @click="loadMoreData" :loading="loading" style="margin-left: 10px;">
加载更多
</n-button>
<span v-else style="margin-left: 10px; color: #999;">已加载全部数据</span>
<span>总共 {{ totalCount }} 条数据(已加载 {{ loadedRanges.size * pageSize }} 条)</span>
</div>
<!-- 虚拟滚动列表 -->
<RecycleScroller
v-if="!initialLoading && firewallRule.length > 0"
class="rules-scroller"
:items="firewallRule"
:items="recyclerItems"
:item-size="80"
key-field="id"
style="height: 500px;"
@@ -189,7 +185,15 @@
@visible="onVisibleRangeUpdate"
v-slot="{ item }"
>
<div class="rule-item" :class="{ selected: checkedRowKeys.includes(item.id) }">
<!-- 占位符项 -->
<div v-if="item.isPlaceholder" class="rule-item loading-placeholder">
<div class="loading-content">
<span>加载中...</span>
</div>
</div>
<!-- 已加载的数据项 -->
<div v-else class="rule-item" :class="{ selected: checkedRowKeys.includes(item.id) }">
<div class="rule-cell checkbox-cell">
<n-checkbox
:checked="checkedRowKeys.includes(item.id)"
@@ -405,23 +409,48 @@ const totalCount = ref(0)
const pageSize = 50
// 已加载的页索引(防止重复加载)
const loadedPages = new Set<number>()
// 防火墙规则列表 - 只存储已加载的数据
const firewallRule = ref<FirewallRuleShow[]>([])
// 防火墙规则列表 - 预分配所有位置,包含null占位符
const firewallRule = ref<(FirewallRuleShow | null)[]>([])
const loadedRanges = new Set<string>() // 例如:"100-149"
// 过滤掉 null 值的计算属性(现在不需要了,直接使用firewallRule)
const validFirewallRules = computed(() => {
return firewallRule.value
// 为RecycleScroller准备的数据,每个项目都有唯一的id
const recyclerItems = computed(() => {
return firewallRule.value.map((item, index) => {
if (item) {
return item
} else {
// 为null项创建占位符对象,模拟FirewallRuleShow结构
return {
id: `placeholder-${index}`,
name: '',
description: '',
enabled: false,
direction: 'inbound' as const,
action: 'allow' as const,
program: '',
protocol: '',
port: '',
profiles: [] as string[],
isPlaceholder: true as const,
index: index
}
}
})
})
// 数据加载状态
const loading = ref(false)
const initialLoading = ref(true)
// 类型守卫函数
function isPlaceholder(item: any): item is { isPlaceholder: true } {
return item && item.isPlaceholder === true
}
// 当前页和是否还有更多数据
const currentPage = ref(0)
const hasMore = ref(true)
// 数据加载状态
const loading = ref(false)
const initialLoading = ref(true)
// 数据转换函数
function convertFirewallRuleToShow(rule: FirewallRule): FirewallRuleShow {
return {
@@ -516,20 +545,26 @@ function getProtocolValue(protocol: string): number {
}
}
// 加载更多数据
async function loadMoreData() {
if (loading.value || !hasMore.value) {
console.log('跳过加载 - loading:', loading.value, 'hasMore:', hasMore.value)
// 分段加载数据
async function loadRange(startIndex: number, limit = pageSize) {
if (loading.value) {
console.log('已在加载中,跳过请求')
return
}
console.log('开始加载更多数据,当前页:', currentPage.value)
const rangeKey = `${startIndex}-${startIndex + limit - 1}`
if (loadedRanges.has(rangeKey)) {
console.log('范围已加载,跳过:', rangeKey)
return
}
console.log('开始加载数据范围:', rangeKey)
try {
loading.value = true
const params = buildQueryParams({
start: currentPage.value * pageSize,
limit: pageSize,
start: startIndex,
limit: limit,
})
console.log('请求参数:', params)
@@ -541,19 +576,24 @@ async function loadMoreData() {
console.log('收到数据:', res.rules.length, '条,总数:', res.totalCount)
// 更新总数
totalCount.value = res.totalCount
// 确保数组已预分配
if (firewallRule.value.length === 0 && res.totalCount > 0) {
console.log('预分配数组大小:', res.totalCount)
firewallRule.value = Array(res.totalCount).fill(null)
totalCount.value = res.totalCount
}
// 添加新数据到现有数组
const newRules = res.rules.map(rule => convertFirewallRuleToShow(rule))
firewallRule.value.push(...newRules)
console.log('当前已加载数据条数:', firewallRule.value.length)
// 填充数据到指定位置
for (let i = 0; i < res.rules.length; i++) {
const index = startIndex + i
if (index < firewallRule.value.length) {
firewallRule.value[index] = convertFirewallRuleToShow(res.rules[i])
console.log(`填充数据到位置 ${index}:`, firewallRule.value[index]?.name)
}
}
// 更新分页状态
currentPage.value++
hasMore.value = firewallRule.value.length < res.totalCount
console.log('更新状态 - 当前页:', currentPage.value, '还有更多:', hasMore.value)
loadedRanges.add(rangeKey)
console.log('范围加载完成:', rangeKey, '已加载范围数:', loadedRanges.size)
} catch (error) {
console.error('加载数据失败:', error)
message.error('数据加载失败')
@@ -562,49 +602,65 @@ async function loadMoreData() {
}
}
// 重置并加载首页数据
// 重置并预分配数组
async function resetAndLoadFirst() {
try {
console.log('开始重置并加载首页数据')
console.log('开始重置并预分配数组')
initialLoading.value = true
firewallRule.value = []
currentPage.value = 0
hasMore.value = true
loadedRanges.clear()
await loadMoreData()
console.log('首页数据详情:', firewallRule.value.slice(0, 3)) // 显示前3条数据的详情
console.log('数据结构检查:', {
totalCount: totalCount.value,
firewallRuleLength: firewallRule.value.length,
firstItem: firewallRule.value[0],
hasValidIds: firewallRule.value.every(item => item && item.id)
// 先获取总数
const params = buildQueryParams({
start: 0,
limit: 1, // 只获取1条数据来获取总数
})
console.log('首页加载完成,总条数:', totalCount.value, '当前数组长度:', firewallRule.value.length)
console.log('获取总数的请求参数:', params)
const res = await getFirewallRules(params)
if (!res) {
console.log('获取总数失败')
return
}
console.log('获取到总数:', res.totalCount)
totalCount.value = res.totalCount
// 预分配整个数组
firewallRule.value = Array(res.totalCount).fill(null)
console.log('预分配完成,数组长度:', firewallRule.value.length)
// 加载首页数据
await loadRange(0, pageSize)
console.log('首页加载完成')
} catch (error) {
console.error('加载首页数据失败:', error)
console.error('初始化失败:', error)
message.error('数据加载失败')
} finally {
initialLoading.value = false
}
}
/**
* 滚动事件处理 - 检测是否需要加载更多数据
*/
/**
* 滚动事件处理 - 检测是否需要加载更多数据
*/
function onVisibleRangeUpdate(event?: any) {
console.log('可见范围更新触发:', event)
// 检查事件参数格式
if (!event) {
console.log('事件参数为空,跳过处理')
return
}
// 尝试不同的事件参数格式
let startIndex = 0
let endIndex = 0
if (event.startIndex !== undefined && event.endIndex !== undefined) {
// 标准格式
startIndex = event.startIndex
@@ -621,24 +677,21 @@ function onVisibleRangeUpdate(event?: any) {
console.log('无法解析事件参数格式,使用滚动位置检测')
return
}
console.log('解析的可见范围:', { startIndex, endIndex })
// 检查是否接近列表末尾,如果是则加载更多数据
const threshold = 10 // 距离末尾10项时开始加载
const nearEnd = endIndex >= firewallRule.value.length - threshold
console.log('检查是否需要加载更多:', {
endIndex,
currentLength: firewallRule.value.length,
nearEnd,
hasMore: hasMore.value,
loading: loading.value
})
if (nearEnd && hasMore.value && !loading.value) {
console.log('可见范围触发加载更多数据')
loadMoreData()
// 计算需要加载的数据段
const alignedStart = Math.floor(startIndex / pageSize) * pageSize
const alignedEnd = Math.ceil((endIndex + 1) / pageSize) * pageSize
console.log('计算的加载范围:', alignedStart, alignedEnd)
// 加载可见范围内的数据段
for (let i = alignedStart; i < alignedEnd; i += pageSize) {
if (i < totalCount.value) {
console.log('尝试加载范围:', i, 'to', i + pageSize - 1)
loadRange(i, pageSize)
}
}
}
@@ -650,24 +703,39 @@ function onVisibleRangeUpdate(event?: any) {
*/
function onScroll(event: Event) {
console.log('滚动事件触发')
// 尝试手动检测滚动位置
// 尝试基于滚动位置估算可见范围
const target = event.target as HTMLElement
if (target) {
const { scrollTop, scrollHeight, clientHeight } = target
const scrollPercentage = (scrollTop + clientHeight) / scrollHeight
console.log('滚动位置信息:', {
scrollTop,
scrollHeight,
clientHeight,
scrollPercentage: Math.round(scrollPercentage * 100) + '%'
})
// 如果滚动到80%以上,尝试加载更多
if (scrollPercentage > 0.8 && hasMore.value && !loading.value) {
console.log('滚动位置触发加载更多数据')
loadMoreData()
// 估算当前可见的项目范围
const itemHeight = 80 // item-size
const visibleStartIndex = Math.floor(scrollTop / itemHeight)
const visibleEndIndex = Math.ceil((scrollTop + clientHeight) / itemHeight)
console.log('估算的可见范围:', { visibleStartIndex, visibleEndIndex })
// 计算需要加载的数据段
const alignedStart = Math.floor(visibleStartIndex / pageSize) * pageSize
const alignedEnd = Math.ceil((visibleEndIndex + 1) / pageSize) * pageSize
console.log('基于滚动位置的加载范围:', alignedStart, alignedEnd)
// 加载可见范围内的数据段
for (let i = alignedStart; i < alignedEnd; i += pageSize) {
if (i < totalCount.value) {
console.log('滚动位置触发加载范围:', i, 'to', i + pageSize - 1)
loadRange(i, pageSize)
}
}
}
}
@@ -690,12 +758,14 @@ watch([searchQuery, filters], () => {
// 全选相关计算属性
const allSelected = computed({
get: () => {
const validRules = validFirewallRules.value
const validRules = firewallRule.value.filter(rule => rule !== null)
return validRules.length > 0 && checkedRowKeys.value.length === validRules.length
},
set: (value: boolean) => {
if (value) {
checkedRowKeys.value = validFirewallRules.value.map((rule) => rule.id)
checkedRowKeys.value = firewallRule.value
.filter(rule => rule !== null)
.map((rule) => rule!.id)
} else {
checkedRowKeys.value = []
}
@@ -703,7 +773,7 @@ const allSelected = computed({
})
const indeterminate = computed(() => {
const validRules = validFirewallRules.value
const validRules = firewallRule.value.filter(rule => rule !== null)
return checkedRowKeys.value.length > 0 && checkedRowKeys.value.length < validRules.length
})
@@ -723,9 +793,9 @@ const deleteRule = async (id: string) => {
console.log('删除规则:', id)
// 从本地数据中移除(临时方案,应该调用API)
const index = firewallRule.value.findIndex((rule) => rule.id === id)
const index = firewallRule.value.findIndex((rule) => rule?.id === id)
if (index !== -1) {
firewallRule.value.splice(index, 1)
firewallRule.value[index] = null // 设为null而不是删除,保持索引不变
totalCount.value = Math.max(0, totalCount.value - 1)
// 从选中项中移除
checkedRowKeys.value = checkedRowKeys.value.filter((key) => key !== id)
@@ -762,20 +832,15 @@ const handleSaveRule = async (rule: unknown) => {
if (rule.id) {
// 编辑现有规则
const index = firewallRule.value.findIndex((r) => r.id === rule.id)
const index = firewallRule.value.findIndex((r) => r?.id === rule.id)
if (index !== -1) {
firewallRule.value[index] = rule as FirewallRuleShow
message.success('规则更新成功')
}
} else {
// 添加新规则
const newRule = {
...rule,
id: crypto.randomUUID()
} as FirewallRuleShow
firewallRule.value.unshift(newRule)
totalCount.value += 1
message.success('规则创建成功')
// 添加新规则 - 这里需要重新加载数据,因为数组大小固定
message.success('规则创建成功,正在刷新数据...')
resetAndLoadFirst() // 重新加载以获取新的总数和数据
}
// 重置状态