Browse Source

优化 usePage

icssoa 7 months ago
parent
commit
f2621341a4

+ 0 - 1
cool/hooks/index.ts

@@ -1,5 +1,4 @@
 export * from "./refs";
 export * from "./refs";
-export * from "./page";
 export * from "./pager";
 export * from "./pager";
 export * from "./long-press";
 export * from "./long-press";
 export * from "./cache";
 export * from "./cache";

+ 0 - 102
cool/hooks/page.ts

@@ -1,102 +0,0 @@
-import { config } from "@/config";
-import { router } from "../router";
-import { getPx, isH5, isHarmony } from "../utils";
-import { ctx } from "../ctx";
-
-class Page {
-	scrolls: Map<string, ((top: number) => void)[]> = new Map();
-
-	path() {
-		return router.path();
-	}
-
-	/**
-	 * 触发滚动事件
-	 * @param top 滚动距离
-	 */
-	triggerScroll(top: number) {
-		const callbacks = this.scrolls.get(this.path()) ?? [];
-		callbacks.forEach((cb) => {
-			cb(top);
-		});
-	}
-
-	/**
-	 * 注册滚动事件回调
-	 * @param callback 回调函数
-	 */
-	onPageScroll(callback: (top: number) => void) {
-		const callbacks = this.scrolls.get(this.path()) ?? [];
-		callbacks.push(callback);
-		this.scrolls.set(this.path(), callbacks);
-	}
-
-	/**
-	 * 是否需要计算 tabBar 高度
-	 * @returns boolean
-	 */
-	hasCustomTabBar() {
-		if (router.isTabPage()) {
-			if (isHarmony()) {
-				return false;
-			}
-
-			return config.isCustomTabBar || isH5();
-		}
-
-		return false;
-	}
-
-	/**
-	 * 是否存在自定义 topbar
-	 * @returns boolean
-	 */
-	hasCustomTopbar() {
-		return router.route()?.isCustomNavbar ?? false;
-	}
-
-	/**
-	 * 获取 tabBar 高度
-	 * @returns tabBar 高度
-	 */
-	getTabBarHeight() {
-		let h = ctx.tabBar.height == null ? 50 : getPx(ctx.tabBar.height!);
-
-		if (this.hasCustomTabBar()) {
-			h += this.getSafeAreaHeight("bottom");
-		}
-
-		return h;
-	}
-
-	/**
-	 * 获取安全区域高度
-	 * @param type 类型
-	 * @returns 安全区域高度
-	 */
-	getSafeAreaHeight(type: "top" | "bottom") {
-		const { safeAreaInsets } = uni.getWindowInfo();
-
-		let h: number;
-
-		if (type == "top") {
-			h = safeAreaInsets.top;
-		} else {
-			h = safeAreaInsets.bottom;
-
-			// #ifdef APP-ANDROID
-			if (h == 0) {
-				h = 16;
-			}
-			// #endif
-		}
-
-		return h;
-	}
-}
-
-export const page = new Page();
-
-export function usePage(): Page {
-	return page;
-}

+ 0 - 4
cool/index.ts

