feat: 使用Transition重构Popup为center类型的Popup添加zoom-in动画 (#699)

* feat:  使用Transition重构Popup为center类型的Popup添加zoom-in动画

 Closes: #687

* fix: 🐛 修复 Transition 动画类名重复的问题
This commit is contained in:
不如摸鱼去 2024-11-08 13:52:06 +08:00 committed by GitHub
parent 5e55da4839
commit 0dd34d0649
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 269 additions and 272 deletions

View File

@ -8,7 +8,21 @@
`v-model` 为绑定值,表示是否展示弹出层。
```html
<wd-popup v-model="show" custom-style="padding: 30px 40px;" @close="handleClose">内容</wd-popup>
<wd-popup v-model="show" custom-style="border-radius:32rpx;" @close="handleClose">
<text class="custom-txt">弹弹弹</text>
</wd-popup>
```
```css
.custom-txt {
color: black;
width: 400rpx;
height: 400rpx;
display: flex;
justify-content: center;
align-items: center;
font-size: 40rpx;
border-radius: 32rpx;
}
```
## 弹出位置
@ -89,6 +103,7 @@ h5 滚动穿透不需要处理,组件已默认开启 `lock-scroll`。
| hide-when-close | 是否当关闭时将弹出层隐藏display: none) | boolean | - | true | - |
| lazy-render | 弹层内容懒渲染,触发展示时才渲染内容 | boolean | - | true | - |
| safe-area-inset-bottom | 弹出面板是否设置底部安全距离iphone X 类型的机型) | boolean | - | false | - |
| transition | 动画类型,参见 wd-transition 组件的name | string | fade / fade-up / fade-down / fade-left / fade-right / slide-up / slide-down / slide-left / slide-right / zoom-in | - | - |
| lockScroll | 是否锁定背景滚动 | boolean | - | true | 0.1.30 |
## Events

View File

