Merge pull request #315 from mt21625457/main
perf(前端): 优化页面加载性能和用户体验 和 修复静态 import 导致入口文件膨胀问题
This commit is contained in:
109
frontend/src/components/common/NavigationProgress.vue
Normal file
109
frontend/src/components/common/NavigationProgress.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 导航进度条组件
|
||||
* 在页面顶部显示加载进度,提供导航反馈
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
import { useNavigationLoadingState } from '@/composables/useNavigationLoading'
|
||||
|
||||
const { isLoading } = useNavigationLoadingState()
|
||||
|
||||
// 进度条可见性
|
||||
const isVisible = computed(() => isLoading.value)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="progress-fade">
|
||||
<div
|
||||
v-show="isVisible"
|
||||
class="navigation-progress"
|
||||
role="progressbar"
|
||||
aria-label="Loading"
|
||||
aria-valuenow="0"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
>
|
||||
<div class="navigation-progress-bar" />
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.navigation-progress {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
z-index: 9999;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.navigation-progress-bar {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
theme('colors.primary.400') 20%,
|
||||
theme('colors.primary.500') 50%,
|
||||
theme('colors.primary.400') 80%,
|
||||
transparent 100%
|
||||
);
|
||||
animation: progress-slide 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 暗色模式下的进度条颜色 */
|
||||
:root.dark .navigation-progress-bar {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
theme('colors.primary.500') 20%,
|
||||
theme('colors.primary.400') 50%,
|
||||
theme('colors.primary.500') 80%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* 进度条滑动动画 */
|
||||
@keyframes progress-slide {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
/* 淡入淡出过渡 */
|
||||
.progress-fade-enter-active {
|
||||
transition: opacity 0.15s ease-out;
|
||||
}
|
||||
|
||||
.progress-fade-leave-active {
|
||||
transition: opacity 0.3s ease-out;
|
||||
}
|
||||
|
||||
.progress-fade-enter-from,
|
||||
.progress-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 减少动画模式 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.navigation-progress-bar {
|
||||
animation: progress-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes progress-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* NavigationProgress 组件单元测试
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { ref } from 'vue'
|
||||
import NavigationProgress from '../../common/NavigationProgress.vue'
|
||||
|
||||
// Mock useNavigationLoadingState
|
||||
const mockIsLoading = ref(false)
|
||||
|
||||
vi.mock('@/composables/useNavigationLoading', () => ({
|
||||
useNavigationLoadingState: () => ({
|
||||
isLoading: mockIsLoading
|
||||
})
|
||||
}))
|
||||
|
||||
describe('NavigationProgress', () => {
|
||||
beforeEach(() => {
|
||||
mockIsLoading.value = false
|
||||
})
|
||||
|
||||
it('isLoading=false 时进度条应该隐藏', () => {
|
||||
mockIsLoading.value = false
|
||||
const wrapper = mount(NavigationProgress)
|
||||
|
||||
const progressBar = wrapper.find('.navigation-progress')
|
||||
// v-show 会设置 display: none
|
||||
expect(progressBar.isVisible()).toBe(false)
|
||||
})
|
||||
|
||||
it('isLoading=true 时进度条应该可见', async () => {
|
||||
mockIsLoading.value = true
|
||||
const wrapper = mount(NavigationProgress)
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const progressBar = wrapper.find('.navigation-progress')
|
||||
expect(progressBar.exists()).toBe(true)
|
||||
expect(progressBar.isVisible()).toBe(true)
|
||||
})
|
||||
|
||||
it('应该有正确的 ARIA 属性', () => {
|
||||
mockIsLoading.value = true
|
||||
const wrapper = mount(NavigationProgress)
|
||||
|
||||
const progressBar = wrapper.find('.navigation-progress')
|
||||
expect(progressBar.attributes('role')).toBe('progressbar')
|
||||
expect(progressBar.attributes('aria-label')).toBe('Loading')
|
||||
expect(progressBar.attributes('aria-valuemin')).toBe('0')
|
||||
expect(progressBar.attributes('aria-valuemax')).toBe('100')
|
||||
})
|
||||
|
||||
it('进度条应该有动画 class', () => {
|
||||
mockIsLoading.value = true
|
||||
const wrapper = mount(NavigationProgress)
|
||||
|
||||
const bar = wrapper.find('.navigation-progress-bar')
|
||||
expect(bar.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('应该正确响应 isLoading 状态变化', async () => {
|
||||
// 测试初始状态为 false
|
||||
mockIsLoading.value = false
|
||||
const wrapper = mount(NavigationProgress)
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// 初始状态隐藏
|
||||
expect(wrapper.find('.navigation-progress').isVisible()).toBe(false)
|
||||
|
||||
// 卸载后重新挂载以测试 true 状态
|
||||
wrapper.unmount()
|
||||
|
||||
// 改变为 true 后重新挂载
|
||||
mockIsLoading.value = true
|
||||
const wrapper2 = mount(NavigationProgress)
|
||||
await wrapper2.vm.$nextTick()
|
||||
expect(wrapper2.find('.navigation-progress').isVisible()).toBe(true)
|
||||
|
||||
// 清理
|
||||
wrapper2.unmount()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user