feat: BodyScrollbar

This commit is contained in:
二刺螈 2024-11-26 14:43:23 +08:00
parent b125088e27
commit a899d73227
2 changed files with 209 additions and 0 deletions

View File

@ -0,0 +1,184 @@
<script setup lang="ts">
import {
useElementBounding,
useEventListener,
useWindowScroll,
useWindowSize,
useStyleTag,
} from '@vueuse/core';
import { computed, onMounted, shallowRef, type CSSProperties } from 'vue';
useStyleTag(
`
body::-webkit-scrollbar {
display: none;
}
html {
scrollbar-width: none;
}
`.trim(),
);
const { y, x } = useWindowScroll();
const { height: winH, width: winW } = useWindowSize();
const bodyRef = shallowRef<HTMLElement>(); // support ssr
onMounted(() => {
bodyRef.value = document.body;
});
const body = useElementBounding(bodyRef);
const yShow = computed(() => body.height.value > winH.value);
const yHeight = computed(() => {
const clientHeight = body.height.value;
const bodyHeight = clientHeight;
return (winH.value / bodyHeight) * winH.value;
});
const translateY = computed(() => {
const clientHeight = body.height.value;
const height = yHeight.value;
return (y.value / (clientHeight - winH.value)) * (winH.value - height);
});
const yStyle = computed<CSSProperties>(() => {
if (!yShow.value) return {};
return {
transform: `translateY(${translateY.value}px)`,
height: `${yHeight.value}px`,
};
});
const clickBoxY = async (e: MouseEvent) => {
const deltaY =
yHeight.value *
0.9 *
(e.clientY < yHeight.value + translateY.value ? -1 : 1);
const clientHeight = body.height.value;
const bodyHeight = clientHeight;
const height = (winH.value / bodyHeight) * winH.value;
y.value += (deltaY / (winH.value - height)) * (clientHeight - winH.value);
};
const yDragging = shallowRef(false);
let lastYEvent: MouseEvent | undefined = undefined;
const pointerdownY = (e: MouseEvent) => {
lastYEvent = e;
yDragging.value = true;
};
useEventListener('pointermove', (e) => {
if (!lastYEvent) return;
const deltaY = e.clientY - lastYEvent.clientY;
lastYEvent = e;
const clientHeight = body.height.value;
const bodyHeight = clientHeight;
const height = (winH.value / bodyHeight) * winH.value;
y.value += (deltaY / (winH.value - height)) * (clientHeight - winH.value);
});
useEventListener('pointerup', () => {
lastYEvent = undefined;
yDragging.value = false;
});
const xShow = computed(() => body.width.value > winW.value);
const xWidth = computed(() => {
const clientWidth = body.width.value;
const bodyWidth = clientWidth;
return (winW.value / bodyWidth) * winW.value;
});
const translateX = computed(() => {
const clientWidth = body.width.value;
const width = xWidth.value;
return (x.value / (clientWidth - winW.value)) * (winW.value - width);
});
const xStyle = computed<CSSProperties>(() => {
if (!xShow.value) return {};
return {
transform: `translateX(${translateX.value}px)`,
width: `${xWidth.value}px`,
};
});
const clickBoxX = (e: MouseEvent) => {
const deltaX =
xWidth.value * 0.9 * (e.clientX < xWidth.value + translateX.value ? -1 : 1);
const clientWidth = body.width.value;
const bodyWidth = clientWidth;
const width = (winW.value / bodyWidth) * winW.value;
const newX =
x.value + (deltaX / (winW.value - width)) * (clientWidth - winW.value);
x.value = newX;
};
const xDragging = shallowRef(false);
let lastXEvent: MouseEvent | undefined = undefined;
const pointerdownX = (e: MouseEvent) => {
lastXEvent = e;
xDragging.value = true;
};
useEventListener('pointermove', (e) => {
if (!lastXEvent) return;
const deltaX = e.clientX - lastXEvent.clientX;
lastXEvent = e;
const clientWidth = body.width.value;
const bodyWidth = clientWidth;
const width = (winW.value / bodyWidth) * winW.value;
x.value += (deltaX / (winW.value - width)) * (clientWidth - winW.value);
});
useEventListener('pointerup', () => {
lastXEvent = undefined;
xDragging.value = false;
});
useEventListener('selectstart', (e) => {
if (lastXEvent || lastYEvent) {
e.preventDefault();
}
});
</script>
<template>
<div fixed class="BodyScrollbar">
<div
v-show="yShow"
scrollbar-y
fixed
right-2px
top-0
bottom-0
z-100
w-8px
@pointerdown="pointerdownY"
@click="clickBoxY"
>
<div
@click.stop
w-full
bg="#909399"
opacity-30
hover:opacity="50"
:style="yStyle"
:class="{
'opacity-50': yDragging,
}"
></div>
</div>
<div
v-if="xShow"
scrollbar-x
fixed
bottom-2px
left-0
right-0
z-100
h-8px
@pointerdown="pointerdownX"
@click="clickBoxX"
>
<div
@click.stop
h-full
bg="#909399"
opacity-30
hover:opacity="50"
:style="xStyle"
:class="{
'opacity-50': xDragging,
}"
></div>
</div>
</div>
</template>

View File

@ -2,7 +2,16 @@ import 'uno.css';
import type { Theme } from 'vitepress'; import type { Theme } from 'vitepress';
import DefaultTheme from 'vitepress/theme'; import DefaultTheme from 'vitepress/theme';
import components from '../components'; import components from '../components';
import BodyScrollbar from '../components/BodyScrollbar.vue';
import './custom.css'; import './custom.css';
import {
Fragment,
h,
Teleport,
defineComponent,
shallowRef,
onMounted,
} from 'vue';
// 兼容旧链接/短链重定向 // 兼容旧链接/短链重定向
if (!import.meta.env.SSR) { if (!import.meta.env.SSR) {
@ -31,8 +40,24 @@ if (!import.meta.env.SSR) {
} }
} }
const ScrollbarWrapper = defineComponent(() => {
const show = shallowRef(false);
onMounted(() => {
const isMobile = 'ontouchstart' in document.documentElement;
show.value = !isMobile;
});
return () => {
return show.value
? h(Teleport, { to: document.body }, h(BodyScrollbar))
: undefined;
};
});
export default { export default {
extends: DefaultTheme, extends: DefaultTheme,
Layout() {
return h(Fragment, null, [h(DefaultTheme.Layout), h(ScrollbarWrapper)]);
},
enhanceApp({ app }) { enhanceApp({ app }) {
Object.entries(components).forEach(([name, component]) => { Object.entries(components).forEach(([name, component]) => {
app.component(name, component); app.component(name, component);