feat: 新增 back-to-top
This commit is contained in:
parent
2abdb84ae8
commit
c0c3122592
174
theme/src/client/components/BackToTop.vue
Normal file
174
theme/src/client/components/BackToTop.vue
Normal file
@ -0,0 +1,174 @@
|
||||
<script lang="ts" setup>
|
||||
import { useElementSize, useWindowScroll, useWindowSize } from '@vueuse/core'
|
||||
import { computed, onMounted, ref, shallowRef, watch } from 'vue'
|
||||
import { usePageData } from '@vuepress/client'
|
||||
import type { PlumeThemePageData } from '../../shared/index.js'
|
||||
import IconBackToTop from './icons/IconBackToTop.vue'
|
||||
|
||||
const body = shallowRef<HTMLElement | null>()
|
||||
const { height: bodyHeight } = useElementSize(body)
|
||||
const { height: windowHeight } = useWindowSize()
|
||||
onMounted(() => {
|
||||
body.value = document.body
|
||||
})
|
||||
|
||||
const page = usePageData<PlumeThemePageData>()
|
||||
|
||||
const { y } = useWindowScroll()
|
||||
const isScrolling = ref(false)
|
||||
|
||||
const progress = computed(
|
||||
() => (y.value / (bodyHeight.value - windowHeight.value)) * 100,
|
||||
)
|
||||
const percent = computed(() => `${Math.round(progress.value)}%`)
|
||||
|
||||
const stroke = computed(() =>
|
||||
`calc(${Math.PI * progress.value}% - ${4 * Math.PI}px) calc(${Math.PI * 100}% - ${4 * Math.PI}px)`,
|
||||
)
|
||||
|
||||
const mustHidden = computed(() => {
|
||||
return page.value.frontmatter.backToTop === false || page.value.frontmatter.home
|
||||
})
|
||||
|
||||
const show = computed(() => {
|
||||
if (bodyHeight.value < windowHeight.value)
|
||||
return false
|
||||
|
||||
else
|
||||
return y.value > windowHeight.value / 2
|
||||
})
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
let timer: NodeJS.Timeout | null = null
|
||||
function resetScrolling() {
|
||||
timer && clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
isScrolling.value = false
|
||||
}, 1000)
|
||||
}
|
||||
watch(y, () => {
|
||||
isScrolling.value = true
|
||||
resetScrolling()
|
||||
})
|
||||
|
||||
function handleClick() {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="fade">
|
||||
<button
|
||||
v-show="!mustHidden && (show || isScrolling)"
|
||||
type="button"
|
||||
class="back-to-top-button"
|
||||
aria-label="back to top"
|
||||
@click="handleClick"
|
||||
>
|
||||
<span class="percent" :class="{ show: isScrolling }">{{ percent }}</span>
|
||||
<IconBackToTop class="icon" :class="{ show: !isScrolling }" />
|
||||
<svg aria-hidden="true">
|
||||
<circle cx="50%" cy="50%" :style="{ 'stroke-dasharray': stroke }" />
|
||||
</svg>
|
||||
</button>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.back-to-top-button {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
bottom: 64px;
|
||||
z-index: var(--vp-z-index-back-to-top);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 100%;
|
||||
box-shadow: var(--vp-shadow-2);
|
||||
background-color: var(--vp-c-bg);
|
||||
inset-inline-end: 1rem;
|
||||
transition: background 0.25s ease, color 0.25s ease, box-shadow 0.25s ease;
|
||||
}
|
||||
|
||||
.back-to-top-button .percent,
|
||||
.back-to-top-button .icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.back-to-top-button .percent.show,
|
||||
.back-to-top-button .icon.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.back-to-top-button .percent {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
line-height: 36px;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.back-to-top-button .icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.back-to-top-button svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.back-to-top-button svg circle {
|
||||
fill: none;
|
||||
stroke: var(--vp-c-brand-2);
|
||||
transform: rotate(-90deg);
|
||||
transform-origin: 50% 50%;
|
||||
r: 16;
|
||||
stroke-dasharray: 0% 314.1593%;
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.back-to-top-button {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.back-to-top-button .percent {
|
||||
line-height: 48px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.back-to-top-button .icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.back-to-top-button svg circle {
|
||||
r: 22;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.back-to-top-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
3
theme/src/client/components/icons/IconBackToTop.vue
Normal file
3
theme/src/client/components/icons/IconBackToTop.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="M24.008 14.1V42M12 26l12-12l12 12M12 6h24" /></svg>
|
||||
</template>
|
||||
@ -14,6 +14,7 @@ import Page from '../components/Page.vue'
|
||||
import Sidebar from '../components/Sidebar.vue'
|
||||
import SkipLink from '../components/SkipLink.vue'
|
||||
import VFooter from '../components/VFooter.vue'
|
||||
import BackToTop from '../components/BackToTop.vue'
|
||||
import {
|
||||
useCloseSidebarOnEscape,
|
||||
useSidebar,
|
||||
@ -56,6 +57,7 @@ provide('is-sidebar-open', isSidebarOpen)
|
||||
<Friends v-else-if="page.frontmatter.friends" />
|
||||
<Blog v-else-if="isBlogLayout" />
|
||||
<Page v-else />
|
||||
<BackToTop />
|
||||
<VFooter />
|
||||
</LayoutContent>
|
||||
</div>
|
||||
|
||||
@ -26,6 +26,7 @@ export interface PlumeThemePageFrontmatter {
|
||||
contributors?: boolean
|
||||
prev?: string | NavItemWithLink
|
||||
next?: string | NavItemWithLink
|
||||
backToTop?: boolean
|
||||
}
|
||||
|
||||
export interface PlumeThemePostFrontmatter extends PlumeThemePageFrontmatter {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user