A:触底加载
This commit is contained in:
1 parent
628b8e4d8c
commit
a9c58b4bb0
1 file changed
+190
-61
@@ -161,13 +161,32 @@
|
|||||||
<div class="header-cell actions-cell">操作</div>
|
<div class="header-cell actions-cell">操作</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据调试信息 -->
|
||||||
|
<div v-if="initialLoading" class="loading-info">
|
||||||
|
<span>正在加载数据...</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="firewallRule.length === 0" class="no-data">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 虚拟滚动列表 -->
|
<!-- 虚拟滚动列表 -->
|
||||||
<RecycleScroller
|
<RecycleScroller
|
||||||
|
v-if="!initialLoading && firewallRule.length > 0"
|
||||||
class="rules-scroller"
|
class="rules-scroller"
|
||||||
:items="validFirewallRules"
|
:items="firewallRule"
|
||||||
:item-size="80"
|
:item-size="80"
|
||||||
:key-field="'id'"
|
key-field="id"
|
||||||
@update="onVisibleRangeUpdate"
|
style="height: 500px;"
|
||||||
|
@scroll="onScroll"
|
||||||
|
@resize="onResize"
|
||||||
|
@visible="onVisibleRangeUpdate"
|
||||||
v-slot="{ item }"
|
v-slot="{ item }"
|
||||||
>
|
>
|
||||||
<div class="rule-item" :class="{ selected: checkedRowKeys.includes(item.id) }">
|
<div class="rule-item" :class="{ selected: checkedRowKeys.includes(item.id) }">
|
||||||
@@ -386,19 +405,23 @@ const totalCount = ref(0)
|
|||||||
const pageSize = 50
|
const pageSize = 50
|
||||||
// 已加载的页索引(防止重复加载)
|
// 已加载的页索引(防止重复加载)
|
||||||
const loadedPages = new Set<number>()
|
const loadedPages = new Set<number>()
|
||||||
// 防火墙规则列表
|
// 防火墙规则列表 - 只存储已加载的数据
|
||||||
const firewallRule = ref<(FirewallRuleShow | null)[]>([])
|
const firewallRule = ref<FirewallRuleShow[]>([])
|
||||||
const loadedRanges = new Set<string>() // 例如:"100-149"
|
const loadedRanges = new Set<string>() // 例如:"100-149"
|
||||||
|
|
||||||
// 过滤掉 null 值的计算属性
|
// 过滤掉 null 值的计算属性(现在不需要了,直接使用firewallRule)
|
||||||
const validFirewallRules = computed(() => {
|
const validFirewallRules = computed(() => {
|
||||||
return firewallRule.value.filter((rule): rule is FirewallRuleShow => rule !== null)
|
return firewallRule.value
|
||||||
})
|
})
|
||||||
|
|
||||||
// 数据加载状态
|
// 数据加载状态
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const initialLoading = ref(true)
|
const initialLoading = ref(true)
|
||||||
|
|
||||||
|
// 当前页和是否还有更多数据
|
||||||
|
const currentPage = ref(0)
|
||||||
|
const hasMore = ref(true)
|
||||||
|
|
||||||
// 数据转换函数
|
// 数据转换函数
|
||||||
function convertFirewallRuleToShow(rule: FirewallRule): FirewallRuleShow {
|
function convertFirewallRuleToShow(rule: FirewallRule): FirewallRuleShow {
|
||||||
return {
|
return {
|
||||||
@@ -493,40 +516,44 @@ function getProtocolValue(protocol: string): number {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分段加载数据
|
// 加载更多数据
|
||||||
async function loadRange(startIndex: number, limit = pageSize) {
|
async function loadMoreData() {
|
||||||
if (loading.value) return
|
if (loading.value || !hasMore.value) {
|
||||||
|
console.log('跳过加载 - loading:', loading.value, 'hasMore:', hasMore.value)
|
||||||
const rangeKey = `${startIndex}-${startIndex + limit - 1}`
|
return
|
||||||
if (loadedRanges.has(rangeKey)) return
|
}
|
||||||
|
|
||||||
|
console.log('开始加载更多数据,当前页:', currentPage.value)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const params = buildQueryParams({
|
const params = buildQueryParams({
|
||||||
start: startIndex,
|
start: currentPage.value * pageSize,
|
||||||
limit: limit,
|
limit: pageSize,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log('请求参数:', params)
|
||||||
const res = await getFirewallRules(params)
|
const res = await getFirewallRules(params)
|
||||||
if (!res) return
|
if (!res) {
|
||||||
|
console.log('请求返回空结果')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('收到数据:', res.rules.length, '条,总数:', res.totalCount)
|
||||||
|
|
||||||
// 更新总数
|
// 更新总数
|
||||||
totalCount.value = res.totalCount
|
totalCount.value = res.totalCount
|
||||||
|
|
||||||
// 确保数组足够大
|
|
||||||
if (firewallRule.value.length < res.totalCount) {
|
|
||||||
firewallRule.value = Array(res.totalCount).fill(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 填充数据
|
// 添加新数据到现有数组
|
||||||
for (let i = 0; i < res.rules.length; i++) {
|
const newRules = res.rules.map(rule => convertFirewallRuleToShow(rule))
|
||||||
const index = startIndex + i
|
firewallRule.value.push(...newRules)
|
||||||
if (index < firewallRule.value.length) {
|
console.log('当前已加载数据条数:', firewallRule.value.length)
|
||||||
firewallRule.value[index] = convertFirewallRuleToShow(res.rules[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadedRanges.add(rangeKey)
|
// 更新分页状态
|
||||||
|
currentPage.value++
|
||||||
|
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('数据加载失败')
|
||||||
@@ -538,27 +565,22 @@ async function loadRange(startIndex: number, limit = pageSize) {
|
|||||||
// 重置并加载首页数据
|
// 重置并加载首页数据
|
||||||
async function resetAndLoadFirst() {
|
async function resetAndLoadFirst() {
|
||||||
try {
|
try {
|
||||||
|
console.log('开始重置并加载首页数据')
|
||||||
initialLoading.value = true
|
initialLoading.value = true
|
||||||
loadedRanges.clear()
|
|
||||||
firewallRule.value = []
|
firewallRule.value = []
|
||||||
|
currentPage.value = 0
|
||||||
const params = buildQueryParams({
|
hasMore.value = true
|
||||||
start: 0,
|
|
||||||
limit: pageSize,
|
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 res = await getFirewallRules(params)
|
|
||||||
if (!res) return
|
|
||||||
|
|
||||||
totalCount.value = res.totalCount
|
console.log('首页加载完成,总条数:', totalCount.value, '当前数组长度:', firewallRule.value.length)
|
||||||
firewallRule.value = Array(res.totalCount).fill(null)
|
|
||||||
|
|
||||||
// 填充首页数据
|
|
||||||
for (let i = 0; i < res.rules.length; i++) {
|
|
||||||
firewallRule.value[i] = convertFirewallRuleToShow(res.rules[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
loadedRanges.add(`0-${pageSize - 1}`)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载首页数据失败:', error)
|
console.error('加载首页数据失败:', error)
|
||||||
message.error('数据加载失败')
|
message.error('数据加载失败')
|
||||||
@@ -568,22 +590,95 @@ async function resetAndLoadFirst() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 滚动事件
|
* 滚动事件处理 - 检测是否需要加载更多数据
|
||||||
* @param startIndex
|
|
||||||
* @param endIndex
|
|
||||||
*/
|
*/
|
||||||
function onVisibleRangeUpdate({ startIndex, endIndex }) {
|
function onVisibleRangeUpdate(event?: any) {
|
||||||
console.log('可见范围:', startIndex, endIndex)
|
console.log('可见范围更新触发:', event)
|
||||||
const alignedStart = Math.floor(startIndex / pageSize) * pageSize
|
|
||||||
const alignedEnd = Math.ceil((endIndex + 1) / pageSize) * pageSize
|
// 检查事件参数格式
|
||||||
|
if (!event) {
|
||||||
|
console.log('事件参数为空,跳过处理')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试不同的事件参数格式
|
||||||
|
let startIndex = 0
|
||||||
|
let endIndex = 0
|
||||||
|
|
||||||
|
if (event.startIndex !== undefined && event.endIndex !== undefined) {
|
||||||
|
// 标准格式
|
||||||
|
startIndex = event.startIndex
|
||||||
|
endIndex = event.endIndex
|
||||||
|
} else if (event.start !== undefined && event.end !== undefined) {
|
||||||
|
// 另一种可能的格式
|
||||||
|
startIndex = event.start
|
||||||
|
endIndex = event.end
|
||||||
|
} else if (typeof event === 'object' && event.length) {
|
||||||
|
// 如果是数组格式
|
||||||
|
startIndex = event[0] || 0
|
||||||
|
endIndex = event[1] || firewallRule.value.length - 1
|
||||||
|
} else {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = alignedStart; i < alignedEnd; i += pageSize) {
|
/**
|
||||||
if (i < totalCount.value) {
|
* 普通滚动事件处理
|
||||||
loadRange(i, pageSize)
|
*/
|
||||||
|
/**
|
||||||
|
* 普通滚动事件处理
|
||||||
|
*/
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调整大小事件处理
|
||||||
|
*/
|
||||||
|
function onResize() {
|
||||||
|
console.log('RecycleScroller调整大小')
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化数据
|
// 初始化数据
|
||||||
resetAndLoadFirst()
|
resetAndLoadFirst()
|
||||||
|
|
||||||
@@ -626,11 +721,12 @@ const editRule = (rule: unknown) => {
|
|||||||
const deleteRule = async (id: string) => {
|
const deleteRule = async (id: string) => {
|
||||||
// TODO: 实现删除规则API调用
|
// TODO: 实现删除规则API调用
|
||||||
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.splice(index, 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)
|
||||||
message.success('规则删除成功')
|
message.success('规则删除成功')
|
||||||
@@ -663,10 +759,10 @@ const clearFilters = () => {
|
|||||||
const handleSaveRule = async (rule: unknown) => {
|
const handleSaveRule = async (rule: unknown) => {
|
||||||
// TODO: 实现保存规则API调用
|
// TODO: 实现保存规则API调用
|
||||||
console.log('保存规则:', rule)
|
console.log('保存规则:', rule)
|
||||||
|
|
||||||
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('规则更新成功')
|
||||||
@@ -1569,6 +1665,39 @@ const handleSaveRule = async (rule: unknown) => {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 调试信息样式 */
|
||||||
|
.loading-info, .no-data, .data-info {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-info {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载占位符样式 */
|
||||||
|
.loading-placeholder {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
/* vue-virtual-scroller 样式 */
|
/* vue-virtual-scroller 样式 */
|
||||||
:deep(.vue-recycle-scroller) {
|
:deep(.vue-recycle-scroller) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
Reference in new issue
Block a user