@ -1,3 +1,12 @@
<!--
* @Author: weisheng
* @Date: 2024-10-12 13:07:08
* @LastEditTime: 2024-11-08 13:14:48
* @LastEditors: weisheng
* @Description:
* @FilePath: \wot-design-uni\src\App.vue
* 记得注释
-->
<script setup lang="ts">
import { onLaunch, onShow, onHide, onThemeChange } from '@dcloudio/uni-app'
import { useDark } from './store'
@ -7,7 +16,7 @@ onThemeChange((option) => {
darkMode.setDark(option.theme === 'dark')
})
onLaunch((ctx) => {
onLaunch(() => {
const systemInfo = uni.getSystemInfoSync()
darkMode.setDark(systemInfo.theme === 'dark')
@ -18,8 +27,6 @@ onLaunch((ctx) => {
//
if (typeof event.data === 'boolean') {
darkMode.setDark(event.data)
} else {
darkMode.setDark(false)
}
})
// #endif

View File

@ -45,7 +45,9 @@
</wd-cell-group>
</demo-block>
<wd-popup v-model="show1" custom-style="padding: 30px 40px;" @close="handleClose1"><text class="custom-txt">内容</text></wd-popup>
<wd-popup v-model="show1" @close="handleClose1" custom-style="border-radius:32rpx;">
<text class="custom-txt">弹弹弹</text>
</wd-popup>
<wd-popup v-model="show2" position="top" custom-style="height: 200px;" @close="handleClose2"></wd-popup>
<wd-popup v-model="show3" position="right" custom-style="width: 200px;" @close="handleClose3"></wd-popup>
<wd-popup v-model="show4" position="bottom" custom-style="height: 200px;" @close="handleClose4"></wd-popup>
@ -168,5 +170,12 @@ function handleClose10() {
.custom-txt {
color: black;
width: 400rpx;
height: 400rpx;
display: flex;
justify-content: center;
align-items: center;
font-size: 40rpx;
border-radius: 32rpx;
}
</style>

View File

@ -16,6 +16,7 @@
</demo-block>
<demo-block title="Zoom 动画">
<wd-button @click="zoomIn">zoom-in</wd-button>
<wd-button @click="zoomOut">zoom-out</wd-button>
</demo-block>
<demo-block title="自定义动画">
<wd-button @click="custom">custom</wd-button>
@ -25,7 +26,6 @@
<wd-transition
:show="customShow"
name=""
:duration="{ enter: 700, leave: 1000 }"
enter-class="custom-enter"
enter-active-class="custom-enter-active"
@ -39,10 +39,11 @@
</view>
</template>
<script lang="ts" setup>
import type { TransitionName } from '@/uni_modules/wot-design-uni/components/wd-transition/types'
import { ref } from 'vue'
const show = ref<boolean>(false)
const name = ref<any>('')
const name = ref<TransitionName>()
const customShow = ref<boolean>(false)
function fade() {
transition('fade')
@ -74,13 +75,16 @@ function slideRight() {
function zoomIn() {
transition('zoom-in')
}
function zoomOut() {
transition('zoom-out')
}
function custom() {
customShow.value = true
setTimeout(() => {
customShow.value = false
}, 1200)
}
function transition(transition: string) {
function transition(transition: TransitionName) {
name.value = transition
show.value = true
setTimeout(() => {

View File

@ -103,6 +103,23 @@
}
}
/* 定义状态m */
@mixin mdeep($modifier...) {
$selectors: "";
@each $item in $modifier {
$selectors: #{$selectors + & + $modifierSeparator + $item + ","};
}
@at-root {
:deep() {
#{$selectors} {
@content;
}
}
}
}
/* 对于需要需要嵌套在 m 底下的 e调用这个混合宏一般在切换整个组件的状态如切换颜色的时候 */
@mixin me($element...) {
$selector: &;

View File

@ -1,23 +1,33 @@
@import './../common/abstracts/_mixin.scss';
@import './../common/abstracts/variable.scss';
@import '../wd-overlay/index.scss';
.wot-theme-dark {
@include b(popup) {
background: $-dark-background2;
@include b(popup-wrapper) {
:deep() {
.wd-popup {
background: $-dark-background2;
}
@include e(close) {
color: $-dark-color;
.wd-popup__close {
color: $-dark-color;
}
}
}
}
@include b(popup) {
position: fixed;
max-height: 100%;
overflow-y: auto;
background: #fff;
@include b(popup-wrapper) {
:deep() {
.wd-popup {
position: fixed;
max-height: 100%;
overflow-y: auto;
background: #fff;
}
}
}
@include b(popup) {
@include edeep(close) {
position: absolute;
top: 10px;
@ -27,86 +37,48 @@
transform: rotate(-45deg);
}
@include m(center) {
@include mdeep(center) {
left: 50%;
top: 50%;
transform: translate3d(-50%, -50%, 0);
transform-origin: 0% 0%;
&.wd-zoom-in-enter,
&.wd-zoom-in-leave-to {
transform: scale(0.8) translate3d(-50%, -50%, 0) !important;
}
@include when(deep) {
&.wd-zoom-in-enter,
&.wd-zoom-in-leave-to {
transform: scale(0.1) translate3d(-50%, -50%, 0) !important;
}
}
}
@include m(left) {
@include mdeep(left) {
top: 0;
bottom: 0;
left: 0;
}
@include m(right) {
@include mdeep(right) {
top: 0;
right: 0;
bottom: 0;
}
@include m(top) {
@include mdeep(top) {
top: 0;
left: 0;
right: 0;
}
@include m(bottom) {
@include mdeep(bottom) {
right: 0;
bottom: 0;
left: 0;
}
}
.wd-center-enter-active,
.wd-center-leave-active {
transition-property: opacity;
}
.wd-center-enter,
.wd-center-leave-to {
opacity: 0;
}
.wd-top-enter-active,
.wd-top-leave-active,
.wd-bottom-enter-active,
.wd-bottom-leave-active,
.wd-left-enter-active,
.wd-left-leave-active,
.wd-right-enter-active,
.wd-right-enter-active {
transition-property: transform;
}
.wd-top-enter,
.wd-top-leave-to {
transform: translate3d(0, -100%, 0);
}
.wd-bottom-enter,
.wd-bottom-leave-to {
transform: translate3d(0, 100%, 0);
}
.wd-left-enter,
.wd-left-leave-to {
transform: translate3d(-100%, 0, 0);
}
.wd-right-enter,
.wd-right-leave-to {
transform: translate3d(100%, 0, 0);
}
.wd-zoom-in-enter-active,
.wd-zoom-in-leave-active {
transition-property: opacity, transform;
transform-origin: center center;
}
.wd-zoom-in-enter,
.wd-zoom-in-leave-to {
opacity: 0;
transform: translate3d(-50%, -50%, 0) scale(0.7);
}

View File

@ -1,33 +1,49 @@
/*
* @Author: weisheng
* @Date: 2024-03-18 11:22:03
* @LastEditTime: 2024-03-18 15:29:43
* @LastEditTime: 2024-11-08 12:55:58
* @LastEditors: weisheng
* @Description:
* @FilePath: \wot-design-uni\src\uni_modules\wot-design-uni\components\wd-popup\types.ts
*
*/
import type { PropType } from 'vue'
import { baseProps, makeBooleanProp, makeNumberProp, makeStringProp } from '../common/props'
import type { TransitionName } from '../wd-transition/types'
export type PopupType = 'center' | 'top' | 'right' | 'bottom' | 'left'
export const popupProps = {
...baseProps,
transition: String,
/**
* wd-transition name
* string
* fade / fade-up / fade-down / fade-left / fade-right / slide-up / slide-down / slide-left / slide-right / zoom-in
*/
transition: String as PropType<TransitionName>,
/**
*
* boolean
* false
*/
closable: makeBooleanProp(false),
/**
*
* string
* center
* center / top / right / bottom / left
*/
position: makeStringProp<PopupType>('center'),
/**
*
* boolean
* true
*/
closeOnClickModal: makeBooleanProp(true),
/**
*
* number | boolean
* 300
*/
duration: {
type: [Number, Boolean],
@ -35,22 +51,32 @@ export const popupProps = {
},
/**
*
* boolean
* true
*/
modal: makeBooleanProp(true),
/**
*
* number
* 10
*/
zIndex: makeNumberProp(10),
/**
* display: none)
* boolean
* true
*/
hideWhenClose: makeBooleanProp(true),
/**
*
* string
* ''
*/
modalStyle: makeStringProp(''),
/**
* iphone X
* boolean
* false
*/
safeAreaInsetBottom: makeBooleanProp(false),
/**
@ -59,10 +85,14 @@ export const popupProps = {
modelValue: makeBooleanProp(false),
/**
*
* boolean
* true
*/
lazyRender: makeBooleanProp(true),
/**
*
* boolean
* true
*/
lockScroll: makeBooleanProp(true)
}

View File

@ -1,17 +1,33 @@
<template>
<wd-overlay
v-if="modal"
:show="modelValue"
:z-index="zIndex"
:lock-scroll="lockScroll"
:duration="duration"
:custom-style="modalStyle"
@click="handleClickModal"
@touchmove="noop"
/>
<view v-if="!lazyRender || inited" :class="rootClass" :style="style" @transitionend="onTransitionEnd">
<slot />
<wd-icon v-if="closable" custom-class="wd-popup__close" name="add" @click="close" />
<view class="wd-popup-wrapper">
<wd-overlay
v-if="modal"
:show="modelValue"
:z-index="zIndex"
:lock-scroll="lockScroll"
:duration="duration"
:custom-style="modalStyle"
@click="handleClickModal"
@touchmove="noop"
/>
<wd-transition
:lazy-render="lazyRender"
:custom-class="rootClass"
:custom-style="style"
:duration="duration"
:show="modelValue"
:name="transitionName"
:destroy="hideWhenClose"
@before-enter="emit('before-enter')"
@enter="emit('enter')"
@after-enter="emit('after-enter')"
@before-leave="emit('before-leave')"
@leave="emit('leave')"
@after-leave="emit('after-leave')"
>
<slot />
<wd-icon v-if="closable" custom-class="wd-popup__close" name="add" @click="close" />
</wd-transition>
</view>
</template>
@ -29,9 +45,9 @@ export default {
<script lang="ts" setup>
import wdIcon from '../wd-icon/wd-icon.vue'
import wdOverlay from '../wd-overlay/wd-overlay.vue'
import { computed, onBeforeMount, ref, watch } from 'vue'
import { isObj, requestAnimationFrame } from '../common/util'
import { computed, onBeforeMount, ref } from 'vue'
import { popupProps } from './types'
import type { TransitionName } from '../wd-transition/types'
const props = defineProps(popupProps)
const emit = defineEmits([
@ -46,53 +62,42 @@ const emit = defineEmits([
'close'
])
const getClassNames = (name?: string) => {
if (!name) {
return {
enter: 'enter-class enter-active-class',
'enter-to': 'enter-to-class enter-active-class',
leave: 'leave-class leave-active-class',
'leave-to': 'leave-to-class leave-active-class'
}
/**
* 弹出位置
*/
const transitionName = computed<TransitionName | TransitionName[]>(() => {
if (props.transition) {
return props.transition
}
return {
enter: `wd-${name}-enter wd-${name}-enter-active`,
'enter-to': `wd-${name}-enter-to wd-${name}-enter-active`,
leave: `wd-${name}-leave wd-${name}-leave-active`,
'leave-to': `wd-${name}-leave-to wd-${name}-leave-active`
if (props.position === 'center') {
return ['zoom-in', 'fade']
}
}
//
const inited = ref<boolean>(false)
//
const display = ref<boolean>(false)
//
const status = ref<string>('')
//
const transitionEnded = ref<boolean>(false)
//
const currentDuration = ref<number>(300)
//
const classes = ref<string>('')
if (props.position === 'left') {
return 'slide-left'
}
if (props.position === 'right') {
return 'slide-right'
}
if (props.position === 'bottom') {
return 'slide-up'
}
if (props.position === 'top') {
return 'slide-down'
}
return 'slide-up'
})
const safeBottom = ref<number>(0)
const name = ref<string>('') //
const style = computed(() => {
return `z-index: ${props.zIndex}; padding-bottom: ${safeBottom.value}px; -webkit-transition-duration: ${
currentDuration.value
}ms; transition-duration: ${currentDuration.value}ms; ${display.value || !props.hideWhenClose ? '' : 'display: none;'} ${props.customStyle}`
return `z-index:${props.zIndex}; padding-bottom: ${safeBottom.value}px;${props.customStyle}`
})
const rootClass = computed(() => {
return `wd-popup wd-popup--${props.position} ${props.customClass || ''} ${classes.value || ''}`
return `wd-popup wd-popup--${props.position} ${!props.transition && props.position === 'center' ? 'is-deep' : ''} ${props.customClass || ''}`
})
onBeforeMount(() => {
observerTransition()
if (props.safeAreaInsetBottom) {
const { safeArea, screenHeight, safeAreaInsets } = uni.getSystemInfoSync()
@ -107,95 +112,8 @@ onBeforeMount(() => {
safeBottom.value = 0
}
}
if (props.modelValue) {
enter()
}
})
watch(
() => props.modelValue,
(newVal) => {
observermodelValue(newVal)
},
{ deep: true, immediate: true }
)
watch(
[() => props.position, () => props.transition],
() => {
observerTransition()
},
{ deep: true, immediate: true }
)
function observermodelValue(value: boolean) {
value ? enter() : leave()
}
function enter() {
const classNames = getClassNames(props.transition || props.position)
const duration = props.transition === 'none' ? 0 : isObj(props.duration) ? (props.duration as any).enter : props.duration
status.value = 'enter'
emit('before-enter')
requestAnimationFrame(() => {
emit('enter')
classes.value = classNames.enter
currentDuration.value = duration
requestAnimationFrame(() => {
inited.value = true
display.value = true
requestAnimationFrame(() => {
transitionEnded.value = false
classes.value = classNames['enter-to']
})
})
})
}
function leave() {
if (!display.value) return
const classNames = getClassNames(props.transition || props.position)
const duration = props.transition === 'none' ? 0 : isObj(props.duration) ? (props.duration as any).leave : props.duration
status.value = 'leave'
emit('before-leave')
requestAnimationFrame(() => {
emit('leave')
classes.value = classNames.leave
currentDuration.value = duration
requestAnimationFrame(() => {
transitionEnded.value = false
const timer = setTimeout(() => {
onTransitionEnd()
clearTimeout(timer)
}, currentDuration.value)
classes.value = classNames['leave-to']
})
})
}
function onTransitionEnd() {
if (transitionEnded.value) return
transitionEnded.value = true
if (status.value === 'leave') {
//
emit('after-leave')
} else if (status.value === 'enter') {
//
emit('after-enter')
}
if (!props.modelValue && display.value) {
display.value = false
}
}
function observerTransition() {
const { transition, position } = props
name.value = transition || position
}
function handleClickModal() {
emit('click-modal')
if (props.closeOnClickModal) {

View File

@ -2,25 +2,14 @@
transition-timing-function: ease;
}
.wd-fade-enter-active,
.wd-fade-leave-active {
transition-property: opacity;
}
.wd-fade-enter,
.wd-fade-leave-to {
opacity: 0;
}
.wd-fade-up-enter-active,
.wd-fade-up-leave-active,
.wd-fade-down-enter-active,
.wd-fade-down-leave-active,
.wd-fade-left-enter-active,
.wd-fade-left-leave-active,
.wd-fade-right-enter-active,
.wd-fade-right-enter-active {
transition-property: opacity, transform;
.wd-fade-enter-active,
.wd-fade-leave-active {
transition-property: opacity;
}
.wd-fade-up-enter,
@ -47,17 +36,6 @@
opacity: 0;
}
.wd-slide-up-enter-active,
.wd-slide-up-leave-active,
.wd-slide-down-enter-active,
.wd-slide-down-leave-active,
.wd-slide-left-enter-active,
.wd-slide-left-leave-active,
.wd-slide-right-enter-active,
.wd-slide-right-enter-active {
transition-property: transform;
}
.wd-slide-up-enter,
.wd-slide-up-leave-to {
transform: translate3d(0, 100%, 0);
@ -78,14 +56,40 @@
transform: translate3d(100%, 0, 0);
}
.wd-zoom-in-enter-active,
.wd-zoom-in-leave-active {
transition-property: opacity, transform;
transform-origin: center center;
}
.wd-zoom-in-enter,
.wd-zoom-in-leave-to {
opacity: 0;
transform: scale(0.7);
}
transform: scale(0.8);
}
.wd-zoom-out-enter,
.wd-zoom-out-leave-to {
transform: scale(1.2);
opacity: 0;
}
.wd-zoom-in-enter-active,
.wd-zoom-in-leave-active,
.wd-zoom-out-enter-active,
.wd-zoom-out-leave-active,
.wd-fade-up-enter-active,
.wd-fade-up-leave-active,
.wd-fade-down-enter-active,
.wd-fade-down-leave-active,
.wd-fade-left-enter-active,
.wd-fade-left-leave-active,
.wd-fade-right-enter-active,
.wd-fade-right-leave-active {
transition-property: opacity, transform;
}
.wd-slide-up-enter-active,
.wd-slide-up-leave-active,
.wd-slide-down-enter-active,
.wd-slide-down-leave-active,
.wd-slide-left-enter-active,
.wd-slide-left-leave-active,
.wd-slide-right-enter-active,
.wd-slide-right-leave-active {
transition-property: transform;
}

View File

@ -1,5 +1,14 @@
/*
* @Author: weisheng
* @Date: 2024-09-01 15:42:04
* @LastEditTime: 2024-11-06 23:50:08
* @LastEditors: weisheng
* @Description:
* @FilePath: \wot-design-uni\src\uni_modules\wot-design-uni\components\wd-transition\types.ts
*
*/
import type { ExtractPropTypes, PropType } from 'vue'
import { baseProps, makeBooleanProp, makeNumberProp, makeStringProp } from '../common/props'
import { baseProps, makeBooleanProp, makeStringProp } from '../common/props'
export type TransitionName =
| 'fade'
@ -33,22 +42,25 @@ export const transitionProps = {
type: [Object, Number, Boolean] as PropType<Record<string, number> | number | boolean>,
default: 300
},
/**
*
* boolean
* false
*/
lazyRender: makeBooleanProp(false),
/**
*
* string
* fade / fade-up / fade-down / fade-left / fade-right / slide-up / slide-down / slide-left / slide-right / zoom-in
* 'fade'
*/
name: makeStringProp<TransitionName | ''>('fade'),
name: [String, Array] as PropType<TransitionName | TransitionName[]>,
/**
*
* display: none)
* boolean
* true
* false
*/
lazyRender: makeBooleanProp(true),
destroy: makeBooleanProp(true),
/**
*
* string

View File

@ -1,5 +1,5 @@
<template>
<view v-if="inited" :class="rootClass" :style="style" @transitionend="onTransitionEnd" @click="handleClick">
<view v-if="!lazyRender || inited" :class="rootClass" :style="style" @transitionend="onTransitionEnd" @click="handleClick">
<slot />
</view>
</template>
@ -18,24 +18,33 @@ export default {
<script lang="ts" setup>
import { computed, onBeforeMount, ref, watch } from 'vue'
import { isObj, isPromise, requestAnimationFrame } from '../common/util'
import { transitionProps } from './types'
import { transitionProps, type TransitionName } from './types'
import { AbortablePromise } from '../common/AbortablePromise'
const getClassNames = (name?: string) => {
if (!name) {
return {
enter: `${props.enterClass} ${props.enterActiveClass}`,
'enter-to': `${props.enterToClass} ${props.enterActiveClass}`,
leave: `${props.leaveClass} ${props.leaveActiveClass}`,
'leave-to': `${props.leaveToClass} ${props.leaveActiveClass}`
}
}
const getClassNames = (name?: TransitionName | TransitionName[]) => {
let enter: string = `${props.enterClass} ${props.enterActiveClass}`
let enterTo: string = `${props.enterToClass} ${props.enterActiveClass}`
let leave: string = `${props.leaveClass} ${props.leaveActiveClass}`
let leaveTo: string = `${props.leaveToClass} ${props.leaveActiveClass}`
if (Array.isArray(name)) {
for (let index = 0; index < name.length; index++) {
enter = `wd-${name[index]}-enter wd-${name[index]}-enter-active ${enter}`
enterTo = `wd-${name[index]}-enter-to wd-${name[index]}-enter-active ${enterTo}`
leave = `wd-${name[index]}-leave wd-${name[index]}-leave-active ${leave}`
leaveTo = `wd-${name[index]}-leave-to wd-${name[index]}-leave-active ${leaveTo}`
}
} else if (name) {
enter = `wd-${name}-enter wd-${name}-enter-active ${enter}`
enterTo = `wd-${name}-enter-to wd-${name}-enter-active ${enterTo}`
leave = `wd-${name}-leave wd-${name}-leave-active ${leave}`
leaveTo = `wd-${name}-leave-to wd-${name}-leave-active ${leaveTo}`
}
return {
enter: `wd-${name}-enter wd-${name}-enter-active`,
'enter-to': `wd-${name}-enter-to wd-${name}-enter-active`,
leave: `wd-${name}-leave wd-${name}-leave-active`,
'leave-to': `wd-${name}-leave-to wd-${name}-leave-active`
enter: enter,
'enter-to': enterTo,
leave: leave,
'leave-to': leaveTo
}
}
@ -65,7 +74,7 @@ const leaveLifeCyclePromises = ref<AbortablePromise<unknown> | null>(null)
const style = computed(() => {
return `-webkit-transition-duration:${currentDuration.value}ms;transition-duration:${currentDuration.value}ms;${
display.value ? '' : 'display: none;'
display.value || !props.destroy ? '' : 'display: none;'
}${props.customStyle}`
})