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