mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-23 10:58:13 +08:00
180 lines
3.8 KiB
Vue
180 lines
3.8 KiB
Vue
<script lang="ts" setup>
|
|
import { useElementSize, useWindowScroll, useWindowSize } from '@vueuse/core'
|
|
import { computed, onMounted, ref, shallowRef, watch } from 'vue'
|
|
import { useData } from '../composables/index.js'
|
|
|
|
const body = shallowRef<HTMLElement | null>()
|
|
const { height: bodyHeight } = useElementSize(body)
|
|
const { height: windowHeight } = useWindowSize()
|
|
onMounted(() => {
|
|
body.value = document.body
|
|
})
|
|
|
|
const { page } = useData()
|
|
|
|
const { y } = useWindowScroll()
|
|
const isScrolling = ref(false)
|
|
|
|
const progress = computed(
|
|
() => (y.value / (bodyHeight.value - windowHeight.value)) * 100,
|
|
)
|
|
|
|
// #72 back to top percentage issue
|
|
const percent = computed(() => `${Math.min(Math.round(progress.value), 100) || 0}%`)
|
|
|
|
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.pageLayout === 'home' && page.value.frontmatter.config && (page.value.frontmatter.config as any).length <= 1)
|
|
})
|
|
|
|
const show = computed(() => {
|
|
if (bodyHeight.value < windowHeight.value)
|
|
return false
|
|
|
|
else
|
|
return y.value > windowHeight.value / 2
|
|
})
|
|
|
|
let timer: NodeJS.Timeout | null = null
|
|
function resetScrolling() {
|
|
if (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="vp-back-to-top"
|
|
aria-label="back to top"
|
|
@click="handleClick"
|
|
>
|
|
<span class="percent" :class="{ show: isScrolling }" data-allow-mismatch>{{ percent }}</span>
|
|
<span class="icon vpi-back-to-top" :class="{ show: !isScrolling }" />
|
|
<svg aria-hidden="true">
|
|
<circle cx="50%" cy="50%" data-allow-mismatch :style="{ 'stroke-dasharray': stroke }" />
|
|
</svg>
|
|
</button>
|
|
</Transition>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.vp-back-to-top {
|
|
position: fixed;
|
|
inset-inline-end: 1rem;
|
|
right: 24px;
|
|
bottom: calc(var(--vp-footer-height, 82px) - 18px);
|
|
z-index: var(--vp-z-index-back-to-top);
|
|
width: 36px;
|
|
height: 36px;
|
|
background-color: var(--vp-c-bg);
|
|
border-radius: 100%;
|
|
box-shadow: var(--vp-shadow-2);
|
|
transition:
|
|
background-color var(--vp-t-color),
|
|
box-shadow var(--vp-t-color);
|
|
}
|
|
|
|
.vp-back-to-top .percent,
|
|
.vp-back-to-top .icon {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
opacity: 0;
|
|
transition: opacity 0.5s ease, color var(--vp-t-color);
|
|
}
|
|
|
|
.vp-back-to-top .percent.show,
|
|
.vp-back-to-top .icon.show {
|
|
opacity: 1;
|
|
}
|
|
|
|
.vp-back-to-top .percent {
|
|
width: 100%;
|
|
height: 100%;
|
|
font-size: 10px;
|
|
line-height: 36px;
|
|
text-align: center;
|
|
user-select: none;
|
|
}
|
|
|
|
.vp-back-to-top .icon {
|
|
top: 50%;
|
|
left: 50%;
|
|
width: 18px;
|
|
height: 18px;
|
|
color: var(--vp-c-text-3);
|
|
transform: translate(-50%, -50%);
|
|
}
|
|
|
|
.vp-back-to-top svg {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.vp-back-to-top svg circle {
|
|
fill: none;
|
|
r: 16px;
|
|
stroke: var(--vp-c-brand-2);
|
|
stroke-dasharray: 0% 314.1593%;
|
|
stroke-width: 4px;
|
|
transform: rotate(-90deg);
|
|
transform-origin: 50% 50%;
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.vp-back-to-top {
|
|
bottom: calc(var(--vp-footer-height, 88px) - 24px);
|
|
width: 48px;
|
|
height: 48px;
|
|
}
|
|
|
|
.vp-back-to-top .percent {
|
|
font-size: 14px;
|
|
line-height: 48px;
|
|
}
|
|
|
|
.vp-back-to-top .icon {
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
|
|
.vp-back-to-top svg circle {
|
|
r: 22px;
|
|
}
|
|
}
|
|
|
|
.fade-enter-active,
|
|
.fade-leave-active {
|
|
transition: opacity 0.5s ease;
|
|
}
|
|
|
|
.fade-enter-from,
|
|
.fade-leave-to {
|
|
opacity: 0;
|
|
}
|
|
|
|
@media print {
|
|
.vp-back-to-top {
|
|
display: none;
|
|
}
|
|
}
|
|
</style>
|