@@ -1,12 +1,8 @@
-import { page } from "./hooks";
 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) {
-			page.triggerScroll(e.scrollTop);
-		},
 		onShow() {
 		onShow() {
 			// #ifdef H5
 			// #ifdef H5
 			setTimeout(() => {
 			setTimeout(() => {

+ 9 - 9
cool/service/index.ts

@@ -81,18 +81,10 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
 				timeout,
 				timeout,
 
 
 				success(res) {
 				success(res) {
-					if (!isObject(res.data as any)) {
-						resolve(res.data as T);
-						return;
-					}
-
-					// 解析响应数据
-					const { code, message, data } = parse<Response>(res.data ?? { code: 0 })!;
-
 					// 401 无权限
 					// 401 无权限
 					if (res.statusCode == 401) {
 					if (res.statusCode == 401) {
 						user.logout();
 						user.logout();
-						return reject({ message } as Response);
+						return reject({ message: t("无权限") } as Response);
 					}
 					}
 
 
 					// 502 服务异常
 					// 502 服务异常
@@ -111,6 +103,14 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
 
 
 					// 200 正常响应
 					// 200 正常响应
 					if (res.statusCode == 200) {
 					if (res.statusCode == 200) {
+						if (!isObject(res.data as any)) {
+							resolve(res.data as T);
+							return;
+						}
+
+						// 解析响应数据
+						const { code, message, data } = parse<Response>(res.data ?? { code: 0 })!;
+
 						switch (code) {
 						switch (code) {
 							case 1000:
 							case 1000:
 								resolve(data as T);
 								resolve(data as T);

+ 1 - 0
cool/utils/index.ts

@@ -4,4 +4,5 @@ export * from "./device";
 export * from "./file";
 export * from "./file";
 export * from "./parse";
 export * from "./parse";
 export * from "./path";
 export * from "./path";
+export * from "./rect";
 export * from "./storage";
 export * from "./storage";

+ 68 - 0
cool/utils/rect.ts

@@ -0,0 +1,68 @@
+import { config } from "@/config";
+import { router } from "../router";
+import { isH5, isHarmony } from "./comm";
+import { ctx } from "../ctx";
+import { getPx } from "./parse";
+
+/**
+ * 是否需要计算 tabBar 高度
+ * @returns boolean
+ */
+export function hasCustomTabBar() {
+	if (router.isTabPage()) {
+		if (isHarmony()) {
+			return false;
+		}
+
+		return config.isCustomTabBar || isH5();
+	}
+
+	return false;
+}
+
+/**
+ * 是否存在自定义 topbar
+ * @returns boolean
+ */
+export function hasCustomTopbar() {
+	return router.route()?.isCustomNavbar ?? false;
+}
+
+/**
+ * 获取安全区域高度
+ * @param type 类型
+ * @returns 安全区域高度
+ */
+export function getSafeAreaHeight(type: "top" | "bottom") {
+	const { safeAreaInsets } = uni.getWindowInfo();
+
+	let h: number;
+
+	if (type == "top") {
+		h = safeAreaInsets.top;
+	} else {
+		h = safeAreaInsets.bottom;
+
+		// #ifdef APP-ANDROID
+		if (h == 0) {
+			h = 16;
+		}
+		// #endif
+	}
+
+	return h;
+}
+
+/**
+ * 获取 tabBar 高度
+ * @returns tabBar 高度
+ */
+export function getTabBarHeight() {
+	let h = ctx.tabBar.height == null ? 50 : getPx(ctx.tabBar.height!);
+
+	if (hasCustomTabBar()) {
+		h += getSafeAreaHeight("bottom");
+	}
+
+	return h;
+}

+ 17 - 9
uni_modules/cool-ui/components/cl-back-top/cl-back-top.uvue

@@ -1,5 +1,5 @@
 <template>
 <template>
-	<view class="cl-back-top-wrapper" :style="{ bottom }">
+	<view class="cl-back-top-wrapper" :style="{ bottom }" @tap="toTop">
 		<view
 		<view
 			class="cl-back-top"
 			class="cl-back-top"
 			:class="{
 			:class="{
@@ -12,8 +12,9 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
+import { getTabBarHeight, hasCustomTabBar } from "@/cool";
 import { computed, onMounted, ref, watch, type PropType } from "vue";
 import { computed, onMounted, ref, watch, type PropType } from "vue";
-import { usePage } from "@/cool";
+import { usePage } from "../../hooks";
 
 
 defineOptions({
 defineOptions({
 	name: "cl-back-top"
 	name: "cl-back-top"
@@ -26,9 +27,11 @@ const props = defineProps({
 	}
 	}
 });
 });
 
 
-const page = usePage();
 const { screenHeight } = uni.getWindowInfo();
 const { screenHeight } = uni.getWindowInfo();
 
 
+// cl-page 上下文
+const page = usePage();
+
 // 是否显示回到顶部按钮
 // 是否显示回到顶部按钮
 const visible = ref(false);
 const visible = ref(false);
 
 
@@ -36,25 +39,30 @@ const visible = ref(false);
 const bottom = computed(() => {
 const bottom = computed(() => {
 	let h = 20;
 	let h = 20;
 
 
-	if (page.hasCustomTabBar()) {
-		h += page.getTabBarHeight();
+	if (hasCustomTabBar()) {
+		h += getTabBarHeight();
 	}
 	}
 
 
 	return h + "px";
 	return h + "px";
 });
 });
 
 
-// 控制是否显示回到顶部按钮
-function update(top: number) {
+// 控制是否显示
+function onVisible(top: number) {
 	visible.value = top > screenHeight - 100;
 	visible.value = top > screenHeight - 100;
 }
 }
 
 
+// 回到顶部
+function toTop() {
+	page.scrollToTop();
+}
+
 onMounted(() => {
 onMounted(() => {
 	if (props.top != null) {
 	if (props.top != null) {
 		// 监听参数变化
 		// 监听参数变化
 		watch(
 		watch(
 			computed(() => props.top!),
 			computed(() => props.top!),
 			(top: number) => {
 			(top: number) => {
-				update(top);
+				onVisible(top);
 			},
 			},
 			{
 			{
 				immediate: true
 				immediate: true
@@ -63,7 +71,7 @@ onMounted(() => {
 	} else {
 	} else {
 		// 监听页面滚动
 		// 监听页面滚动
 		page.onPageScroll((top) => {
 		page.onPageScroll((top) => {
-			update(top);
+			onVisible(top);
 		});
 		});
 	}
 	}
 });
 });

+ 2 - 5
uni_modules/cool-ui/components/cl-cropper/cl-cropper.uvue

@@ -132,7 +132,7 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import { computed, ref, reactive, nextTick, getCurrentInstance } from "vue";
 import { computed, ref, reactive, nextTick, getCurrentInstance } from "vue";
 import type { PassThroughProps } from "../../types";
 import type { PassThroughProps } from "../../types";
-import { canvasToPng, getDevicePixelRatio, parsePt, usePage, uuid } from "@/cool";
+import { canvasToPng, getDevicePixelRatio, getSafeAreaHeight, parsePt, uuid } from "@/cool";
 
 
 // 定义遮罩层样式类型
 // 定义遮罩层样式类型
 type MaskStyle = {
 type MaskStyle = {
@@ -225,9 +225,6 @@ const emit = defineEmits(["crop", "load", "error"]);
 // 获取当前实例
 // 获取当前实例
 const { proxy } = getCurrentInstance()!;
 const { proxy } = getCurrentInstance()!;
 
 
-// 获取页面实例,用于获取视图尺寸
-const page = usePage();
-
 // 创建唯一的canvas ID
 // 创建唯一的canvas ID
 const canvasId = `cl-cropper__${uuid()}`;
 const canvasId = `cl-cropper__${uuid()}`;
 
 
@@ -382,7 +379,7 @@ const maskStyle = computed<MaskStyle>(() => {
 
 
 // 底部按钮组样式
 // 底部按钮组样式
 const opStyle = computed(() => {
 const opStyle = computed(() => {
-	let bottom = page.getSafeAreaHeight("bottom");
+	let bottom = getSafeAreaHeight("bottom");
 
 
 	if (bottom == 0) {
 	if (bottom == 0) {
 		bottom = 10;
 		bottom = 10;

+ 5 - 8
uni_modules/cool-ui/components/cl-float-view/cl-float-view.uvue

@@ -12,7 +12,7 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { router, usePage } from "@/cool";
+import { getSafeAreaHeight, getTabBarHeight, hasCustomTabBar, router } from "@/cool";
 import { computed, reactive } from "vue";
 import { computed, reactive } from "vue";
 
 
 defineOptions({
 defineOptions({
@@ -60,9 +60,6 @@ const props = defineProps({
 // 获取设备屏幕信息
 // 获取设备屏幕信息
 const { screenWidth, statusBarHeight, screenHeight } = uni.getWindowInfo();
 const { screenWidth, statusBarHeight, screenHeight } = uni.getWindowInfo();
 
 
-// 页面实例
-const page = usePage();
-
 /**
 /**
  * 悬浮按钮位置状态类型定义
  * 悬浮按钮位置状态类型定义
  */
  */
@@ -110,8 +107,8 @@ const viewStyle = computed(() => {
 	let bottomOffset = 0;
 	let bottomOffset = 0;
 
 
 	// 标签页需要额外减去标签栏高度和安全区域
 	// 标签页需要额外减去标签栏高度和安全区域
-	if (page.hasCustomTabBar()) {
-		bottomOffset += page.getTabBarHeight();
+	if (hasCustomTabBar()) {
+		bottomOffset += getTabBarHeight();
 	}
 	}
 
 
 	// 设置水平位置
 	// 设置水平位置
@@ -151,7 +148,7 @@ function calculateMaxY(): number {
 
 
 	// 标签页需要额外减去标签栏高度和安全区域
 	// 标签页需要额外减去标签栏高度和安全区域
 	if (router.isTabPage()) {
 	if (router.isTabPage()) {
-		maxY -= page.getTabBarHeight();
+		maxY -= getTabBarHeight();
 	}
 	}
 
 
 	return maxY;
 	return maxY;
@@ -206,7 +203,7 @@ function onTouchMove(e: TouchEvent) {
 	let minY = 0;
 	let minY = 0;
 	// 非标签页时,底部需要考虑安全区域
 	// 非标签页时,底部需要考虑安全区域
 	if (!router.isTabPage()) {
 	if (!router.isTabPage()) {
-		minY += page.getSafeAreaHeight("bottom");
+		minY += getSafeAreaHeight("bottom");
 	}
 	}
 
 
 	// 确保按钮不超出屏幕上下边界
 	// 确保按钮不超出屏幕上下边界

+ 2 - 3
uni_modules/cool-ui/components/cl-footer/cl-footer.uvue

@@ -20,7 +20,7 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { isDark, isHarmony, parsePt, usePage } from "@/cool";
+import { getSafeAreaHeight, isDark, isHarmony, parsePt } from "@/cool";
 import { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from "vue";
 import { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from "vue";
 import type { PassThroughProps } from "../../types";
 import type { PassThroughProps } from "../../types";
 
 
@@ -46,7 +46,6 @@ const props = defineProps({
 });
 });
 
 
 const { proxy } = getCurrentInstance()!;
 const { proxy } = getCurrentInstance()!;
-const page = usePage();
 
 
 type PassThrough = {
 type PassThrough = {
 	className?: string;
 	className?: string;
@@ -77,7 +76,7 @@ function getHeight() {
 						height.value = h;
 						height.value = h;
 
 
 						// 如果内容高度大于最小高度,则显示
 						// 如果内容高度大于最小高度,则显示
-						visible.value = h > props.minHeight + page.getSafeAreaHeight("bottom");
+						visible.value = h > props.minHeight + getSafeAreaHeight("bottom");
 					})
 					})
 					.exec();
 					.exec();
 			},
 			},

+ 9 - 1
uni_modules/cool-ui/components/cl-form-item/cl-form-item.uvue

@@ -1,5 +1,13 @@
 <template>
 <template>
-	<view class="cl-form-item" :class="[pt.className]">
+	<view
+		class="cl-form-item"
+		:class="[
+			{
+				'cl-form-item--error': isError
+			},
+			pt.className
+		]"
+	>
 		<view class="cl-form-item__inner" :class="[`is-${labelPosition}`, pt.inner?.className]">
 		<view class="cl-form-item__inner" :class="[`is-${labelPosition}`, pt.inner?.className]">
 			<view
 			<view
 				class="cl-form-item__label"
 				class="cl-form-item__label"

+ 42 - 1
uni_modules/cool-ui/components/cl-form/cl-form.uvue

@@ -14,10 +14,11 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { computed, nextTick, ref, watch, type PropType } from "vue";
+import { computed, getCurrentInstance, nextTick, ref, watch, type PropType } from "vue";
 import { isEmpty, isString, parsePt, parseToObject } from "@/cool";
 import { isEmpty, isString, parsePt, parseToObject } from "@/cool";
 import type { ClFormLabelPosition, ClFormRule, ClFormValidateError } from "../../types";
 import type { ClFormLabelPosition, ClFormRule, ClFormValidateError } from "../../types";
 import { $t, t } from "@/locale";
 import { $t, t } from "@/locale";
+import { usePage } from "../../hooks";
 
 
 defineOptions({
 defineOptions({
 	name: "cl-form"
 	name: "cl-form"
@@ -64,9 +65,19 @@ const props = defineProps({
 	disabled: {
 	disabled: {
 		type: Boolean,
 		type: Boolean,
 		default: false
 		default: false
+	},
+	// 滚动到第一个错误位置
+	scrollToError: {
+		type: Boolean,
+		default: true
 	}
 	}
 });
 });
 
 
+const { proxy } = getCurrentInstance()!;
+
+// cl-page 上下文
+const page = usePage();
+
 // 透传样式类型
 // 透传样式类型
 type PassThrough = {
 type PassThrough = {
 	className?: string;
 	className?: string;
@@ -269,6 +280,31 @@ function validateField(prop: string): string | null {
 	return error;
 	return error;
 }
 }
 
 
+// 滚动到第一个错误位置
+function scrollToError(prop: string) {
+	if (props.scrollToError == false) {
+		return;
+	}
+
+	nextTick(() => {
+		let component = proxy;
+
+		// #ifdef MP
+		component = proxy?.$children.find((e: any) => e.prop == prop);
+		// #endif
+
+		uni.createSelectorQuery()
+			.in(component)
+			.select(".cl-form-item--error")
+			.boundingClientRect((res) => {
+				if (res != null) {
+					page.scrollTo(((res as NodeInfo).top ?? 0) + page.getScrollTop());
+				}
+			})
+			.exec();
+	});
+}
+
 // 验证整个表单
 // 验证整个表单
 function validate(callback: (valid: boolean, errors: ClFormValidateError[]) => void) {
 function validate(callback: (valid: boolean, errors: ClFormValidateError[]) => void) {
 	const errs = [] as ClFormValidateError[];
 	const errs = [] as ClFormValidateError[];
@@ -284,6 +320,11 @@ function validate(callback: (valid: boolean, errors: ClFormValidateError[]) => v
 		}
 		}
 	});
 	});
 
 
+	// 滚动到第一个错误位置
+	if (errs.length > 0) {
+		scrollToError(errs[0].field);
+	}
+
 	callback(errs.length == 0, errs);
 	callback(errs.length == 0, errs);
 }
 }
 
 

+ 25 - 8
uni_modules/cool-ui/components/cl-page/cl-page.uvue

@@ -23,11 +23,11 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { computed, onMounted, ref, watch } from "vue";
 import { computed, onMounted, ref, watch } from "vue";
-import { router, usePage } from "@/cool";
 import Theme from "./theme.uvue";
 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";
 
 
 defineOptions({
 defineOptions({
 	name: "cl-page"
 	name: "cl-page"
@@ -41,34 +41,45 @@ defineProps({
 	}
 	}
 });
 });
 
 
-const page = usePage();
+// 滚动距离
+const scrollTop = ref(0);
 
 
 // scroll-view 滚动位置
 // scroll-view 滚动位置
 const scrollViewTop = ref(0);
 const scrollViewTop = ref(0);
 
 
 // view 滚动事件
 // view 滚动事件
 function onScroll(e: UniScrollEvent) {
 function onScroll(e: UniScrollEvent) {
-	page.triggerScroll(e.detail.scrollTop);
+	scrollTop.value = e.detail.scrollTop;
 }
 }
 
 
-// 回到顶部
-function scrollToTop() {
+// 页面滚动事件
+onPageScroll((e) => {
+	scrollTop.value = e.scrollTop;
+});
+
+// 滚动到指定位置
+function scrollTo(top: number) {
 	// #ifdef H5
 	// #ifdef H5
-	window.scrollTo({ top: 0, behavior: "smooth" });
+	window.scrollTo({ top, behavior: "smooth" });
 	// #endif
 	// #endif
 
 
 	// #ifdef MP
 	// #ifdef MP
 	uni.pageScrollTo({
 	uni.pageScrollTo({
-		scrollTop: 0,
+		scrollTop: top,
 		duration: 300
 		duration: 300
 	});
 	});
 	// #endif
 	// #endif
 
 
 	// #ifdef APP
 	// #ifdef APP
-	scrollViewTop.value = 0 + Math.random() / 1000;
+	scrollViewTop.value = top;
 	// #endif
 	// #endif
 }
 }
 
 
+// 回到顶部
+function scrollToTop() {
+	scrollTo(0 + Math.random() / 1000);
+}
+
 onMounted(() => {
 onMounted(() => {
 	// 标题多语言
 	// 标题多语言
 	// #ifdef H5 || APP
 	// #ifdef H5 || APP
@@ -91,4 +102,10 @@ onMounted(() => {
 	);
 	);
 	// #endif
 	// #endif
 });
 });
+
+defineExpose({
+	scrollTop,
+	scrollTo,
+	scrollToTop
+});
 </script>
 </script>

+ 3 - 6
uni_modules/cool-ui/components/cl-popup/cl-popup.uvue

@@ -100,7 +100,7 @@
 
 
 <script lang="ts" setup>
 <script lang="ts" setup>
 import { computed, reactive, ref, watch, type PropType } from "vue";
 import { computed, reactive, ref, watch, type PropType } from "vue";
-import { parsePt, parseRpx, usePage } from "@/cool";
+import { getSafeAreaHeight, getTabBarHeight, hasCustomTabBar, parsePt, parseRpx } from "@/cool";
 import type { ClPopupDirection, PassThroughProps } from "../../types";
 import type { ClPopupDirection, PassThroughProps } from "../../types";
 import { isDark, router } from "@/cool";
 import { isDark, router } from "@/cool";
 import { config } from "../../config";
 import { config } from "../../config";
@@ -185,15 +185,12 @@ const props = defineProps({
 // 定义组件事件
 // 定义组件事件
 const emit = defineEmits(["update:modelValue", "open", "opened", "close", "closed", "maskClose"]);
 const emit = defineEmits(["update:modelValue", "open", "opened", "close", "closed", "maskClose"]);
 
 
-// 页面实例
-const page = usePage();
-
+// 透传样式类型定义
 type HeaderPassThrough = {
 type HeaderPassThrough = {
 	className?: string;
 	className?: string;
 	text?: PassThroughProps;
 	text?: PassThroughProps;
 };
 };
 
 
-// 透传样式类型定义
 type PassThrough = {
 type PassThrough = {
 	className?: string;
 	className?: string;
 	inner?: PassThroughProps;
 	inner?: PassThroughProps;
@@ -255,7 +252,7 @@ const paddingBottom = computed(() => {
 	let h = 0;
 	let h = 0;
 
 
 	if (props.direction == "bottom") {
 	if (props.direction == "bottom") {
-		h += page.hasCustomTabBar() ? page.getTabBarHeight() : page.getSafeAreaHeight("bottom");
+		h += hasCustomTabBar() ? getTabBarHeight() : getSafeAreaHeight("bottom");
 	}
 	}
 
 
 	return h + "px";
 	return h + "px";

+ 2 - 2
uni_modules/cool-ui/components/cl-safe-area/cl-safe-area.uvue

@@ -11,7 +11,7 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { isDark, page, parsePt } from "@/cool";
+import { getSafeAreaHeight, isDark, parsePt } from "@/cool";
 import { computed, type PropType } from "vue";
 import { computed, type PropType } from "vue";
 
 
 defineOptions({
 defineOptions({
@@ -37,7 +37,7 @@ const pt = computed(() => parsePt<PassThrough>(props.pt));
 
 
 // 高度
 // 高度
 const height = computed(() => {
 const height = computed(() => {
-	return page.getSafeAreaHeight(props.type) + "px";
+	return getSafeAreaHeight(props.type) + "px";
 });
 });
 </script>
 </script>
 
 

+ 5 - 2
uni_modules/cool-ui/components/cl-sticky/cl-sticky.uvue

@@ -26,8 +26,9 @@
 </template>
 </template>
 
 
 <script lang="ts" setup>
 <script lang="ts" setup>
-import { isEmpty, router, usePage } from "@/cool";
+import { isEmpty, router } from "@/cool";
 import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref, watch } from "vue";
 import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref, watch } from "vue";
+import { usePage } from "../../hooks";
 
 
 defineOptions({
 defineOptions({
 	name: "cl-sticky"
 	name: "cl-sticky"
@@ -53,9 +54,11 @@ const props = defineProps({
 	}
 	}
 });
 });
 
 
-const page = usePage();
 const { proxy } = getCurrentInstance()!;
 const { proxy } = getCurrentInstance()!;
 
 
+// cl-page 上下文
+const page = usePage();
+
 // 定义Rect类型,表示元素的位置信息
 // 定义Rect类型,表示元素的位置信息
 type Rect = {
 type Rect = {
 	height: number; // 高度
 	height: number; // 高度

+ 2 - 1
uni_modules/cool-ui/hooks/index.ts

@@ -1,3 +1,4 @@
-export * from "./ui";
 export * from "./component";
 export * from "./component";
 export * from "./form";
 export * from "./form";
+export * from "./page";
+export * from "./ui";

+ 68 - 0
uni_modules/cool-ui/hooks/page.ts

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

+ 6 - 0
uni_modules/cool-ui/types/component.d.ts

@@ -192,3 +192,9 @@ declare type ClFormItemComponentPublicInstance = {
 	prop: string;
 	prop: string;
 	isError: boolean;
 	isError: boolean;
 };
 };
+
+declare type ClPageComponentPublicInstance = {
+	scrollTop: number;
+	scrollTo: (top: number) => void;
+	scrollToTop: () => void;
+};