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 下的仅滚动时显示滚动条的特性,并保证在各个浏览器的表现一致。

![2023-03-26 13 44 04](https://user-images.githubusercontent.com/21301288/227757677-ca2709be-03bc-4bdd-a74c-2e42a452417a.gif)

#### Special notes for your reviewer:

测试方式:使用不同的浏览器测试滚动条是否可以自动隐藏以及样式是否一致即可。

#### Does this PR introduce a user-facing change?

```release-note
优化 Console 端页面滚动条的样式。
```
pull/3568/head
Ryan Wang 2023-03-27 17:20:22 +08:00 committed by GitHub
parent 2b73a56b6c
commit 403702021c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 210 additions and 113 deletions

View File

@ -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",

View File

@ -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;

View File

@ -1,4 +1,5 @@
import "./styles/tailwind.css";
import "overlayscrollbars/overlayscrollbars.css";
export * from "./components";
export * from "./icons/icons";

View File

@ -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'}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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"

View File

@ -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";