mirror of https://github.com/halo-dev/halo
perf: improve the style of the scrollbar (#3587)
#### What type of PR is this? /kind improvement /area console /milestone 2.4.x #### What this PR does / why we need it: 优化页面滚动条的样式,引入 [OverlayScrollbars](https://github.com/KingSora/OverlayScrollbars) 实现类似于 macOS 下的仅滚动时显示滚动条的特性,并保证在各个浏览器的表现一致。  #### Special notes for your reviewer: 测试方式:使用不同的浏览器测试滚动条是否可以自动隐藏以及样式是否一致即可。 #### Does this PR introduce a user-facing change? ```release-note 优化 Console 端页面滚动条的样式。 ```pull/3568/head
parent
2b73a56b6c
commit
403702021c
|
@ -73,6 +73,8 @@
|
|||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"lodash.sortby": "^4.7.0",
|
||||
"overlayscrollbars": "^2.1.0",
|
||||
"overlayscrollbars-vue": "^0.5.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pinia": "^2.0.26",
|
||||
"pretty-bytes": "^6.0.0",
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, nextTick, ref, watch } from "vue";
|
||||
import { computed, nextTick, reactive, ref, watch } from "vue";
|
||||
import { IconClose } from "../../icons/icons";
|
||||
import type { UseOverlayScrollbarsParams } from "overlayscrollbars-vue";
|
||||
import { useOverlayScrollbars } from "overlayscrollbars-vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -77,6 +79,29 @@ watch(
|
|||
}
|
||||
}
|
||||
);
|
||||
|
||||
// body scroll
|
||||
const modalBody = ref(null);
|
||||
const reactiveParams = reactive<UseOverlayScrollbarsParams>({
|
||||
options: {
|
||||
scrollbars: {
|
||||
autoHide: "scroll",
|
||||
autoHideDelay: 600,
|
||||
},
|
||||
},
|
||||
defer: true,
|
||||
});
|
||||
const [initialize, instance] = useOverlayScrollbars(reactiveParams);
|
||||
watch(
|
||||
() => props.visible,
|
||||
(value) => {
|
||||
if (value) {
|
||||
if (modalBody.value) initialize({ target: modalBody.value });
|
||||
} else {
|
||||
instance()?.destroy();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<Teleport :disabled="!mountToBody" to="body">
|
||||
|
@ -134,7 +159,7 @@ watch(
|
|||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
<div :class="bodyClass" class="modal-body">
|
||||
<div ref="modalBody" :class="bodyClass" class="modal-body">
|
||||
<slot />
|
||||
</div>
|
||||
<div v-if="$slots.footer" class="modal-footer">
|
||||
|
@ -222,7 +247,7 @@ watch(
|
|||
}
|
||||
|
||||
.modal-body {
|
||||
@apply overflow-y-auto
|
||||
@apply overflow-y-hidden
|
||||
overflow-x-hidden
|
||||
flex-1;
|
||||
word-wrap: break-word;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import "./styles/tailwind.css";
|
||||
import "overlayscrollbars/overlayscrollbars.css";
|
||||
|
||||
export * from "./components";
|
||||
export * from "./icons/icons";
|
||||
|
|
|
@ -75,6 +75,8 @@ importers:
|
|||
lodash.isequal: ^4.5.0
|
||||
lodash.merge: ^4.6.2
|
||||
lodash.sortby: ^4.7.0
|
||||
overlayscrollbars: ^2.1.0
|
||||
overlayscrollbars-vue: ^0.5.1
|
||||
path-browserify: ^1.0.1
|
||||
pinia: ^2.0.26
|
||||
postcss: ^8.4.19
|
||||
|
@ -145,6 +147,8 @@ importers:
|
|||
lodash.isequal: 4.5.0
|
||||
lodash.merge: 4.6.2
|
||||
lodash.sortby: 4.7.0
|
||||
overlayscrollbars: 2.1.0
|
||||
overlayscrollbars-vue: 0.5.1_qvtrscepjgkfzkdt53swvamjn4
|
||||
path-browserify: 1.0.1
|
||||
pinia: 2.0.26_e7lp6ggkpgyi5vqd44m2kxvk6i
|
||||
pretty-bytes: 6.0.0
|
||||
|
@ -8671,6 +8675,20 @@ packages:
|
|||
resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==}
|
||||
dev: true
|
||||
|
||||
/overlayscrollbars-vue/0.5.1_qvtrscepjgkfzkdt53swvamjn4:
|
||||
resolution: {integrity: sha512-hYoS7qm1xq4Kv7GpCTpo1YSUmgSXF2z9OcWbwGloGO8ZXefhADwDlYy6e35r2g5Gh04slZPsyQIqEXA/CptFMA==}
|
||||
peerDependencies:
|
||||
overlayscrollbars: ^2.0.0
|
||||
vue: ^3.2.25
|
||||
dependencies:
|
||||
overlayscrollbars: 2.1.0
|
||||
vue: 3.2.45
|
||||
dev: false
|
||||
|
||||
/overlayscrollbars/2.1.0:
|
||||
resolution: {integrity: sha512-L6p4o4aWse5pDstRnJjZaos+al+bkuAgzGIlWwlsxRSgW6+7Kvrp+kAzlWoTZ1bgB4CJj+8u5bjdq8XHEhWjrw==}
|
||||
dev: false
|
||||
|
||||
/p-filter/2.1.0:
|
||||
resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
<script lang="ts" setup>
|
||||
import { RouterView, useRoute } from "vue-router";
|
||||
import { computed, watch, ref } from "vue";
|
||||
import { computed, watch, ref, reactive, onMounted } from "vue";
|
||||
import { useTitle } from "@vueuse/core";
|
||||
import { useFavicon } from "@vueuse/core";
|
||||
import { useSystemConfigMapStore } from "./stores/system-configmap";
|
||||
import { storeToRefs } from "pinia";
|
||||
import axios from "axios";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
useOverlayScrollbars,
|
||||
type UseOverlayScrollbarsParams,
|
||||
} from "overlayscrollbars-vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
@ -61,6 +65,22 @@ const favicon = computed(() => {
|
|||
});
|
||||
|
||||
useFavicon(favicon);
|
||||
|
||||
// body scroll
|
||||
const body = document.querySelector("body");
|
||||
const reactiveParams = reactive<UseOverlayScrollbarsParams>({
|
||||
options: {
|
||||
scrollbars: {
|
||||
autoHide: "scroll",
|
||||
autoHideDelay: 600,
|
||||
},
|
||||
},
|
||||
defer: true,
|
||||
});
|
||||
const [initialize] = useOverlayScrollbars(reactiveParams);
|
||||
onMounted(() => {
|
||||
if (body) initialize({ target: body });
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -69,7 +89,6 @@ useFavicon(favicon);
|
|||
|
||||
<style lang="scss">
|
||||
body {
|
||||
overflow-y: overlay;
|
||||
background: #eff4f9;
|
||||
}
|
||||
|
||||
|
@ -79,27 +98,11 @@ body {
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track-piece {
|
||||
background-color: #f8f8f8;
|
||||
-webkit-border-radius: 2em;
|
||||
-moz-border-radius: 2em;
|
||||
border-radius: 2em;
|
||||
.v-popper__popper {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: #ddd;
|
||||
background-clip: padding-box;
|
||||
-webkit-border-radius: 2em;
|
||||
-moz-border-radius: 2em;
|
||||
border-radius: 2em;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #bbb;
|
||||
.v-popper--theme-tooltip {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -111,6 +111,7 @@ import type { queueAsPromised } from "fastq";
|
|||
import type { Attachment } from "@halo-dev/api-client";
|
||||
import { useFetchAttachmentPolicy } from "@/modules/contents/attachments/composables/use-attachment-policy";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
@ -548,7 +549,12 @@ watch(
|
|||
}"
|
||||
>
|
||||
<template #extra>
|
||||
<div class="h-full w-72 overflow-y-auto border-l bg-white">
|
||||
<OverlayScrollbarsComponent
|
||||
element="div"
|
||||
:options="{ scrollbars: { autoHide: 'scroll' } }"
|
||||
class="h-full w-72 border-l bg-white"
|
||||
defer
|
||||
>
|
||||
<VTabs v-model:active-id="extraActiveId" type="outline">
|
||||
<VTabItem
|
||||
id="toc"
|
||||
|
@ -733,7 +739,7 @@ watch(
|
|||
</div>
|
||||
</VTabItem>
|
||||
</VTabs>
|
||||
</div>
|
||||
</OverlayScrollbarsComponent>
|
||||
</template>
|
||||
</RichTextEditor>
|
||||
</template>
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
useRouter,
|
||||
type RouteRecordRaw,
|
||||
} from "vue-router";
|
||||
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import { nextTick, onMounted, onUnmounted, reactive, ref, watch } from "vue";
|
||||
import axios from "axios";
|
||||
import GlobalSearchModal from "@/components/global-search/GlobalSearchModal.vue";
|
||||
import LoginModal from "@/components/login/LoginModal.vue";
|
||||
|
@ -31,6 +31,10 @@ import { rbacAnnotations } from "@/constants/annotations";
|
|||
import { useScroll } from "@vueuse/core";
|
||||
import { defineStore, storeToRefs } from "pinia";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
useOverlayScrollbars,
|
||||
type UseOverlayScrollbarsParams,
|
||||
} from "overlayscrollbars-vue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
@ -209,6 +213,23 @@ onMounted(() => {
|
|||
y.value = navbarScrollStore.y;
|
||||
});
|
||||
});
|
||||
|
||||
// aside scroll
|
||||
const reactiveParams = reactive<UseOverlayScrollbarsParams>({
|
||||
options: {
|
||||
scrollbars: {
|
||||
autoHide: "scroll",
|
||||
autoHideDelay: 600,
|
||||
},
|
||||
},
|
||||
defer: true,
|
||||
});
|
||||
const [initialize] = useOverlayScrollbars(reactiveParams);
|
||||
onMounted(() => {
|
||||
if (navbarScroller.value) {
|
||||
initialize({ target: navbarScroller.value });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -227,7 +248,7 @@ onMounted(() => {
|
|||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div ref="navbarScroller" class="flex-1 overflow-y-auto">
|
||||
<div ref="navbarScroller" class="flex-1 overflow-y-hidden">
|
||||
<div class="px-3">
|
||||
<div
|
||||
class="flex cursor-pointer items-center rounded bg-gray-100 p-2 text-gray-400 transition-all hover:text-gray-900"
|
||||
|
@ -297,7 +318,7 @@ onMounted(() => {
|
|||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="content w-full overflow-y-auto pb-12 mb-safe md:pb-0">
|
||||
<main class="content w-full pb-12 mb-safe md:pb-0">
|
||||
<slot v-if="$slots.default" />
|
||||
<RouterView v-else />
|
||||
</main>
|
||||
|
|
|
@ -11,6 +11,7 @@ import { apiClient } from "@/utils/api-client";
|
|||
import { formatDatetime } from "@/utils/date";
|
||||
import { postLabels } from "@/constants/labels";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||||
|
||||
const { data } = useQuery<ListedPost[]>({
|
||||
queryKey: ["widget-recent-posts"],
|
||||
|
@ -32,65 +33,72 @@ const { data } = useQuery<ListedPost[]>({
|
|||
</script>
|
||||
<template>
|
||||
<VCard
|
||||
:body-class="['h-full', '!p-0', 'overflow-y-auto']"
|
||||
:body-class="['h-full', '!p-0', '!overflow-auto']"
|
||||
class="h-full"
|
||||
:title="$t('core.dashboard.widgets.presets.recent_published.title')"
|
||||
>
|
||||
<ul class="box-border h-full w-full divide-y divide-gray-100" role="list">
|
||||
<li v-for="(post, index) in data" :key="index">
|
||||
<VEntity>
|
||||
<template #start>
|
||||
<VEntityField
|
||||
:title="post.post.spec.title"
|
||||
:route="{
|
||||
name: 'PostEditor',
|
||||
query: { name: post.post.metadata.name },
|
||||
}"
|
||||
>
|
||||
<template #description>
|
||||
<VSpace>
|
||||
<span class="text-xs text-gray-500">
|
||||
{{
|
||||
$t(
|
||||
"core.dashboard.widgets.presets.recent_published.visits",
|
||||
{ visits: post.stats.visit || 0 }
|
||||
)
|
||||
}}
|
||||
<OverlayScrollbarsComponent
|
||||
element="div"
|
||||
:options="{ scrollbars: { autoHide: 'scroll' } }"
|
||||
class="h-full w-full"
|
||||
defer
|
||||
>
|
||||
<ul class="box-border h-full w-full divide-y divide-gray-100" role="list">
|
||||
<li v-for="(post, index) in data" :key="index">
|
||||
<VEntity>
|
||||
<template #start>
|
||||
<VEntityField
|
||||
:title="post.post.spec.title"
|
||||
:route="{
|
||||
name: 'PostEditor',
|
||||
query: { name: post.post.metadata.name },
|
||||
}"
|
||||
>
|
||||
<template #description>
|
||||
<VSpace>
|
||||
<span class="text-xs text-gray-500">
|
||||
{{
|
||||
$t(
|
||||
"core.dashboard.widgets.presets.recent_published.visits",
|
||||
{ visits: post.stats.visit || 0 }
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">
|
||||
{{
|
||||
$t(
|
||||
"core.dashboard.widgets.presets.recent_published.comments",
|
||||
{ comments: post.stats.totalComment || 0 }
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</VSpace>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a
|
||||
v-if="post.post.status?.permalink"
|
||||
target="_blank"
|
||||
:href="post.post.status?.permalink"
|
||||
:title="post.post.status?.permalink"
|
||||
class="hidden text-gray-600 transition-all hover:text-gray-900 group-hover:inline-block"
|
||||
>
|
||||
<IconExternalLinkLine class="h-3.5 w-3.5" />
|
||||
</a>
|
||||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
<template #end>
|
||||
<VEntityField>
|
||||
<template #description>
|
||||
<span class="truncate text-xs tabular-nums text-gray-500">
|
||||
{{ formatDatetime(post.post.spec.publishTime) }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">
|
||||
{{
|
||||
$t(
|
||||
"core.dashboard.widgets.presets.recent_published.comments",
|
||||
{ comments: post.stats.totalComment || 0 }
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</VSpace>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a
|
||||
v-if="post.post.status?.permalink"
|
||||
target="_blank"
|
||||
:href="post.post.status?.permalink"
|
||||
:title="post.post.status?.permalink"
|
||||
class="hidden text-gray-600 transition-all hover:text-gray-900 group-hover:inline-block"
|
||||
>
|
||||
<IconExternalLinkLine class="h-3.5 w-3.5" />
|
||||
</a>
|
||||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
<template #end>
|
||||
<VEntityField>
|
||||
<template #description>
|
||||
<span class="truncate text-xs tabular-nums text-gray-500">
|
||||
{{ formatDatetime(post.post.spec.publishTime) }}
|
||||
</span>
|
||||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
</VEntity>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
</VEntity>
|
||||
</li>
|
||||
</ul>
|
||||
</OverlayScrollbarsComponent>
|
||||
</VCard>
|
||||
</template>
|
||||
|
|
|
@ -19,6 +19,7 @@ import { useRouter } from "vue-router";
|
|||
import ThemePreviewModal from "@/modules/interface/themes/components/preview/ThemePreviewModal.vue";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||||
|
||||
interface Action {
|
||||
icon: Component;
|
||||
|
@ -159,40 +160,48 @@ const actions: Action[] = [
|
|||
</script>
|
||||
<template>
|
||||
<VCard
|
||||
:body-class="['h-full', 'overflow-y-auto', '@container']"
|
||||
:body-class="['h-full', '@container', '!p-0', '!overflow-auto']"
|
||||
class="h-full"
|
||||
:title="$t('core.dashboard.widgets.presets.quicklink.title')"
|
||||
>
|
||||
<div
|
||||
class="grid grid-cols-1 gap-2 overflow-hidden @sm:grid-cols-2 @md:grid-cols-3"
|
||||
<OverlayScrollbarsComponent
|
||||
element="div"
|
||||
:options="{ scrollbars: { autoHide: 'scroll' } }"
|
||||
class="h-full w-full"
|
||||
style="padding: 12px 16px"
|
||||
defer
|
||||
>
|
||||
<div
|
||||
v-for="(action, index) in actions"
|
||||
:key="index"
|
||||
v-permission="action.permissions"
|
||||
class="group relative cursor-pointer rounded-lg bg-gray-50 p-4 transition-all hover:bg-gray-100"
|
||||
@click="action.action"
|
||||
class="grid grid-cols-1 gap-2 overflow-hidden @sm:grid-cols-2 @md:grid-cols-3"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
v-for="(action, index) in actions"
|
||||
:key="index"
|
||||
v-permission="action.permissions"
|
||||
class="group relative cursor-pointer rounded-lg bg-gray-50 p-4 transition-all hover:bg-gray-100"
|
||||
@click="action.action"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
class="inline-flex rounded-lg bg-teal-50 p-3 text-teal-700 ring-4 ring-white"
|
||||
>
|
||||
<component :is="action.icon"></component>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-8">
|
||||
<h3 class="text-sm font-semibold">
|
||||
{{ action.title }}
|
||||
</h3>
|
||||
</div>
|
||||
<span
|
||||
class="inline-flex rounded-lg bg-teal-50 p-3 text-teal-700 ring-4 ring-white"
|
||||
aria-hidden="true"
|
||||
class="pointer-events-none absolute top-6 right-6 text-gray-300 transition-all group-hover:translate-x-1 group-hover:text-gray-400"
|
||||
>
|
||||
<component :is="action.icon"></component>
|
||||
<IconArrowRight />
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-8">
|
||||
<h3 class="text-sm font-semibold">
|
||||
{{ action.title }}
|
||||
</h3>
|
||||
</div>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="pointer-events-none absolute top-6 right-6 text-gray-300 transition-all group-hover:translate-x-1 group-hover:text-gray-400"
|
||||
>
|
||||
<IconArrowRight />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</OverlayScrollbarsComponent>
|
||||
</VCard>
|
||||
<ThemePreviewModal
|
||||
v-model:visible="themePreviewVisible"
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
import { storeToRefs } from "pinia";
|
||||
import { computed, markRaw, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
|
||||
const props = withDefaults(
|
||||
|
@ -315,11 +316,13 @@ const iframeClasses = computed(() => {
|
|||
leave-to-class="-translate-x-full"
|
||||
appear
|
||||
>
|
||||
<div
|
||||
<OverlayScrollbarsComponent
|
||||
v-if="themesVisible || settingsVisible"
|
||||
class="relative h-full w-96 overflow-y-auto"
|
||||
element="div"
|
||||
:options="{ scrollbars: { autoHide: 'scroll' } }"
|
||||
class="relative h-full w-96"
|
||||
:class="{ '!overflow-hidden': switching }"
|
||||
style="overflow-y: overlay"
|
||||
defer
|
||||
>
|
||||
<transition
|
||||
enter-active-class="transform transition ease-in-out duration-300 delay-150"
|
||||
|
@ -432,7 +435,7 @@ const iframeClasses = computed(() => {
|
|||
</VButton>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</OverlayScrollbarsComponent>
|
||||
</transition>
|
||||
<div
|
||||
class="flex h-full flex-1 items-center justify-center transition-all duration-300"
|
||||
|
|
|
@ -2,3 +2,4 @@ import "@halo-dev/richtext-editor/dist/style.css";
|
|||
import "@halo-dev/components/dist/style.css";
|
||||
import "@/styles/tailwind.css";
|
||||
import "@/styles/index.css";
|
||||
import "overlayscrollbars/overlayscrollbars.css";
|
||||
|
|
Loading…
Reference in New Issue