perf(frontend): 收敛导航预取并去重首屏请求

- 导航预取仅保留 pointerdown 触发,移除 mouseenter/focus,避免鼠标划过误触发
- 后台预取只做组件懒加载,不再预取各页业务数据,减少首屏资源争抢
- 版本状态检查增加 sessionStorage 缓存(正常 20 分钟 / 错误 5 分钟 TTL)
- fetchModules、必读公告拉取增加请求去重,避免并发重复请求
- 更新检查改用可清理的定时器,组件卸载时清理
- UsageRecordsTable 搜索防抖改为自定义实现,卸载时取消挂起 emit 并补充测试
This commit is contained in:
elky
2026-07-01 20:42:52 +08:00
parent a0f7074e59
commit 2e5ff32e1a
6 changed files with 158 additions and 110 deletions
@@ -1057,8 +1057,8 @@
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useDebounceFn, useLocalStorage } from '@vueuse/core'
import { ref, computed, onBeforeUnmount, watch } from 'vue'
import { useLocalStorage } from '@vueuse/core'
import {
TableCard,
Badge,
@@ -1355,10 +1355,26 @@ const timeRangeModel = computed({
})
// 通用搜索(输入防抖)
const SEARCH_EMIT_DEBOUNCE_MS = 300
const localSearch = ref(props.filterSearch)
const emitSearchDebounced = useDebounceFn((value: string) => {
emit('update:filterSearch', value)
}, 300)
let searchEmitTimer: ReturnType<typeof setTimeout> | null = null
function cancelPendingSearchEmit() {
if (searchEmitTimer !== null) {
clearTimeout(searchEmitTimer)
searchEmitTimer = null
}
}
function scheduleSearchEmit(value: string) {
cancelPendingSearchEmit()
searchEmitTimer = setTimeout(() => {
searchEmitTimer = null
if (value !== props.filterSearch) {
emit('update:filterSearch', value)
}
}, SEARCH_EMIT_DEBOUNCE_MS)
}
function getDisplayStatus(record: UsageRecord) {
return resolveDisplayRequestStatus(record)
@@ -1422,12 +1438,18 @@ function formatRecordProviderSegment(record: UsageRecord): string {
watch(() => props.filterSearch, (value) => {
if (value !== localSearch.value) {
cancelPendingSearchEmit()
localSearch.value = value
}
})
watch(localSearch, (value) => {
emitSearchDebounced(value)
if (value === props.filterSearch) return
scheduleSearchEmit(value)
})
onBeforeUnmount(() => {
cancelPendingSearchEmit()
})
// 使用复用的行点击逻辑
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { createApp, defineComponent, h, type App } from 'vue'
import { createApp, defineComponent, h, nextTick, type App } from 'vue'
import UsageRecordsTable from '../UsageRecordsTable.vue'
import type { UsageRecord } from '../../types'
@@ -167,6 +167,7 @@ afterEach(() => {
app.unmount()
root.remove()
}
vi.useRealTimers()
})
describe('UsageRecordsTable', () => {
@@ -314,6 +315,31 @@ describe('UsageRecordsTable', () => {
expect(onUpdateHideUnknownRecords).toHaveBeenCalledWith(true)
})
it('debounces usage search updates', async () => {
vi.useFakeTimers()
const onUpdateFilterSearch = vi.fn()
const root = mountUsageRecordsTable([buildRecord()], {
'onUpdate:filterSearch': onUpdateFilterSearch,
})
const input = root.querySelector<HTMLInputElement>('#usage-records-search')
expect(input).not.toBeNull()
input!.value = 'a'
input!.dispatchEvent(new Event('input'))
input!.value = 'ab'
input!.dispatchEvent(new Event('input'))
input!.value = 'abc'
input!.dispatchEvent(new Event('input'))
await nextTick()
await vi.advanceTimersByTimeAsync(299)
expect(onUpdateFilterSearch).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(1)
expect(onUpdateFilterSearch).toHaveBeenCalledTimes(1)
expect(onUpdateFilterSearch).toHaveBeenCalledWith('abc')
})
it('shows retry and fallback markers together when both flags are set', () => {
const root = mountUsageRecordsTable([buildRecord({
has_fallback: true,