Popup.vue 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. <template>
  2. <view
  3. :class="[
  4. 'nana-popup',
  5. position,
  6. showAnimState ? 'show' : '',
  7. mask ? 'stop' : '',
  8. mask ? (show2 ? 'show2' : '') : 'no-mask',
  9. ]"
  10. :style="{
  11. ...selectStyleType(position, 'bottom', {
  12. center: {
  13. justifyContent: 'center',
  14. alignItems: 'center',
  15. },
  16. top: {
  17. justifyContent: 'flex-start',
  18. alignItems: 'center',
  19. },
  20. bottom: {
  21. justifyContent: 'flex-end',
  22. alignItems: 'center',
  23. },
  24. left: {
  25. alignItems: 'flex-start',
  26. justifyContent: 'center',
  27. },
  28. right: {
  29. alignItems: 'flex-end',
  30. justifyContent: 'center',
  31. },
  32. }),
  33. top: inset[0] ? `${themeContext.resolveThemeSize(inset[0])}` : undefined,
  34. right: inset[1] ? `${themeContext.resolveThemeSize(inset[1])}` : undefined,
  35. bottom: inset[2] ? `${themeContext.resolveThemeSize(inset[2])}` : undefined,
  36. left: inset[3] ? `${themeContext.resolveThemeSize(inset[3])}` : undefined,
  37. zIndex: popupZIndex,
  38. }"
  39. >
  40. <view
  41. class="nana-popup-mask"
  42. :style="{
  43. backgroundColor: mask ? themeContext.resolveThemeColor(maskColor) : '',
  44. transitionDuration: `${duration}ms`,
  45. zIndex: popupZIndex + 1,
  46. }"
  47. @mousedown.stop="handleClose"
  48. @touchstart.stop="handleClose"
  49. @click.stop="handleClose"
  50. >
  51. </view>
  52. <view
  53. v-if="show2"
  54. :class="[
  55. 'nana-popup-content',
  56. noTransition ? 'no-transition' : '',
  57. position,
  58. ] "
  59. :style="{
  60. ...selectStyleType(position, 'bottom', {
  61. center: {
  62. flexDirection: 'row',
  63. borderRadius: radius,
  64. },
  65. top: {
  66. borderBottomLeftRadius: radius,
  67. borderBottomRightRadius: radius,
  68. width: '100%',
  69. height: themeContext.resolveThemeSize(props.size),
  70. },
  71. bottom: {
  72. borderTopLeftRadius: radius,
  73. borderTopRightRadius: radius,
  74. width: '100%',
  75. height: themeContext.resolveThemeSize(props.size),
  76. },
  77. left: {
  78. borderTopRightRadius: radius,
  79. borderBottomRightRadius: radius,
  80. height: '100%',
  81. width: themeContext.resolveThemeSize(props.size),
  82. },
  83. right: {
  84. borderTopLeftRadius: radius,
  85. borderBottomLeftRadius: radius,
  86. height: '100%',
  87. width: themeContext.resolveThemeSize(props.size),
  88. },
  89. }),
  90. zIndex: popupZIndex + 2,
  91. backgroundColor: themeContext.resolveThemeColor(backgroundColor),
  92. margin: `${themeContext.resolveThemeSize(margin[0])} ${themeContext.resolveThemeSize(margin[1])} ${themeContext.resolveThemeSize(margin[2])} ${themeContext.resolveThemeSize(margin[3])}`,
  93. ...innerStyle,
  94. }"
  95. @click.stop
  96. >
  97. <SafeAreaPadding
  98. :top="safeArea && (position === 'top' || position === 'left' || position === 'right')"
  99. :bottom="safeArea && (position === 'bottom' || position === 'left' || position === 'right')"
  100. >
  101. <PopupTitle
  102. v-if="position !== 'top'"
  103. :closeable="closeable"
  104. :closeIcon="closeIcon"
  105. :closeIconSize="closeIconSize"
  106. :closeIconPosition="closeIconPosition"
  107. :top="true"
  108. @close="doClose"
  109. />
  110. <slot />
  111. <PopupTitle
  112. v-if="position === 'top'"
  113. :closeable="closeable"
  114. :closeIcon="closeIcon"
  115. :closeIconSize="closeIconSize"
  116. :closeIconPosition="closeIconPosition"
  117. @close="doClose"
  118. />
  119. </SafeAreaPadding>
  120. </view>
  121. </view>
  122. </template>
  123. <script setup lang="ts">
  124. import { computed, ref, watch } from 'vue';
  125. import { useTheme, type ViewStyle } from '../theme/ThemeDefine';
  126. import { selectStyleType } from '../theme/ThemeTools';
  127. import { SimpleDelay } from '@imengyu/imengyu-utils';
  128. import PopupTitle from './PopupTitle.vue';
  129. import SafeAreaPadding from '../layout/space/SafeAreaPadding.vue';
  130. import { getCurrentZIndex } from './CommonRoot';
  131. /**
  132. * Popup 的显示位置
  133. */
  134. export type PopupPosition = 'center'|'top'|'bottom'|'left'|'right';
  135. /**
  136. * Popup 关闭按钮显示位置
  137. */
  138. export type PopupCloseButtonPosition = 'left'|'right';
  139. /**
  140. * Popup 组件属性
  141. */
  142. export interface PopupProps {
  143. /**
  144. * 是否显示当前弹窗
  145. */
  146. show: boolean;
  147. /**
  148. * 弹出层圆角
  149. */
  150. round?: boolean;
  151. /**
  152. * 是否可以点击遮罩关闭当前弹出层,同时会显示一个关闭按扭,默认否
  153. */
  154. closeable?: boolean;
  155. /**
  156. * 关闭按扭,如果设置false则不显示
  157. * @default 'close'
  158. */
  159. closeIcon?: string|false;
  160. /**
  161. * 关闭按扭大小
  162. * @default 40
  163. */
  164. closeIconSize?: number;
  165. /**
  166. * 关闭按扭位置
  167. */
  168. closeIconPosition?: PopupCloseButtonPosition,
  169. /**
  170. * 指定当前弹出层弹出位置
  171. */
  172. position?: PopupPosition,
  173. /**
  174. * 遮罩的颜色
  175. */
  176. maskColor?: string,
  177. /**
  178. * 是否显示遮罩,默认是
  179. * @default true
  180. */
  181. mask?: boolean,
  182. /**
  183. * 对话框偏移边距,默认为0,0,0,0
  184. * @default [0,0,0,0]
  185. */
  186. margin?: number[],
  187. /**
  188. * 强制设置整体边距(包括遮罩层),默认为[undefined,undefined,undefined,undefined]
  189. * @default [undefined,undefined,undefined,undefined]
  190. */
  191. inset?: (number|string|undefined)[],
  192. /**
  193. * 弹出层背景颜色,默认是 白色
  194. * @default white
  195. */
  196. backgroundColor?: string;
  197. /**
  198. * 从侧边弹出时,是否自动设置安全区,默认是
  199. * @default true
  200. */
  201. safeArea?: boolean,
  202. /**
  203. * 指定当前弹出层的特殊样式
  204. */
  205. innerStyle?: ViewStyle,
  206. /**
  207. * 指定弹出层动画时长,毫秒
  208. * @default 230
  209. */
  210. duration?: number,
  211. /**
  212. * 指定弹出层从侧边弹出的高度,如果是横向弹出,则设置宽度,默认是30%, 设置 auto 让大小自动根据内容调整
  213. * @default '30%'
  214. */
  215. size?: string|number;
  216. /**
  217. * 指定弹出层的 z-index 层级,默认是 1010
  218. * @default 0
  219. */
  220. zIndex?: number;
  221. /**
  222. * 是否禁用动画,默认是 false
  223. * @default false
  224. */
  225. noTransition?: boolean;
  226. }
  227. const emit = defineEmits([ 'update:show', 'close', 'closeAnimFinished' ])
  228. const props = withDefaults(defineProps<PopupProps>(), {
  229. closeIcon: 'close',
  230. closeIconSize: 40,
  231. closeIconPosition: 'right',
  232. position: 'center',
  233. maskColor: 'background.mask',
  234. mask: true,
  235. margin: () => [0,0,0,0],
  236. inset: () => [undefined,undefined,undefined,undefined],
  237. backgroundColor: 'white',
  238. safeArea: true,
  239. duration: 230,
  240. zIndex: 0,
  241. size: '30%',
  242. });
  243. function handleClick(e: Event) {
  244. e.stopPropagation();
  245. }
  246. function handleClose(e: Event) {
  247. e.stopPropagation();
  248. if (props.closeable)
  249. doClose();
  250. }
  251. function doClose() {
  252. emit('update:show', false);
  253. emit('close');
  254. }
  255. const themeContext = useTheme();
  256. const show2 = ref(false);
  257. const showAnimState = ref(false);
  258. const radius = computed(() => props.round ? themeContext.resolveThemeSize('PopupRadius', 30) : 0);
  259. const popupZIndex = ref(props.zIndex);
  260. let lateStopTimer : SimpleDelay|undefined;
  261. watch(() => props.show, (v) => {
  262. show2.value = true;
  263. if (!v) {
  264. showAnimState.value = false;
  265. if (lateStopTimer)
  266. lateStopTimer.stop();
  267. lateStopTimer = new SimpleDelay(undefined, () => {
  268. lateStopTimer = undefined;
  269. show2.value = false;
  270. }, props.duration)
  271. lateStopTimer.start();
  272. } else {
  273. popupZIndex.value = props.zIndex > 0 ? props.zIndex : getCurrentZIndex();
  274. if (lateStopTimer)
  275. lateStopTimer.stop();
  276. lateStopTimer = new SimpleDelay(undefined, () => {
  277. lateStopTimer = undefined;
  278. showAnimState.value = true
  279. }, 20)
  280. lateStopTimer.start();
  281. }
  282. }, { immediate: true });
  283. </script>
  284. <style lang="scss">
  285. .nana-popup {
  286. position: fixed;
  287. top: 0;
  288. left: 0;
  289. right: 0;
  290. bottom: 0;
  291. display: flex;
  292. flex-direction: column;
  293. pointer-events: none;
  294. overflow: hidden;
  295. &.show2 {
  296. pointer-events: auto;
  297. > .nana-popup-mask {
  298. pointer-events: auto;
  299. }
  300. }
  301. &.no-mask {
  302. > .nana-popup-mask {
  303. pointer-events: none;
  304. }
  305. }
  306. // &.stop {
  307. // }
  308. .nana-popup-mask {
  309. position: absolute;
  310. top: 0;
  311. left: 0;
  312. right: 0;
  313. bottom: 0;
  314. pointer-events: none;
  315. opacity: 0;
  316. transition: opacity ease-in-out 0.3s;
  317. }
  318. .nana-popup-content {
  319. position: relative;
  320. overflow: hidden;
  321. transition: all ease-in-out 0.3s;
  322. opacity: 0.3;
  323. pointer-events: auto;
  324. &.no-transition {
  325. transition: none;
  326. }
  327. &.center {
  328. transform: translateY(-10px);
  329. }
  330. &.top {
  331. transform: translateY(-100vh);
  332. }
  333. &.bottom {
  334. transform: translateY(200vh);
  335. }
  336. &.left {
  337. transform: translateX(-750rpx);
  338. }
  339. &.right {
  340. transform: translateX(750rpx);
  341. }
  342. }
  343. &.show {
  344. > .nana-popup-mask {
  345. opacity: 1;
  346. }
  347. > .nana-popup-content {
  348. opacity: 1;
  349. transform: translateX(0) translateY(0);
  350. }
  351. }
  352. }
  353. </style>