优化页面滚动事件

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 { initLocale } from "@/locale";
export function cool(app: VueApp) {
app.mixin({
onPageScroll(e) {
scroller.emit(e.scrollTop);
},
onShow() {
// #ifdef H5
setTimeout(() => {
@@ -18,11 +22,12 @@ export function cool(app: VueApp) {
console.log(app);
}
export * from "./utils";
export * from "./theme";
export * from "./router";
export * from "./service";
export * from "./hooks";
export * from "./ctx";
export * from "./hooks";
export * from "./router";
export * from "./scroller";
export * from "./service";
export * from "./store";
export * from "./theme";
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="{
indexBar: {
className: '!fixed'
},
itemHover: {
className: 'bg-gray-200'
}
}"
>
@@ -29,12 +32,16 @@
<script lang="ts" setup>
import { request } from "@/cool";
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";
const ui = useUi();
const data = ref<ClListViewItem[]>([]);
onReady(() => {
ui.showLoading();
request<UTSJSONObject[]>({
url: "https://unix.cool-js.com/data/pca_flat.json"
})
@@ -43,6 +50,9 @@ onReady(() => {
})
.catch((err) => {
console.error(err);
})
.finally(() => {
ui.hideLoading();
});
});
</script>

View File

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

View File

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

View File

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

View File

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

View File

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