前端页面添加BackToTop图标

This commit is contained in:
jingrow 2025-11-02 02:57:48 +08:00
parent 7132875265
commit 84e263d4eb
2 changed files with 210 additions and 0 deletions

View File

@ -37,6 +37,9 @@
class="mobile-overlay" class="mobile-overlay"
@click="toggleSidebar" @click="toggleSidebar"
></div> ></div>
<!-- 返回顶部按钮 -->
<BackToTop />
</n-layout> </n-layout>
</template> </template>
@ -45,6 +48,7 @@ import { ref, watch, onMounted, onUnmounted } from 'vue'
import { NLayout, NLayoutSider, NLayoutHeader, NLayoutContent } from 'naive-ui' import { NLayout, NLayoutSider, NLayoutHeader, NLayoutContent } from 'naive-ui'
import AppSidebar from './AppSidebar.vue' import AppSidebar from './AppSidebar.vue'
import AppHeader from './AppHeader.vue' import AppHeader from './AppHeader.vue'
import BackToTop from './BackToTop.vue'
const SIDEBAR_COLLAPSE_KEY = 'app.sidebar.collapsed' const SIDEBAR_COLLAPSE_KEY = 'app.sidebar.collapsed'
const collapsed = ref(localStorage.getItem(SIDEBAR_COLLAPSE_KEY) === 'true') const collapsed = ref(localStorage.getItem(SIDEBAR_COLLAPSE_KEY) === 'true')

View File

@ -0,0 +1,206 @@
<template>
<transition name="back-to-top-fade">
<n-button
v-if="visible"
circle
type="primary"
size="large"
class="back-to-top-btn"
@click="scrollToTop"
:title="t('Back to top')"
>
<template #icon>
<Icon icon="tabler:arrow-up" :width="20" :height="20" />
</template>
</n-button>
</transition>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import { NButton } from 'naive-ui'
import { Icon } from '@iconify/vue'
import { t } from '@/shared/i18n'
// /
const visible = ref(false)
//
const SCROLL_THRESHOLD = 300
//
const registeredContainers = ref<(Element | Window)[]>([])
//
const findScrollContainers = (): (Element | Window)[] => {
const containers: (Element | Window)[] = [window]
// .content-wrapper
const contentWrapper = document.querySelector('.content-wrapper')
if (contentWrapper) {
containers.push(contentWrapper)
}
//
const scrollableElements = document.querySelectorAll('[class*="scroll"], [class*="content"], [class*="wrapper"]')
scrollableElements.forEach(el => {
const style = window.getComputedStyle(el)
if (style.overflowY === 'auto' || style.overflowY === 'scroll' ||
style.overflow === 'auto' || style.overflow === 'scroll') {
if (!containers.includes(el)) {
containers.push(el)
}
}
})
console.log('找到的所有滚动容器:', containers.map(c =>
c === window ? 'window' : (c as Element).className || (c as Element).tagName
))
return containers
}
//
const getScrollTop = (container: Element | Window): number => {
if (container === window) {
return window.pageYOffset || document.documentElement.scrollTop || 0
} else {
return (container as Element).scrollTop || 0
}
}
//
const scrollToTop = () => {
const containers = registeredContainers.value.length > 0
? registeredContainers.value
: findScrollContainers()
console.log('滚动容器前状态:', containers.map(c => ({
container: c === window ? 'window' : (c as Element).className || (c as Element).tagName,
scrollTop: getScrollTop(c)
})))
//
containers.forEach(container => {
if (container === window) {
window.scrollTo({
top: 0,
behavior: 'smooth'
})
} else {
const el = container as Element
el.scrollTo({
top: 0,
behavior: 'smooth'
})
}
})
console.log('执行了滚动操作')
}
//
const handleScroll = () => {
// 使
const containers = registeredContainers.value.length > 0
? registeredContainers.value
: findScrollContainers()
if (containers.length === 0) {
return
}
//
let maxScrollTop = 0
containers.forEach(container => {
const scrollTop = getScrollTop(container)
if (scrollTop > maxScrollTop) {
maxScrollTop = scrollTop
}
})
//
visible.value = maxScrollTop > SCROLL_THRESHOLD
//
if (maxScrollTop > 0) {
console.log('滚动检测:', {
maxScrollTop,
visible: visible.value,
containers: containers.map(c => ({
container: c === window ? 'window' : (c as Element).className || (c as Element).tagName,
scrollTop: getScrollTop(c)
}))
})
}
}
onMounted(() => {
// DOM
nextTick(() => {
const containers = findScrollContainers()
//
registeredContainers.value = containers
//
containers.forEach(container => {
container.addEventListener('scroll', handleScroll, { passive: true })
})
//
handleScroll()
})
})
onUnmounted(() => {
//
registeredContainers.value.forEach(container => {
container.removeEventListener('scroll', handleScroll)
})
registeredContainers.value = []
})
</script>
<style scoped>
.back-to-top-btn {
position: fixed;
right: 24px;
bottom: 24px;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.back-to-top-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}
/* 移动端调整位置 */
@media (max-width: 768px) {
.back-to-top-btn {
right: 16px;
bottom: 16px;
width: 48px;
height: 48px;
}
}
/* 淡入淡出动画 */
.back-to-top-fade-enter-active,
.back-to-top-fade-leave-active {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.back-to-top-fade-enter-from,
.back-to-top-fade-leave-to {
opacity: 0;
transform: scale(0.8) translateY(10px);
}
.back-to-top-fade-enter-to,
.back-to-top-fade-leave-from {
opacity: 1;
transform: scale(1) translateY(0);
}
</style>