版本发布
This commit is contained in:
593
uni_modules/cool-ui/components/cl-popup/cl-popup.uvue
Normal file
593
uni_modules/cool-ui/components/cl-popup/cl-popup.uvue
Normal file
@@ -0,0 +1,593 @@
|
||||
<template>
|
||||
<view
|
||||
class="cl-popup-wrapper"
|
||||
:class="[`cl-popup-wrapper--${direction}`]"
|
||||
:style="{
|
||||
zIndex,
|
||||
pointerEvents
|
||||
}"
|
||||
v-show="visible"
|
||||
v-if="keepAlive ? true : visible"
|
||||
@touchmove.stop.prevent
|
||||
>
|
||||
<view
|
||||
class="cl-popup-mask"
|
||||
:class="[
|
||||
{
|
||||
'is-open': status == 1,
|
||||
'is-close': status == 2
|
||||
},
|
||||
pt.mask?.className
|
||||
]"
|
||||
@tap="maskClose"
|
||||
v-if="showMask"
|
||||
></view>
|
||||
|
||||
<view
|
||||
class="cl-popup"
|
||||
:class="[
|
||||
{
|
||||
'is-open': status == 1,
|
||||
'is-close': status == 2,
|
||||
'is-custom-navbar': router.isCustomNavbarPage(),
|
||||
'stop-transition': swipe.isTouch
|
||||
},
|
||||
pt.className
|
||||
]"
|
||||
:style="popupStyle"
|
||||
@touchstart="onTouchStart"
|
||||
@touchmove="onTouchMove"
|
||||
@touchend="onTouchEnd"
|
||||
>
|
||||
<view
|
||||
class="cl-popup__inner"
|
||||
:class="[
|
||||
{
|
||||
'is-dark': isDark
|
||||
},
|
||||
pt.inner?.className
|
||||
]"
|
||||
:style="{
|
||||
paddingBottom
|
||||
}"
|
||||
>
|
||||
<view
|
||||
class="cl-popup__draw"
|
||||
:class="[
|
||||
{
|
||||
'!bg-surface-400': swipe.isMove
|
||||
},
|
||||
pt.draw?.className
|
||||
]"
|
||||
v-if="isSwipeClose"
|
||||
></view>
|
||||
|
||||
<view class="cl-popup__header" :class="[pt.header?.className]" v-if="showHeader">
|
||||
<slot name="header">
|
||||
<cl-text
|
||||
:pt="{
|
||||
className: `text-lg font-bold ${pt.header?.text?.className}`
|
||||
}"
|
||||
>{{ title }}</cl-text
|
||||
>
|
||||
</slot>
|
||||
|
||||
<cl-icon
|
||||
name="close-circle-fill"
|
||||
:size="40"
|
||||
:pt="{
|
||||
className:
|
||||
'absolute right-[24rpx] !text-surface-400 dark:!text-surface-50'
|
||||
}"
|
||||
@tap="close"
|
||||
v-if="isOpen && showClose"
|
||||
></cl-icon>
|
||||
</view>
|
||||
|
||||
<view class="cl-popup__container" :class="[pt.container?.className]">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref, watch, type PropType } from "vue";
|
||||
import { parsePt, parseRpx, usePage } from "@/cool";
|
||||
import type { ClPopupDirection, PassThroughProps } from "../../types";
|
||||
import { isDark, router } from "@/cool";
|
||||
import { config } from "../../config";
|
||||
|
||||
defineOptions({
|
||||
name: "cl-popup"
|
||||
});
|
||||
|
||||
defineSlots<{
|
||||
header: () => any;
|
||||
}>();
|
||||
|
||||
// 组件属性定义
|
||||
const props = defineProps({
|
||||
// 透传样式配置
|
||||
pt: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
// 是否可见
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 标题
|
||||
title: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
// 弹出方向
|
||||
direction: {
|
||||
type: String as PropType<ClPopupDirection>,
|
||||
default: "bottom"
|
||||
},
|
||||
// 弹出框宽度
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: ""
|
||||
},
|
||||
// 是否显示头部
|
||||
showHeader: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 显示关闭按钮
|
||||
showClose: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否显示遮罩层
|
||||
showMask: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否点击遮罩层关闭弹窗
|
||||
maskClosable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否开启拖拽关闭
|
||||
swipeClose: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 拖拽关闭的阈值
|
||||
swipeCloseThreshold: {
|
||||
type: Number,
|
||||
default: 150
|
||||
},
|
||||
// 触摸事件响应方式
|
||||
pointerEvents: {
|
||||
type: String as PropType<"auto" | "none">,
|
||||
default: "auto"
|
||||
},
|
||||
// 是否开启缓存
|
||||
keepAlive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
// 定义组件事件
|
||||
const emit = defineEmits(["update:modelValue", "open", "opened", "close", "closed", "maskClose"]);
|
||||
|
||||
// 页面实例
|
||||
const page = usePage();
|
||||
|
||||
type HeaderPassThrough = {
|
||||
className?: string;
|
||||
text?: PassThroughProps;
|
||||
};
|
||||
|
||||
// 透传样式类型定义
|
||||
type PassThrough = {
|
||||
className?: string;
|
||||
inner?: PassThroughProps;
|
||||
header?: HeaderPassThrough;
|
||||
container?: PassThroughProps;
|
||||
mask?: PassThroughProps;
|
||||
draw?: PassThroughProps;
|
||||
};
|
||||
|
||||
// 解析透传样式配置
|
||||
const pt = computed(() => parsePt<PassThrough>(props.pt));
|
||||
|
||||
// 控制弹出层显示/隐藏
|
||||
const visible = ref(false);
|
||||
|
||||
// 0: 初始状态 1: 打开中 2: 关闭中
|
||||
const status = ref(0);
|
||||
|
||||
// 标记弹出层是否处于打开状态(包含动画过程)
|
||||
const isOpen = ref(false);
|
||||
|
||||
// 标记弹出层是否已完全打开(动画结束)
|
||||
const isOpened = ref(false);
|
||||
|
||||
// 弹出层z-index值
|
||||
const zIndex = ref(config.zIndex);
|
||||
|
||||
// 计算弹出层高度
|
||||
const height = computed(() => {
|
||||
switch (props.direction) {
|
||||
case "top":
|
||||
case "bottom":
|
||||
return parseRpx(props.size); // 顶部和底部弹出时使用传入的size
|
||||
case "left":
|
||||
case "right":
|
||||
return "100%"; // 左右弹出时占满全高
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
// 计算弹出层宽度
|
||||
const width = computed(() => {
|
||||
switch (props.direction) {
|
||||
case "top":
|
||||
case "bottom":
|
||||
return "100%"; // 顶部和底部弹出时占满全宽
|
||||
case "left":
|
||||
case "right":
|
||||
case "center":
|
||||
return parseRpx(props.size); // 其他方向使用传入的size
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
// 底部安全距离
|
||||
const paddingBottom = computed(() => {
|
||||
let h = 0;
|
||||
|
||||
if (props.direction == "bottom") {
|
||||
h += page.hasCustomTabBar() ? page.getTabBarHeight() : page.getSafeAreaHeight("bottom");
|
||||
}
|
||||
|
||||
return h + "px";
|
||||
});
|
||||
|
||||
// 是否显示拖动条
|
||||
const isSwipeClose = computed(() => {
|
||||
return props.direction == "bottom" && props.swipeClose;
|
||||
});
|
||||
|
||||
// 动画定时器
|
||||
let timer: number = 0;
|
||||
|
||||
// 打开弹出层
|
||||
function open() {
|
||||
// 递增z-index,保证多个弹出层次序
|
||||
zIndex.value = config.zIndex++;
|
||||
|
||||
if (!visible.value) {
|
||||
// 显示弹出层
|
||||
visible.value = true;
|
||||
|
||||
// 触发事件
|
||||
emit("update:modelValue", true);
|
||||
emit("open");
|
||||
|
||||
// 等待DOM更新后开始动画
|
||||
setTimeout(() => {
|
||||
// 设置打开状态
|
||||
status.value = 1;
|
||||
|
||||
// 动画结束后触发opened事件
|
||||
// @ts-ignore
|
||||
timer = setTimeout(() => {
|
||||
isOpened.value = true;
|
||||
emit("opened");
|
||||
}, 350);
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭弹出层
|
||||
function close() {
|
||||
if (status.value == 1) {
|
||||
// 重置打开状态
|
||||
isOpened.value = false;
|
||||
|
||||
// 设置关闭状态
|
||||
status.value = 2;
|
||||
|
||||
// 触发事件
|
||||
emit("close");
|
||||
|
||||
// 清除未完成的定时器
|
||||
if (timer != 0) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
|
||||
// 动画结束后隐藏弹出层
|
||||
// @ts-ignore
|
||||
timer = setTimeout(() => {
|
||||
// 隐藏弹出层
|
||||
visible.value = false;
|
||||
|
||||
// 重置状态
|
||||
status.value = 0;
|
||||
|
||||
// 触发事件
|
||||
emit("update:modelValue", false);
|
||||
emit("closed");
|
||||
}, 350);
|
||||
}
|
||||
}
|
||||
|
||||
// 点击遮罩层关闭
|
||||
function maskClose() {
|
||||
if (props.maskClosable) {
|
||||
close();
|
||||
}
|
||||
|
||||
emit("maskClose");
|
||||
}
|
||||
// 滑动状态类型定义
|
||||
type Swipe = {
|
||||
isMove: boolean; // 是否移动
|
||||
isTouch: boolean; // 是否处于触摸状态
|
||||
startY: number; // 开始触摸的Y坐标
|
||||
offsetY: number; // Y轴偏移量
|
||||
};
|
||||
|
||||
// 初始化滑动状态数据
|
||||
const swipe = reactive<Swipe>({
|
||||
isMove: false, // 是否移动
|
||||
isTouch: false, // 默认非触摸状态
|
||||
startY: 0, // 初始Y坐标为0
|
||||
offsetY: 0 // 初始偏移量为0
|
||||
});
|
||||
|
||||
/**
|
||||
* 触摸开始事件处理
|
||||
* @param e 触摸事件对象
|
||||
* 当弹出层获得焦点且允许滑动关闭时,记录触摸起始位置
|
||||
*/
|
||||
function onTouchStart(e: UniTouchEvent) {
|
||||
if (isOpened.value && isSwipeClose.value) {
|
||||
swipe.isTouch = true; // 标记开始触摸
|
||||
swipe.startY = e.touches[0].clientY; // 记录起始Y坐标
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触摸移动事件处理
|
||||
* @param e 触摸事件对象
|
||||
* 计算手指移动距离,更新弹出层位置
|
||||
*/
|
||||
function onTouchMove(e: UniTouchEvent) {
|
||||
if (swipe.isTouch) {
|
||||
// 标记为移动状态
|
||||
swipe.isMove = true;
|
||||
|
||||
// 计算Y轴偏移量
|
||||
const offsetY = (e.touches[0] as UniTouch).pageY - swipe.startY;
|
||||
|
||||
// 只允许向下滑动(offsetY > 0)
|
||||
if (offsetY > 0) {
|
||||
swipe.offsetY = offsetY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触摸结束事件处理
|
||||
* 根据滑动距离判断是否关闭弹出层
|
||||
*/
|
||||
function onTouchEnd() {
|
||||
if (swipe.isTouch) {
|
||||
// 结束触摸状态
|
||||
swipe.isTouch = false;
|
||||
|
||||
// 结束移动状态
|
||||
swipe.isMove = false;
|
||||
|
||||
// 如果滑动距离超过阈值,则关闭弹出层
|
||||
if (swipe.offsetY > props.swipeCloseThreshold) {
|
||||
close();
|
||||
}
|
||||
|
||||
// 重置偏移量
|
||||
swipe.offsetY = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算弹出层样式
|
||||
* 根据滑动状态动态设置transform属性实现位移动画
|
||||
*/
|
||||
const popupStyle = computed(() => {
|
||||
const style = new Map<string, string>();
|
||||
// 基础样式
|
||||
style.set("height", height.value);
|
||||
style.set("width", width.value);
|
||||
|
||||
// 处于触摸状态时添加位移效果
|
||||
if (swipe.isTouch) {
|
||||
style.set("transform", `translateY(${swipe.offsetY}px)`);
|
||||
}
|
||||
|
||||
return style;
|
||||
});
|
||||
|
||||
// 监听modelValue变化
|
||||
watch(
|
||||
computed(() => props.modelValue),
|
||||
(val: boolean) => {
|
||||
if (val) {
|
||||
open();
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
// 监听状态变化
|
||||
watch(status, (val: number) => {
|
||||
isOpen.value = val == 1;
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
isOpened,
|
||||
isOpen,
|
||||
open,
|
||||
close
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cl-popup-wrapper {
|
||||
@apply h-full w-full;
|
||||
@apply fixed top-0 bottom-0 left-0 right-0;
|
||||
pointer-events: none;
|
||||
|
||||
.cl-popup-mask {
|
||||
@apply absolute top-0 bottom-0 left-0 right-0;
|
||||
@apply h-full w-full;
|
||||
@apply bg-black opacity-0;
|
||||
transition-property: opacity;
|
||||
|
||||
&.is-open {
|
||||
@apply opacity-40;
|
||||
}
|
||||
|
||||
&.is-open,
|
||||
&.is-close {
|
||||
transition-duration: 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.cl-popup {
|
||||
@apply absolute;
|
||||
|
||||
&__inner {
|
||||
@apply bg-white h-full w-full flex flex-col;
|
||||
|
||||
&.is-dark {
|
||||
@apply bg-surface-700;
|
||||
}
|
||||
}
|
||||
|
||||
&__draw {
|
||||
@apply bg-surface-200 rounded-md;
|
||||
@apply absolute top-2 left-1/2;
|
||||
height: 10rpx;
|
||||
width: 70rpx;
|
||||
transform: translateX(-50%);
|
||||
transition-property: background-color;
|
||||
transition-duration: 0.2s;
|
||||
}
|
||||
|
||||
&__header {
|
||||
@apply flex flex-row items-center;
|
||||
height: 90rpx;
|
||||
padding: 0 26rpx;
|
||||
}
|
||||
|
||||
&__container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&.is-open {
|
||||
transform: translate(0, 0) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.is-open,
|
||||
&.is-close {
|
||||
transition-property: transform, opacity;
|
||||
transition-duration: 0.3s;
|
||||
}
|
||||
|
||||
&.stop-transition {
|
||||
transition-property: none;
|
||||
}
|
||||
}
|
||||
|
||||
&--left {
|
||||
.cl-popup {
|
||||
@apply left-0 top-0;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
&--right {
|
||||
.cl-popup {
|
||||
@apply right-0 top-0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
&--top {
|
||||
.cl-popup {
|
||||
@apply left-0 top-0;
|
||||
|
||||
&__inner {
|
||||
@apply rounded-b-2xl;
|
||||
}
|
||||
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
&--left,
|
||||
&--right,
|
||||
&--top {
|
||||
.cl-popup {
|
||||
// #ifdef H5
|
||||
top: 44px;
|
||||
// #endif
|
||||
|
||||
&.is-custom-navbar {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--left,
|
||||
&--right {
|
||||
.cl-popup {
|
||||
// #ifdef H5
|
||||
height: calc(100% - 44px) !important;
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
&--bottom {
|
||||
.cl-popup {
|
||||
@apply left-0 bottom-0;
|
||||
transform: translateY(100%);
|
||||
|
||||
&__inner {
|
||||
@apply rounded-t-2xl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--center {
|
||||
@apply flex flex-col items-center justify-center;
|
||||
|
||||
.cl-popup {
|
||||
transform: scale(1.3);
|
||||
opacity: 0;
|
||||
|
||||
&__inner {
|
||||
@apply rounded-2xl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
32
uni_modules/cool-ui/components/cl-popup/props.ts
Normal file
32
uni_modules/cool-ui/components/cl-popup/props.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { ClPopupDirection, PassThroughProps } from "../../types";
|
||||
|
||||
export type ClPopupHeaderPassThrough = {
|
||||
className?: string;
|
||||
text?: PassThroughProps;
|
||||
};
|
||||
|
||||
export type ClPopupPassThrough = {
|
||||
className?: string;
|
||||
inner?: PassThroughProps;
|
||||
header?: ClPopupHeaderPassThrough;
|
||||
container?: PassThroughProps;
|
||||
mask?: PassThroughProps;
|
||||
draw?: PassThroughProps;
|
||||
};
|
||||
|
||||
export type ClPopupProps = {
|
||||
className?: string;
|
||||
pt?: ClPopupPassThrough;
|
||||
modelValue?: boolean;
|
||||
title?: string;
|
||||
direction?: ClPopupDirection;
|
||||
size?: any;
|
||||
showHeader?: boolean;
|
||||
showClose?: boolean;
|
||||
showMask?: boolean;
|
||||
maskClosable?: boolean;
|
||||
swipeClose?: boolean;
|
||||
swipeCloseThreshold?: number;
|
||||
pointerEvents?: "auto" | "none";
|
||||
keepAlive?: boolean;
|
||||
};
|
||||
Reference in New Issue
Block a user