This commit is contained in:
2026-01-05 12:47:14 +08:00
commit 1fc846fae3
1614 changed files with 162035 additions and 0 deletions

View File

@@ -0,0 +1,147 @@
@import "../overlay/index";
.nut-theme-dark {
.nut-popup {
background: $dark-background2;
&__close-icon {
color: $dark-color;
}
}
}
.nut-popup-slide {
&-center-enter-active,
&-center-leave-active {
transition-timing-function: ease;
transition-property: opacity;
}
&-center-enter-from,
&-center-leave-to {
opacity: 0;
}
&-top-enter-from,
&-top-leave-active {
transform: translate(0, -100%);
}
&-right-enter-from,
&-right-leave-active {
transform: translate(100%, 0);
}
&-bottom-enter-from,
&-bottom-leave-active {
transform: translate(0, 100%);
}
&-left-enter-from,
&-left-leave-active {
transform: translate(-100%, 0);
}
}
.nut-popup--center {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
&.round {
border-radius: $popup-border-radius;
}
}
.nut-popup--bottom {
bottom: 0;
left: 0;
width: 100%;
&.round {
border-radius: $popup-border-radius $popup-border-radius 0 0;
}
&--safebottom {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
}
.nut-popup--right {
top: 0;
right: 0;
&.round {
border-radius: $popup-border-radius 0 0 $popup-border-radius;
}
}
.nut-popup--left {
top: 0;
left: 0;
&.round {
border-radius: 0 $popup-border-radius $popup-border-radius 0;
}
}
.nut-popup--top {
top: 0;
left: 0;
width: 100%;
&.round {
border-radius: 0 0 $popup-border-radius $popup-border-radius;
}
&--safetop {
padding-top: var(--status-bar-height);
padding-top: constant(safe-area-inset-top);
padding-top: env(safe-area-inset-top);
}
}
.nut-popup {
position: fixed;
max-height: 100%;
overflow-y: auto;
background-color: $white;
-webkit-overflow-scrolling: touch;
&__close-icon {
position: absolute !important;
z-index: 1;
width: 30px;
height: 30px;
font-size: 18px;
line-height: 30px;
color: #969799;
text-align: center;
cursor: pointer;
&:active {
opacity: 0.7;
}
&--top-left {
top: $popup-close-icon-margin;
left: $popup-close-icon-margin;
}
&--top-right {
top: $popup-close-icon-margin;
right: $popup-close-icon-margin;
}
&--bottom-left {
bottom: $popup-close-icon-margin;
left: $popup-close-icon-margin;
}
&--bottom-right {
right: $popup-close-icon-margin;
bottom: $popup-close-icon-margin;
}
}
}

View File

@@ -0,0 +1,2 @@
export * from './popup'
export * from './use-popup'

View File

@@ -0,0 +1,79 @@
import type { ExtractPropTypes, PropType } from 'vue'
import { CLOSE_EVENT, CLOSED_EVENT, OPEN_EVENT, OPENED_EVENT, UPDATE_VISIBLE_EVENT } from '../_constants'
import type { Position } from '../_constants/types'
import { commonProps, makeStringProp, truthProp } from '../_utils'
import { overlayProps } from '../overlay/overlay'
import type { NutAnimationName } from '../transition/types'
export const popupProps = {
...overlayProps,
...commonProps,
/**
* @description 弹出位置top,bottom,left,right,center
*/
position: makeStringProp<Position>('center'),
/**
* @description 动画名
*/
transition: {
type: String as PropType<NutAnimationName>,
default: '',
},
/**
* @description 自定义弹框类名
*/
popClass: makeStringProp(''),
/**
* @description 是否显示圆角
*/
round: Boolean,
/**
* @description 是否显示关闭按钮
*/
closeable: Boolean,
/**
* @description 关闭按钮图标
*/
closeIcon: makeStringProp('close'),
/**
* @description 关闭按钮位置top-left,top-right,bottom-left,bottom-right
*/
closeIconPosition: makeStringProp<'top-right' | 'bottom-right' | 'bottom-left' | 'top-left'>('top-right'),
/**
* @description 是否保留弹层关闭后的内容
*/
destroyOnClose: truthProp,
/**
* @description 是否显示遮罩层
*/
overlay: truthProp,
/**
* @description 是否开启 iPhone 系列全面屏底部安全区适配,仅当 `position` 为 `bottom` 时有效
*/
safeAreaInsetBottom: Boolean,
/**
* @description 是否开启 iPhone 顶部安全区适配
*/
safeAreaInsetTop: truthProp,
}
export type PopupProps = ExtractPropTypes<typeof popupProps>
/* eslint-disable unused-imports/no-unused-vars */
export const popupEmits = {
[UPDATE_VISIBLE_EVENT]: (value: boolean) => true,
'click-pop': (event: any) => true,
'click-close-icon': () => true,
'click-overlay': () => true,
[OPEN_EVENT]: () => true,
[OPENED_EVENT]: () => true,
[CLOSE_EVENT]: () => true,
[CLOSED_EVENT]: () => true,
/**
* @deprecated
*/
'opend': () => true,
}
/* eslint-enable unused-imports/no-unused-vars */
export type PopupEmits = typeof popupEmits

View File

@@ -0,0 +1,87 @@
<script lang="ts" setup>
import { computed, defineComponent } from 'vue'
import { PREFIX } from '../_constants'
import NutIcon from '../icon/icon.vue'
import NutOverlay from '../overlay/overlay.vue'
import NutTransition from '../transition/transition.vue'
import { popupEmits, popupProps } from './popup'
import { usePopup } from './use-popup'
const props = defineProps(popupProps)
const emit = defineEmits(popupEmits)
const {
classes,
popStyle,
innerIndex,
showSlot,
transitionName,
onClick,
onClickCloseIcon,
onClickOverlay,
onOpened,
onClosed,
} = usePopup(props, emit)
const innerDuration = computed(() => {
return Number(props.duration)
})
</script>
<script lang="ts">
const componentName = `${PREFIX}-popup`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<NutOverlay
v-if="props.overlay"
:overlay-class="props.overlayClass"
:overlay-style="props.overlayStyle"
:visible="props.visible"
:z-index="innerIndex"
:duration="innerDuration"
:lock-scroll="props.lockScroll"
:close-on-click-overlay="props.closeOnClickOverlay"
:destroy-on-close="props.destroyOnClose"
@click="onClickOverlay"
/>
<NutTransition
:custom-class="classes"
:custom-style="popStyle"
:name="transitionName"
:show="props.visible"
:duration="innerDuration"
:destroy-on-close="props.destroyOnClose"
@after-enter="onOpened"
@after-leave="onClosed"
@click="onClick"
>
<slot v-if="showSlot" />
<view
v-if="props.closeable"
class="nut-popup__close-icon"
:class="`nut-popup__close-icon--${props.closeIconPosition}`"
@click="onClickCloseIcon"
>
<slot name="closeIcon">
<NutIcon name="close" height="12px" />
</slot>
</view>
</NutTransition>
</template>
<style lang="scss">
@import "./index";
</style>

View File

@@ -0,0 +1,132 @@
import type { SetupContext } from 'vue'
import { computed, onMounted, reactive, toRefs, watch } from 'vue'
import {
animationName,
CLOSE_EVENT,
CLOSED_EVENT,
OPEN_EVENT,
OPENED_EVENT,
PREFIX,
UPDATE_VISIBLE_EVENT,
} from '../_constants'
import { useGlobalZIndex } from '../_hooks'
import { getMainClass, getMainStyle } from '../_utils'
import type { NutAnimationName } from '../transition'
import type { PopupEmits, PopupProps } from './popup'
const componentName = `${PREFIX}-popup`
export function usePopup(props: PopupProps, emit: SetupContext<PopupEmits>['emit']) {
const state = reactive({
innerVisible: false,
innerIndex: props.zIndex,
showSlot: true,
})
const classes = computed(() => {
return getMainClass(props, componentName, {
round: props.round,
[`nut-popup--${props.position}`]: true,
[`nut-popup--${props.position}--safebottom`]: props.position === 'bottom' && props.safeAreaInsetBottom,
[`nut-popup--${props.position}--safetop`]: props.position === 'top' && props.safeAreaInsetTop,
[props.popClass]: true,
})
})
const popStyle = computed(() => {
return getMainStyle(props, {
zIndex: state.innerIndex,
transitionDuration: `${props.duration}ms`,
})
})
const transitionName = computed<NutAnimationName>(() => {
return props.transition ? props.transition : `${animationName[props.position]}`
})
const open = () => {
if (state.innerVisible)
return
state.innerIndex = props.zIndex !== undefined ? props.zIndex : useGlobalZIndex()
state.innerVisible = true
emit(UPDATE_VISIBLE_EVENT, true)
state.showSlot = true
emit(OPEN_EVENT)
}
const close = () => {
if (!state.innerVisible)
return
state.innerVisible = false
emit(UPDATE_VISIBLE_EVENT, false)
emit(CLOSE_EVENT)
}
const onClick = (e: any) => {
emit('click-pop', e)
}
const onClickCloseIcon = (e: any) => {
e.stopPropagation()
emit('click-close-icon')
close()
}
const onClickOverlay = () => {
emit('click-overlay')
if (props.closeOnClickOverlay)
close()
}
const onOpened = () => {
emit(OPENED_EVENT)
emit('opend')
}
const onClosed = () => {
emit(CLOSED_EVENT)
state.showSlot = !props.destroyOnClose
}
const applyVisible = (visible: boolean) => {
if (visible && !state.innerVisible) {
open()
}
if (!visible && state.innerVisible) {
state.innerVisible = false
emit(CLOSE_EVENT)
}
}
watch(() => props.visible, (value) => {
applyVisible(value)
})
onMounted(() => {
applyVisible(props.visible)
})
return {
...toRefs(state),
popStyle,
transitionName,
classes,
onClick,
onClickCloseIcon,
onClickOverlay,
onOpened,
onClosed,
}
}