优化页面滚动事件

This commit is contained in:
icssoa
2025-08-11 23:48:02 +08:00
parent f2621341a4
commit 407c7b0521
8 changed files with 119 additions and 74 deletions

View File

@@ -1,8 +1,12 @@
import { scroller } from "./scroller";
import { initTheme, setH5 } from "./theme"; import { initTheme, setH5 } from "./theme";
import { initLocale } from "@/locale"; import { initLocale } from "@/locale";
export function cool(app: VueApp) { export function cool(app: VueApp) {
app.mixin({ app.mixin({
onPageScroll(e) {
scroller.emit(e.scrollTop);
},
onShow() { onShow() {
// #ifdef H5 // #ifdef H5
setTimeout(() => { setTimeout(() => {
@@ -18,11 +22,12 @@ export function cool(app: VueApp) {
console.log(app); console.log(app);
} }
export * from "./utils";
export * from "./theme";
export * from "./router";
export * from "./service";
export * from "./hooks";
export * from "./ctx"; export * from "./ctx";
export * from "./hooks";
export * from "./router";
export * from "./scroller";
export * from "./service";
export * from "./store"; export * from "./store";
export * from "./theme";
export * from "./upload"; export * from "./upload";
export * from "./utils";

23
cool/scroller/index.ts Normal file
View File

@@ -0,0 +1,23 @@
import { router } from "../router";
class Scroller {
list: Map<string, ((top: number) => void)[]> = new Map();
// 触发滚动
emit(top: number) {
const cbs = this.list.get(router.path()) ?? [];
cbs.forEach((cb) => {
cb(top);
});
}
// 监听页面滚动
on(callback: (top: number) => void) {
const path = router.path();
const cbs = this.list.get(path) ?? [];
cbs.push(callback);
this.list.set(path, cbs);
}
}
export const scroller = new Scroller();

View File

@@ -17,6 +17,9 @@
:pt="{ :pt="{
indexBar: { indexBar: {
className: '!fixed' className: '!fixed'
},
itemHover: {
className: 'bg-gray-200'
} }
}" }"
> >
@@ -29,12 +32,16 @@
<script lang="ts" setup> <script lang="ts" setup>
import { request } from "@/cool"; import { request } from "@/cool";
import DemoItem from "../components/item.uvue"; import DemoItem from "../components/item.uvue";
import { useListView, type ClListViewItem } from "@/uni_modules/cool-ui"; import { useListView, useUi, type ClListViewItem } from "@/uni_modules/cool-ui";
import { ref } from "vue"; import { ref } from "vue";
const ui = useUi();
const data = ref<ClListViewItem[]>([]); const data = ref<ClListViewItem[]>([]);
onReady(() => { onReady(() => {
ui.showLoading();
request<UTSJSONObject[]>({ request<UTSJSONObject[]>({
url: "https://unix.cool-js.com/data/pca_flat.json" url: "https://unix.cool-js.com/data/pca_flat.json"
}) })
@@ -43,6 +50,9 @@ onReady(() => {
}) })
.catch((err) => { .catch((err) => {
console.error(err); console.error(err);
})
.finally(() => {
ui.hideLoading();
}); });
}); });
</script> </script>

View File

@@ -12,7 +12,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { getTabBarHeight, hasCustomTabBar } from "@/cool"; import { getTabBarHeight, hasCustomTabBar, scroller } from "@/cool";
import { computed, onMounted, ref, watch, type PropType } from "vue"; import { computed, onMounted, ref, watch, type PropType } from "vue";
import { usePage } from "../../hooks"; import { usePage } from "../../hooks";
@@ -27,10 +27,12 @@ const props = defineProps({
} }
}); });
const emit = defineEmits(["backTop"]);
const { screenHeight } = uni.getWindowInfo(); const { screenHeight } = uni.getWindowInfo();
// cl-page 上下文 // cl-page 上下文
const page = usePage(); const { scrollToTop, onScroll } = usePage();
// 是否显示回到顶部按钮 // 是否显示回到顶部按钮
const visible = ref(false); const visible = ref(false);
@@ -46,6 +48,9 @@ const bottom = computed(() => {
return h + "px"; return h + "px";
}); });
// 是否页面滚动
const isPage = computed(() => props.top == null);
// 控制是否显示 // 控制是否显示
function onVisible(top: number) { function onVisible(top: number) {
visible.value = top > screenHeight - 100; visible.value = top > screenHeight - 100;
@@ -53,11 +58,20 @@ function onVisible(top: number) {
// 回到顶部 // 回到顶部
function toTop() { function toTop() {
page.scrollToTop(); if (isPage.value) {
scrollToTop();
}
emit("backTop");
} }
onMounted(() => { onMounted(() => {
if (props.top != null) { if (isPage.value) {
// 监听页面滚动
onScroll((top) => {
onVisible(top);
});
} else {
// 监听参数变化 // 监听参数变化
watch( watch(
computed(() => props.top!), computed(() => props.top!),
@@ -68,22 +82,16 @@ onMounted(() => {
immediate: true immediate: true
} }
); );
} else {
// 监听页面滚动
page.onPageScroll((top) => {
onVisible(top);
});
} }
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.cl-back-top { .cl-back-top {
@apply flex flex-row items-center justify-center bg-primary-500 rounded-full; @apply flex flex-row items-center justify-center bg-primary-500 rounded-full duration-300;
width: 40px; width: 40px;
height: 40px; height: 40px;
transition-property: transform; transition-property: transform;
transition-duration: 0.3s;
transform: translateX(160rpx); transform: translateX(160rpx);
&.is-show { &.is-show {
@@ -91,8 +99,7 @@ onMounted(() => {
} }
&-wrapper { &-wrapper {
@apply fixed z-50 overflow-visible; @apply fixed right-0 z-50 overflow-visible;
right: 0;
} }
} }
</style> </style>

View File

@@ -1,16 +1,6 @@
<template> <template>
<view class="cl-list-view" :class="[pt.className]"> <view class="cl-list-view" :class="[pt.className]">
<cl-index-bar <!-- 滚动容器 -->
v-if="hasIndex"
v-model="activeIndex"
:list="indexList"
:pt="{
className: parseClass([pt.indexBar?.className])
}"
@change="onIndexChange"
>
</cl-index-bar>
<scroll-view <scroll-view
class="cl-list-view__scroller" class="cl-list-view__scroller"
:class="[pt.scroller?.className]" :class="[pt.scroller?.className]"
@@ -33,6 +23,7 @@
@refresherrestore="onRefresherRestore" @refresherrestore="onRefresherRestore"
@refresherabort="onRefresherAbort" @refresherabort="onRefresherAbort"
> >
<!-- 下拉刷新 -->
<view <view
slot="refresher" slot="refresher"
class="cl-list-view__refresher" class="cl-list-view__refresher"
@@ -59,15 +50,18 @@
</slot> </slot>
</view> </view>
<!-- 列表 -->
<view <view
class="cl-list-view__virtual-list" class="cl-list-view__virtual-list"
:class="[pt.list?.className]" :class="[pt.list?.className]"
:style="listStyle" :style="listStyle"
> >
<!-- 顶部占位 -->
<view class="cl-list-view__spacer-top" :style="spacerTopStyle"> <view class="cl-list-view__spacer-top" :style="spacerTopStyle">
<slot name="top"></slot> <slot name="top"></slot>
</view> </view>
<!-- 列表项 -->
<view <view
v-for="(item, index) in visibleItems" v-for="(item, index) in visibleItems"
:key="item.key" :key="item.key"
@@ -99,6 +93,7 @@
}, },
pt.item?.className pt.item?.className
]" ]"
:hover-class="pt.itemHover?.className"
:style="{ :style="{
height: virtual ? itemHeight + 'px' : 'auto' height: virtual ? itemHeight + 'px' : 'auto'
}" }"
@@ -118,15 +113,29 @@
</view> </view>
</view> </view>
<!-- 底部占位 -->
<view class="cl-list-view__spacer-bottom" :style="spacerBottomStyle"> <view class="cl-list-view__spacer-bottom" :style="spacerBottomStyle">
<slot name="bottom"></slot> <slot name="bottom"></slot>
</view> </view>
</view> </view>
<!-- 空状态 -->
<cl-empty v-if="noData" :fixed="false"></cl-empty> <cl-empty v-if="noData" :fixed="false"></cl-empty>
<cl-back-top :top="scrollTop" v-if="showBackTop" @tap="scrollToTop"></cl-back-top>
</scroll-view> </scroll-view>
<!-- 右侧索引栏 -->
<cl-index-bar
v-if="hasIndex"
v-model="activeIndex"
:list="indexList"
:pt="{
className: parseClass([pt.indexBar?.className])
}"
@change="onIndexChange"
>
</cl-index-bar>
<!-- 索引提示 -->
<view <view
class="cl-list-view__index" class="cl-list-view__index"
:class="[ :class="[
@@ -141,6 +150,9 @@
<cl-text> {{ indexList[activeIndex] }} </cl-text> <cl-text> {{ indexList[activeIndex] }} </cl-text>
</slot> </slot>
</view> </view>
<!-- 回到顶部 -->
<cl-back-top :top="scrollTop" v-if="showBackTop" @back-top="scrollToTop"></cl-back-top>
</view> </view>
</template> </template>
@@ -295,6 +307,7 @@ const { proxy } = getCurrentInstance()!;
type PassThrough = { type PassThrough = {
className?: string; className?: string;
item?: PassThroughProps; item?: PassThroughProps;
itemHover?: PassThroughProps;
list?: PassThroughProps; list?: PassThroughProps;
indexBar?: PassThroughProps; indexBar?: PassThroughProps;
scroller?: PassThroughProps; scroller?: PassThroughProps;

View File

@@ -6,7 +6,7 @@
:scroll-with-animation="true" :scroll-with-animation="true"
@scroll="onScroll" @scroll="onScroll"
> >
<cl-back-top v-if="backTop" @tap="scrollToTop"></cl-back-top> <cl-back-top v-if="backTop"></cl-back-top>
<theme></theme> <theme></theme>
<ui></ui> <ui></ui>
<slot></slot> <slot></slot>
@@ -14,7 +14,7 @@
<!-- #endif --> <!-- #endif -->
<!-- #ifndef APP --> <!-- #ifndef APP -->
<cl-back-top v-if="backTop" @tap="scrollToTop"></cl-back-top> <cl-back-top v-if="backTop"></cl-back-top>
<theme></theme> <theme></theme>
<ui></ui> <ui></ui>
<slot></slot> <slot></slot>
@@ -27,7 +27,7 @@ import Theme from "./theme.uvue";
import Ui from "./ui.uvue"; import Ui from "./ui.uvue";
import { locale, t } from "@/locale"; import { locale, t } from "@/locale";
import { config } from "@/config"; import { config } from "@/config";
import { router } from "@/cool"; import { router, scroller } from "@/cool";
defineOptions({ defineOptions({
name: "cl-page" name: "cl-page"
@@ -49,12 +49,13 @@ const scrollViewTop = ref(0);
// view 滚动事件 // view 滚动事件
function onScroll(e: UniScrollEvent) { function onScroll(e: UniScrollEvent) {
scrollTop.value = e.detail.scrollTop; // 触发滚动事件
scroller.emit(e.detail.scrollTop);
} }
// 页面滚动事件 // 页面滚动事件
onPageScroll((e) => { scroller.on((top) => {
scrollTop.value = e.scrollTop; scrollTop.value = top;
}); });
// 滚动到指定位置 // 滚动到指定位置

View File

@@ -57,9 +57,9 @@ const props = defineProps({
const { proxy } = getCurrentInstance()!; const { proxy } = getCurrentInstance()!;
// cl-page 上下文 // cl-page 上下文
const page = usePage(); const { onScroll } = usePage();
// 定义Rect类型表示元素的位置信息 // 表示元素的位置信息
type Rect = { type Rect = {
height: number; // 高度 height: number; // 高度
width: number; // 宽度 width: number; // 宽度
@@ -67,7 +67,7 @@ type Rect = {
top: number; // 距离页面顶部的距离 top: number; // 距离页面顶部的距离
}; };
// rect为响应式对象存储当前sticky元素的位置信息 // 存储当前sticky元素的位置信息
const rect = reactive<Rect>({ const rect = reactive<Rect>({
height: 0, height: 0,
width: 0, width: 0,
@@ -75,7 +75,7 @@ const rect = reactive<Rect>({
top: 0 top: 0
}); });
// scrollTop为当前页面滚动的距离 // 当前页面滚动的距离
const scrollTop = ref(0); const scrollTop = ref(0);
// 计算属性,判断当前是否处于吸顶状态 // 计算属性,判断当前是否处于吸顶状态
@@ -128,14 +128,16 @@ function getRect() {
}); });
} }
// 监听页面滚动事件
page.onPageScroll((top) => {
scrollTop.value = top;
});
onMounted(() => { onMounted(() => {
// 获取元素位置信息
getRect(); getRect();
// 监听页面滚动事件
onScroll((top) => {
scrollTop.value = top;
});
// 监听参数变化
watch( watch(
computed(() => props.scrollTop), computed(() => props.scrollTop),
(top: number) => { (top: number) => {

View File

@@ -1,66 +1,50 @@
import { router, useParent } from "@/cool"; import { router, scroller, useParent } from "@/cool";
import { computed, watch } from "vue";
type PageScrollCallback = (top: number) => void;
class Page { class Page {
listeners: PageScrollCallback[] = [];
pageRef: ClPageComponentPublicInstance | null = null; pageRef: ClPageComponentPublicInstance | null = null;
constructor() { constructor() {
this.pageRef = useParent<ClPageComponentPublicInstance>("cl-page"); this.pageRef = useParent<ClPageComponentPublicInstance>("cl-page");
if (this.pageRef != null) {
// TODO: 小程序异常
watch(
computed(() => this.pageRef!.scrollTop),
(top: number) => {
this.listeners.forEach((listener) => {
listener(top);
});
}
);
}
} }
/** /**
* 获取页面路径 * 获取页面路径
* @returns 页面路径 * @returns 页面路径
*/ */
path() { path = () => {
return router.path(); return router.path();
} };
/** /**
* 获取滚动位置 * 获取滚动位置
* @returns 滚动位置 * @returns 滚动位置
*/ */
getScrollTop(): number { getScrollTop = (): number => {
return this.pageRef!.scrollTop as number; return this.pageRef!.scrollTop as number;
} };
/** /**
* 滚动到指定位置 * 滚动到指定位置
* @param top 滚动位置 * @param top 滚动位置
*/ */
scrollTo(top: number) { scrollTo = (top: number) => {
this.pageRef!.scrollTo(top); this.pageRef!.scrollTo(top);
} };
/** /**
* 回到顶部 * 回到顶部
*/ */
scrollToTop() { scrollToTop = () => {
this.pageRef!.scrollToTop(); this.pageRef!.scrollToTop();
} };
/** /**
* 监听页面滚动 * 监听页面滚动
* @param callback 回调函数 * @param callback 回调函数
*/ */
onPageScroll(callback: PageScrollCallback) { onScroll = (callback: (top: number) => void) => {
this.listeners.push(callback); scroller.on(callback);
} };
} }
export function usePage(): Page { export function usePage(): Page {