A:完整虚拟列表加载
This commit is contained in:
1 parent
a9c58b4bb0
commit
e377835c9c
1 file changed
+153
-88
@@ -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() // 重新加载以获取新的总数和数据
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
|
||||
Reference in new issue
Block a user