Popup.vue 8.5 KB

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