uniapp-dyckui 4.1.2 → 4.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/assets/{style.BFlsbpSj.css → style.BEkFuBVh.css} +776 -774
  2. package/dist/{index.cjs.js → index.cjs} +841 -839
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/{index.es.js → index.mjs} +841 -839
  5. package/dist/index.mjs.map +1 -0
  6. package/dist/src/components/MyComs/Button/index.vue.d.ts +6 -6
  7. package/dist/src/components/MyComs/Dialog/index.vue.d.ts +6 -6
  8. package/dist/src/components/MyComs/Divider/index.vue.d.ts +5 -5
  9. package/dist/src/components/MyComs/DropdownSelect/dropdownSelect.d.ts +2 -2
  10. package/dist/src/components/MyComs/DropdownSelect/index.vue.d.ts +3 -6
  11. package/dist/src/components/MyComs/DropdownWithBadge/index.vue.d.ts +3 -6
  12. package/dist/src/components/MyComs/FilterDrawer/index.vue.d.ts +3 -6
  13. package/dist/src/components/MyComs/FilterDrawer/useFilterDrawer.d.ts +1 -1
  14. package/dist/src/components/MyComs/InfiniteScroll/index.vue.d.ts +5 -5
  15. package/dist/src/components/MyComs/Popup/index.vue.d.ts +5 -5
  16. package/dist/src/components/MyComs/PullRefresh/index.vue.d.ts +5 -5
  17. package/dist/src/components/MyComs/Swiper/index.vue.d.ts +7 -7
  18. package/dist/src/components/MyComs/Toast/index.vue.d.ts +7 -7
  19. package/dist/src/components/MyComs/index.d.ts +1590 -17
  20. package/package.json +104 -97
  21. package/src/components/MyComs/Dialog/index.ts +3 -2
  22. package/src/components/MyComs/Divider/index.ts +3 -2
  23. package/src/components/MyComs/InfiniteScroll/index.ts +3 -2
  24. package/src/components/MyComs/Popup/index.ts +3 -2
  25. package/src/components/MyComs/Popup/index.vue +837 -835
  26. package/src/components/MyComs/PullRefresh/index.ts +3 -2
  27. package/src/components/MyComs/PullRefresh/index.vue +2 -1
  28. package/src/components/MyComs/Swiper/index.ts +3 -2
  29. package/src/components/MyComs/Swiper/index.vue +246 -245
  30. package/src/components/MyComs/Toast/index.ts +3 -2
  31. package/src/components/MyComs/Toast/index.vue +373 -372
  32. package/src/components/MyComs/index.ts +57 -20
  33. package/dist/index.cjs.js.map +0 -1
  34. package/dist/index.es.js.map +0 -1
@@ -1,835 +1,837 @@
1
- <template>
2
- <view
3
- v-show="showPopup"
4
- class="popup-wrapper"
5
- :class="{
6
- 'modal': mask,
7
- 'non-modal': !mask,
8
- 'closing': isClosing,
9
- }"
10
- @click.self="handleMaskClick"
11
- >
12
- <!-- 遮罩层 -->
13
- <view
14
- v-if="mask"
15
- class="popup-mask"
16
- :style="{ opacity: maskOpacity }"
17
- @click="handleMaskClick"
18
- />
19
-
20
- <!-- 弹窗主体 -->
21
- <view
22
- ref="popupRef"
23
- class="popup-container"
24
- :class="[
25
- `position-${position}`,
26
- `animation-${animation}`,
27
- { closing: isClosing },
28
- ]"
29
- @click.stop
30
- @touchmove.stop
31
- @animationend="handleAnimationEnd"
32
- >
33
- <!-- 自定义头部 -->
34
- <view v-if="$slots.header" class="popup-header">
35
- <slot name="header" />
36
- </view>
37
-
38
- <!-- 自定义标题 -->
39
- <view v-else-if="title" class="popup-header">
40
- <text class="popup-title">{{ title }}</text>
41
- </view>
42
-
43
- <!-- 内容区域 -->
44
- <view class="popup-content">
45
- <slot />
46
- </view>
47
-
48
- <!-- 自定义底部 -->
49
- <view v-if="$slots.footer" class="popup-footer">
50
- <slot name="footer" />
51
- </view>
52
-
53
- <!-- 默认底部按钮 -->
54
- <view v-else-if="showConfirmButton || showCancelButton" class="popup-footer">
55
- <view
56
- v-if="showCancelButton"
57
- class="popup-button popup-button-cancel"
58
- @click="handleCancel"
59
- >
60
- {{ cancelText }}
61
- </view>
62
- <view
63
- v-if="showConfirmButton"
64
- class="popup-button popup-button-confirm"
65
- @click="handleConfirm"
66
- >
67
- {{ confirmText }}
68
- </view>
69
- </view>
70
-
71
- <!-- 关闭按钮 -->
72
- <view
73
- v-if="showCloseButton"
74
- class="popup-close-button"
75
- @click="handleClose"
76
- >
77
- <text class="close-icon">×</text>
78
- </view>
79
- </view>
80
- </view>
81
- </template>
82
-
83
- <script setup lang="ts">
84
- import { onMounted, onUnmounted, ref, watch } from 'vue'
85
-
86
- // Props
87
- interface Props {
88
- // 控制弹窗显示/隐藏(支持v-model)
89
- modelValue?: boolean
90
- // 弹窗标题
91
- title?: string
92
- // 弹窗位置:center, top, bottom, left, right
93
- position?: 'center' | 'top' | 'bottom' | 'left' | 'right'
94
- // 动画效果:fade, scale, slide
95
- animation?: 'fade' | 'scale' | 'slide'
96
- // 是否显示遮罩层
97
- mask?: boolean
98
- // 遮罩层透明度
99
- maskOpacity?: number
100
- // 点击遮罩是否关闭弹窗
101
- closeOnMaskClick?: boolean
102
- // 是否显示关闭按钮
103
- showCloseButton?: boolean
104
- // 是否支持Esc键关闭
105
- closeOnEsc?: boolean
106
- // 是否显示确认按钮
107
- showConfirmButton?: boolean
108
- // 确认按钮文本
109
- confirmText?: string
110
- // 是否显示取消按钮
111
- showCancelButton?: boolean
112
- // 取消按钮文本
113
- cancelText?: string
114
- // 弹窗宽度(百分比或像素)
115
- width?: string | number
116
- // 弹窗高度(百分比或像素)
117
- height?: string | number
118
- // 弹窗最大宽度
119
- maxWidth?: string | number
120
- // 是否禁止背景滚动
121
- lockScroll?: boolean
122
- }
123
-
124
- // 设置默认属性
125
- const props = withDefaults(defineProps<Props>(), {
126
- modelValue: false,
127
- title: '',
128
- position: 'center',
129
- animation: 'slide',
130
- mask: true,
131
- maskOpacity: 0.5,
132
- closeOnMaskClick: true,
133
- showCloseButton: true,
134
- closeOnEsc: true,
135
- showConfirmButton: false,
136
- confirmText: '确定',
137
- showCancelButton: false,
138
- cancelText: '取消',
139
- width: '50%',
140
- height: '100%',
141
- maxWidth: '500px',
142
- lockScroll: true,
143
- })
144
-
145
- // Emits
146
- const emit = defineEmits<{
147
- (e: 'update:modelValue', value: boolean): void
148
- (e: 'open'): void
149
- (e: 'close'): void
150
- (e: 'maskClick'): void
151
- (e: 'confirm'): void
152
- (e: 'cancel'): void
153
- (e: 'closeButtonClick'): void
154
- }>()
155
-
156
- // Refs
157
- const popupRef = ref<any>(null)
158
- const showPopup = ref(props.modelValue)
159
- const isClosing = ref(false)
160
- const lastScrollY = ref(0)
161
-
162
- // Watchers
163
- // 监听modelValue变化
164
- watch(() => props.modelValue, (newVal) => {
165
- showPopup.value = newVal
166
- if (newVal) {
167
- onOpen()
168
- }
169
- else {
170
- onClose()
171
- }
172
- })
173
-
174
- // 监听showPopup变化
175
- watch(() => showPopup.value, (newVal) => {
176
- // 同步到v-model
177
- emit('update:modelValue', newVal)
178
- if (newVal) {
179
- onOpen()
180
- }
181
- else {
182
- onClose()
183
- }
184
- })
185
-
186
- // Methods
187
- // 弹窗打开时
188
- function onOpen() {
189
- // 锁定滚动
190
- if (props.lockScroll) {
191
- lockScroll()
192
- }
193
-
194
- // 添加键盘事件监听
195
- if (props.closeOnEsc) {
196
- window.addEventListener('keydown', handleKeyDown)
197
- }
198
-
199
- // 触发打开事件
200
- emit('open')
201
- }
202
-
203
- // 弹窗关闭时
204
- function onClose() {
205
- // 解锁滚动
206
- if (props.lockScroll) {
207
- unlockScroll()
208
- }
209
-
210
- // 移除键盘事件监听
211
- window.removeEventListener('keydown', handleKeyDown)
212
-
213
- // 触发关闭事件
214
- emit('close')
215
- }
216
-
217
- // 锁定滚动
218
- function lockScroll() {
219
- // #ifdef H5
220
- lastScrollY.value = window.scrollY
221
- document.body.style.position = 'fixed'
222
- document.body.style.top = `-${lastScrollY.value}px`
223
- document.body.style.left = '0'
224
- document.body.style.right = '0'
225
- document.body.style.overflow = 'hidden'
226
- // #endif
227
-
228
- // #ifdef MP
229
- uni.hideTabBar()
230
- // #endif
231
- }
232
-
233
- // 解锁滚动
234
- function unlockScroll() {
235
- // #ifdef H5
236
- document.body.style.position = ''
237
- document.body.style.top = ''
238
- document.body.style.left = ''
239
- document.body.style.right = ''
240
- document.body.style.overflow = ''
241
- window.scrollTo(0, lastScrollY.value)
242
- // #endif
243
-
244
- // #ifdef MP
245
- uni.showTabBar()
246
- // #endif
247
- }
248
-
249
- // 键盘事件处理
250
- function handleKeyDown(e) {
251
- // #ifdef H5
252
- if (e.key === 'Escape' && props.closeOnEsc && showPopup.value) {
253
- handleClose()
254
- }
255
- // #endif
256
- }
257
-
258
- // 点击遮罩层
259
- function handleMaskClick() {
260
- if (props.closeOnMaskClick) {
261
- handleClose()
262
- }
263
- emit('maskClick')
264
- }
265
-
266
- // 点击关闭按钮
267
- function handleClose() {
268
- isClosing.value = true
269
- emit('closeButtonClick')
270
- }
271
-
272
- // 动画结束事件处理
273
- function handleAnimationEnd() {
274
- if (isClosing.value) {
275
- isClosing.value = false
276
- showPopup.value = false
277
- }
278
- }
279
-
280
- // 点击确认按钮
281
- function handleConfirm() {
282
- emit('confirm')
283
- }
284
-
285
- // 点击取消按钮
286
- function handleCancel() {
287
- emit('cancel')
288
- handleClose()
289
- }
290
-
291
- // 生命周期
292
- onMounted(() => {
293
- if (showPopup.value) {
294
- onOpen()
295
- }
296
- })
297
-
298
- onUnmounted(() => {
299
- if (showPopup.value) {
300
- onClose()
301
- }
302
- })
303
- </script>
304
-
305
- <style scoped>
306
- /* 弹窗容器 */
307
- .popup-wrapper {
308
- position: fixed;
309
- top: 0;
310
- left: 0;
311
- right: 0;
312
- bottom: 0;
313
- z-index: 1000;
314
- display: flex;
315
- align-items: center;
316
- justify-content: center;
317
- pointer-events: none;
318
- }
319
-
320
- /* 模态模式 */
321
- .popup-wrapper.modal {
322
- pointer-events: auto;
323
- }
324
-
325
- /* 非模态模式 */
326
- .popup-wrapper.non-modal {
327
- pointer-events: none;
328
- }
329
-
330
- .popup-wrapper.non-modal .popup-container {
331
- pointer-events: auto;
332
- }
333
-
334
- /* 遮罩层 */
335
- .popup-mask {
336
- position: absolute;
337
- top: 0;
338
- left: 0;
339
- right: 0;
340
- bottom: 0;
341
- background-color: rgba(0, 0, 0, 0.5);
342
- transition: opacity 0.3s ease;
343
- }
344
-
345
- /* 弹窗主体 */
346
- .popup-container {
347
- position: fixed;
348
- background-color: white;
349
- border-radius: 16rpx;
350
- box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15);
351
- width: var(--popup-width, 100%);
352
- max-width: var(--popup-max-width, 750rpx);
353
- max-height: 90vh;
354
- overflow: hidden;
355
- pointer-events: auto;
356
- }
357
-
358
- /* 头部 */
359
- .popup-header {
360
- padding: 32rpx 40rpx;
361
- border-bottom: 2rpx solid #ebeef5;
362
- }
363
-
364
- .popup-title {
365
- margin: 0;
366
- font-size: 32rpx;
367
- font-weight: 600;
368
- color: #303133;
369
- }
370
-
371
- /* 内容 */
372
- .popup-content {
373
- padding: 40rpx;
374
- max-height: calc(90vh - 240rpx);
375
- overflow-y: auto;
376
- }
377
-
378
- /* 底部 */
379
- .popup-footer {
380
- padding: 32rpx 40rpx;
381
- border-top: 2rpx solid #ebeef5;
382
- display: flex;
383
- justify-content: flex-end;
384
- gap: 24rpx;
385
- }
386
-
387
- /* 按钮 */
388
- .popup-button {
389
- padding: 16rpx 32rpx;
390
- border: none;
391
- border-radius: 8rpx;
392
- font-size: 28rpx;
393
- cursor: pointer;
394
- transition: all 0.3s ease;
395
- text-align: center;
396
- box-sizing: border-box;
397
- }
398
-
399
- .popup-button-cancel {
400
- background-color: #f5f7fa;
401
- color: #606266;
402
- }
403
-
404
- .popup-button-cancel:hover {
405
- background-color: #e4e7ed;
406
- }
407
-
408
- .popup-button-confirm {
409
- background-color: #409eff;
410
- color: white;
411
- }
412
-
413
- .popup-button-confirm:hover {
414
- background-color: #66b1ff;
415
- }
416
-
417
- /* 关闭按钮 */
418
- .popup-close-button {
419
- position: absolute;
420
- top: 24rpx;
421
- right: 24rpx;
422
- width: 48rpx;
423
- height: 48rpx;
424
- background-color: transparent;
425
- border-radius: 50%;
426
- cursor: pointer;
427
- display: flex;
428
- align-items: center;
429
- justify-content: center;
430
- color: #909399;
431
- transition: all 0.3s ease;
432
- }
433
-
434
- .popup-close-button:hover {
435
- background-color: #f5f7fa;
436
- color: #606266;
437
- }
438
-
439
- /* 关闭图标 */
440
- .close-icon {
441
- font-size: 40rpx;
442
- font-weight: bold;
443
- line-height: 1;
444
- }
445
-
446
- /* 位置样式 */
447
- /* 居中 */
448
- .position-center {
449
- top: 50%;
450
- left: 50%;
451
- transform: translate(-50%, -50%);
452
- }
453
-
454
- /* 顶部 */
455
- .position-top {
456
- top: 0;
457
- left: 0;
458
- width: 100%;
459
- border-radius: 0 0 16rpx 16rpx;
460
- }
461
-
462
- /* 底部 */
463
- .position-bottom {
464
- bottom: 0;
465
- left: 0;
466
- width: 100%;
467
- border-radius: 16rpx 16rpx 0 0;
468
- }
469
-
470
- /* 左侧 */
471
- .position-left {
472
- top: 0;
473
- left: 0;
474
- height: 100%;
475
- border-radius: 0 16rpx 16rpx 0;
476
- }
477
-
478
- /* 右侧 */
479
- .position-right {
480
- top: 0;
481
- right: 0;
482
- height: 100%;
483
- border-radius: 16rpx 0 0 16rpx;
484
- }
485
-
486
- /* 动画样式 */
487
- /* 淡入淡出 */
488
- .animation-fade {
489
- animation: fadeIn 0.3s ease;
490
- }
491
-
492
- @keyframes fadeIn {
493
- from {
494
- opacity: 0;
495
- }
496
- to {
497
- opacity: 1;
498
- }
499
- }
500
-
501
- /* 缩放 */
502
- .animation-scale {
503
- animation: scaleIn 0.3s ease;
504
- }
505
-
506
- @keyframes scaleIn {
507
- from {
508
- opacity: 0;
509
- transform: translate(-50%, -50%) scale(0.8);
510
- }
511
- to {
512
- opacity: 1;
513
- transform: translate(-50%, -50%) scale(1);
514
- }
515
- }
516
-
517
- /* 顶部位置的缩放动画 */
518
- .position-top.animation-scale {
519
- animation: scaleInTop 0.3s ease;
520
- }
521
-
522
- @keyframes scaleInTop {
523
- from {
524
- opacity: 0;
525
- transform: translateY(-100%);
526
- }
527
- to {
528
- opacity: 1;
529
- transform: translateY(0);
530
- }
531
- }
532
-
533
- /* 底部位置的缩放动画 */
534
- .position-bottom.animation-scale {
535
- animation: scaleInBottom 0.3s ease;
536
- }
537
-
538
- @keyframes scaleInBottom {
539
- from {
540
- opacity: 0;
541
- transform: translateY(100%);
542
- }
543
- to {
544
- opacity: 1;
545
- transform: translateY(0);
546
- }
547
- }
548
-
549
- /* 左侧位置的缩放动画 */
550
- .position-left.animation-scale {
551
- animation: scaleInLeft 0.3s ease;
552
- }
553
-
554
- @keyframes scaleInLeft {
555
- from {
556
- opacity: 0;
557
- transform: translateX(-100%);
558
- }
559
- to {
560
- opacity: 1;
561
- transform: translateX(0);
562
- }
563
- }
564
-
565
- /* 右侧位置的缩放动画 */
566
- .position-right.animation-scale {
567
- animation: scaleInRight 0.3s ease;
568
- }
569
-
570
- @keyframes scaleInRight {
571
- from {
572
- opacity: 0;
573
- transform: translateX(100%);
574
- }
575
- to {
576
- opacity: 1;
577
- transform: translateX(0);
578
- }
579
- }
580
-
581
- /* 滑动动画 */
582
- .animation-slide {
583
- animation: slideIn 0.3s ease;
584
- }
585
-
586
- @keyframes slideIn {
587
- from {
588
- opacity: 0;
589
- transform: translate(-50%, -100%);
590
- }
591
- to {
592
- opacity: 1;
593
- transform: translate(-50%, -50%);
594
- }
595
- }
596
-
597
- /* 顶部位置的滑动动画 */
598
- .position-top.animation-slide {
599
- animation: slideInTop 0.3s ease;
600
- }
601
-
602
- @keyframes slideInTop {
603
- from {
604
- opacity: 0;
605
- transform: translateY(-100%);
606
- }
607
- to {
608
- opacity: 1;
609
- transform: translateY(0);
610
- }
611
- }
612
-
613
- /* 底部位置的滑动动画 */
614
- .position-bottom.animation-slide {
615
- animation: slideInBottom 0.3s ease;
616
- }
617
-
618
- @keyframes slideInBottom {
619
- from {
620
- opacity: 0;
621
- transform: translateY(100%);
622
- }
623
- to {
624
- opacity: 1;
625
- transform: translateY(0);
626
- }
627
- }
628
-
629
- /* 左侧位置的滑动动画 */
630
- .position-left.animation-slide {
631
- animation: slideInLeft 0.3s ease;
632
- }
633
-
634
- @keyframes slideInLeft {
635
- from {
636
- opacity: 0;
637
- transform: translateX(-100%);
638
- }
639
- to {
640
- opacity: 1;
641
- transform: translateX(0);
642
- }
643
- }
644
-
645
- /* 右侧位置的滑动动画 */
646
- .position-right.animation-slide {
647
- animation: slideInRight 0.3s ease;
648
- }
649
-
650
- @keyframes slideInRight {
651
- from {
652
- opacity: 0;
653
- transform: translateX(100%);
654
- }
655
- to {
656
- opacity: 1;
657
- transform: translateX(0);
658
- }
659
- }
660
-
661
- /* 关闭动画 */
662
- /* 淡入淡出关闭动画 */
663
- .animation-fade.closing {
664
- animation: fadeOut 0.3s ease;
665
- }
666
-
667
- @keyframes fadeOut {
668
- from {
669
- opacity: 1;
670
- }
671
- to {
672
- opacity: 0;
673
- }
674
- }
675
-
676
- /* 缩放关闭动画 */
677
- .animation-scale.closing {
678
- animation: scaleOut 0.3s ease;
679
- }
680
-
681
- @keyframes scaleOut {
682
- from {
683
- opacity: 1;
684
- transform: translate(-50%, -50%) scale(1);
685
- }
686
- to {
687
- opacity: 0;
688
- transform: translate(-50%, -50%) scale(0.8);
689
- }
690
- }
691
-
692
- /* 顶部位置的缩放关闭动画 */
693
- .position-top.animation-scale.closing {
694
- animation: scaleOutTop 0.3s ease;
695
- }
696
-
697
- @keyframes scaleOutTop {
698
- from {
699
- opacity: 1;
700
- transform: translateY(0);
701
- }
702
- to {
703
- opacity: 0;
704
- transform: translateY(-100%);
705
- }
706
- }
707
-
708
- /* 底部位置的缩放关闭动画 */
709
- .position-bottom.animation-scale.closing {
710
- animation: scaleOutBottom 0.3s ease;
711
- }
712
-
713
- @keyframes scaleOutBottom {
714
- from {
715
- opacity: 1;
716
- transform: translateY(0);
717
- }
718
- to {
719
- opacity: 0;
720
- transform: translateY(100%);
721
- }
722
- }
723
-
724
- /* 左侧位置的缩放关闭动画 */
725
- .position-left.animation-scale.closing {
726
- animation: scaleOutLeft 0.3s ease;
727
- }
728
-
729
- @keyframes scaleOutLeft {
730
- from {
731
- opacity: 1;
732
- transform: translateX(0);
733
- }
734
- to {
735
- opacity: 0;
736
- transform: translateX(-100%);
737
- }
738
- }
739
-
740
- /* 右侧位置的缩放关闭动画 */
741
- .position-right.animation-scale.closing {
742
- animation: scaleOutRight 0.3s ease;
743
- }
744
-
745
- @keyframes scaleOutRight {
746
- from {
747
- opacity: 1;
748
- transform: translateX(0);
749
- }
750
- to {
751
- opacity: 0;
752
- transform: translateX(100%);
753
- }
754
- }
755
-
756
- /* 滑动关闭动画 */
757
- .animation-slide.closing {
758
- animation: slideOut 0.3s ease;
759
- }
760
-
761
- @keyframes slideOut {
762
- from {
763
- opacity: 1;
764
- transform: translate(-50%, -50%);
765
- }
766
- to {
767
- opacity: 0;
768
- transform: translate(-50%, -100%);
769
- }
770
- }
771
-
772
- /* 顶部位置的滑动关闭动画 */
773
- .position-top.animation-slide.closing {
774
- animation: slideOutTop 0.3s ease;
775
- }
776
-
777
- @keyframes slideOutTop {
778
- from {
779
- opacity: 1;
780
- transform: translateY(0);
781
- }
782
- to {
783
- opacity: 0;
784
- transform: translateY(-100%);
785
- }
786
- }
787
-
788
- /* 底部位置的滑动关闭动画 */
789
- .position-bottom.animation-slide.closing {
790
- animation: slideOutBottom 0.3s ease;
791
- }
792
-
793
- @keyframes slideOutBottom {
794
- from {
795
- opacity: 1;
796
- transform: translateY(0);
797
- }
798
- to {
799
- opacity: 0;
800
- transform: translateY(100%);
801
- }
802
- }
803
-
804
- /* 左侧位置的滑动关闭动画 */
805
- .position-left.animation-slide.closing {
806
- animation: slideOutLeft 0.3s ease;
807
- }
808
-
809
- @keyframes slideOutLeft {
810
- from {
811
- opacity: 1;
812
- transform: translateX(0);
813
- }
814
- to {
815
- opacity: 0;
816
- transform: translateX(-100%);
817
- }
818
- }
819
-
820
- /* 右侧位置的滑动关闭动画 */
821
- .position-right.animation-slide.closing {
822
- animation: slideOutRight 0.3s ease;
823
- }
824
-
825
- @keyframes slideOutRight {
826
- from {
827
- opacity: 1;
828
- transform: translateX(0);
829
- }
830
- to {
831
- opacity: 0;
832
- transform: translateX(100%);
833
- }
834
- }
835
- </style>
1
+ <template>
2
+ <view
3
+ v-show="showPopup"
4
+ class="popup-wrapper"
5
+ :class="{
6
+ 'modal': mask,
7
+ 'non-modal': !mask,
8
+ 'closing': isClosing,
9
+ }"
10
+ @click.self="handleMaskClick"
11
+ >
12
+ <!-- 遮罩层 -->
13
+ <view
14
+ v-if="mask"
15
+ class="popup-mask"
16
+ :style="{ opacity: maskOpacity }"
17
+ @click="handleMaskClick"
18
+ />
19
+
20
+ <!-- 弹窗主体 -->
21
+ <view
22
+ ref="popupRef"
23
+ class="popup-container"
24
+ :class="[
25
+ `position-${position}`,
26
+ `animation-${animation}`,
27
+ { closing: isClosing },
28
+ ]"
29
+ @click.stop
30
+ @touchmove.stop
31
+ @animationend="handleAnimationEnd"
32
+ >
33
+ <!-- 自定义头部 -->
34
+ <view v-if="$slots.header" class="popup-header">
35
+ <slot name="header" />
36
+ </view>
37
+
38
+ <!-- 自定义标题 -->
39
+ <view v-else-if="title" class="popup-header">
40
+ <text class="popup-title">{{ title }}</text>
41
+ </view>
42
+
43
+ <!-- 内容区域 -->
44
+ <view class="popup-content">
45
+ <slot />
46
+ </view>
47
+
48
+ <!-- 自定义底部 -->
49
+ <view v-if="$slots.footer" class="popup-footer">
50
+ <slot name="footer" />
51
+ </view>
52
+
53
+ <!-- 默认底部按钮 -->
54
+ <view v-else-if="showConfirmButton || showCancelButton" class="popup-footer">
55
+ <view
56
+ v-if="showCancelButton"
57
+ class="popup-button popup-button-cancel"
58
+ @click="handleCancel"
59
+ >
60
+ {{ cancelText }}
61
+ </view>
62
+ <view
63
+ v-if="showConfirmButton"
64
+ class="popup-button popup-button-confirm"
65
+ @click="handleConfirm"
66
+ >
67
+ {{ confirmText }}
68
+ </view>
69
+ </view>
70
+
71
+ <!-- 关闭按钮 -->
72
+ <view
73
+ v-if="showCloseButton"
74
+ class="popup-close-button"
75
+ @click="handleClose"
76
+ >
77
+ <text class="close-icon">×</text>
78
+ </view>
79
+ </view>
80
+ </view>
81
+ </template>
82
+
83
+ <script setup lang="ts">
84
+ import { onMounted, onUnmounted, ref, watch } from 'vue'
85
+
86
+ // Props
87
+ interface Props {
88
+ // 控制弹窗显示/隐藏(支持v-model)
89
+ modelValue?: boolean
90
+ // 弹窗标题
91
+ title?: string
92
+ // 弹窗位置:center, top, bottom, left, right
93
+ position?: 'center' | 'top' | 'bottom' | 'left' | 'right'
94
+ // 动画效果:fade, scale, slide
95
+ animation?: 'fade' | 'scale' | 'slide'
96
+ // 是否显示遮罩层
97
+ mask?: boolean
98
+ // 遮罩层透明度
99
+ maskOpacity?: number
100
+ // 点击遮罩是否关闭弹窗
101
+ closeOnMaskClick?: boolean
102
+ // 是否显示关闭按钮
103
+ showCloseButton?: boolean
104
+ // 是否支持Esc键关闭
105
+ closeOnEsc?: boolean
106
+ // 是否显示确认按钮
107
+ showConfirmButton?: boolean
108
+ // 确认按钮文本
109
+ confirmText?: string
110
+ // 是否显示取消按钮
111
+ showCancelButton?: boolean
112
+ // 取消按钮文本
113
+ cancelText?: string
114
+ // 弹窗宽度(百分比或像素)
115
+ width?: string | number
116
+ // 弹窗高度(百分比或像素)
117
+ height?: string | number
118
+ // 弹窗最大宽度
119
+ maxWidth?: string | number
120
+ // 是否禁止背景滚动
121
+ lockScroll?: boolean
122
+ }
123
+
124
+ // 设置默认属性
125
+ const props = withDefaults(defineProps<Props>(), {
126
+ modelValue: false,
127
+ title: '',
128
+ position: 'center',
129
+ animation: 'slide',
130
+ mask: true,
131
+ maskOpacity: 0.5,
132
+ closeOnMaskClick: true,
133
+ showCloseButton: true,
134
+ closeOnEsc: true,
135
+ showConfirmButton: false,
136
+ confirmText: '确定',
137
+ showCancelButton: false,
138
+ cancelText: '取消',
139
+ width: '50%',
140
+ height: '100%',
141
+ maxWidth: '500px',
142
+ lockScroll: true,
143
+ })
144
+
145
+ // Emits
146
+ const emit = defineEmits<{
147
+ (e: 'update:modelValue', value: boolean): void
148
+ (e: 'open'): void
149
+ (e: 'close'): void
150
+ (e: 'maskClick'): void
151
+ (e: 'confirm'): void
152
+ (e: 'cancel'): void
153
+ (e: 'closeButtonClick'): void
154
+ }>()
155
+
156
+ // Refs
157
+ const popupRef = ref<any>(null)
158
+ const showPopup = ref(props.modelValue)
159
+ const isClosing = ref(false)
160
+ const lastScrollY = ref(0)
161
+
162
+ // Watchers
163
+ // 监听modelValue变化
164
+ watch(() => props.modelValue, (newVal) => {
165
+ showPopup.value = newVal
166
+ if (newVal) {
167
+ onOpen()
168
+ }
169
+ else {
170
+ onClose()
171
+ }
172
+ })
173
+
174
+ // 监听showPopup变化
175
+ watch(() => showPopup.value, (newVal) => {
176
+ // 同步到v-model
177
+ emit('update:modelValue', newVal)
178
+ if (newVal) {
179
+ onOpen()
180
+ }
181
+ else {
182
+ onClose()
183
+ }
184
+ })
185
+
186
+ // Methods
187
+ // 弹窗打开时
188
+ function onOpen() {
189
+ // 锁定滚动
190
+ if (props.lockScroll) {
191
+ lockScroll()
192
+ }
193
+
194
+ // 添加键盘事件监听
195
+ if (props.closeOnEsc) {
196
+ window.addEventListener('keydown', handleKeyDown)
197
+ }
198
+
199
+ // 触发打开事件
200
+ emit('open')
201
+ }
202
+
203
+ // 弹窗关闭时
204
+ function onClose() {
205
+ // 解锁滚动
206
+ if (props.lockScroll) {
207
+ unlockScroll()
208
+ }
209
+
210
+ // 移除键盘事件监听
211
+ window.removeEventListener('keydown', handleKeyDown)
212
+
213
+ // 触发关闭事件
214
+ emit('close')
215
+ }
216
+
217
+ // 锁定滚动
218
+ function lockScroll() {
219
+ // #ifdef H5
220
+ lastScrollY.value = window.scrollY
221
+ document.body.style.position = 'fixed'
222
+ document.body.style.top = `-${lastScrollY.value}px`
223
+ document.body.style.left = '0'
224
+ document.body.style.right = '0'
225
+ document.body.style.overflow = 'hidden'
226
+ // #endif
227
+
228
+ // #ifdef MP
229
+ // @ts-ignore
230
+ uni.hideTabBar()
231
+ // #endif
232
+ }
233
+
234
+ // 解锁滚动
235
+ function unlockScroll() {
236
+ // #ifdef H5
237
+ document.body.style.position = ''
238
+ document.body.style.top = ''
239
+ document.body.style.left = ''
240
+ document.body.style.right = ''
241
+ document.body.style.overflow = ''
242
+ window.scrollTo(0, lastScrollY.value)
243
+ // #endif
244
+
245
+ // #ifdef MP
246
+ // @ts-ignore
247
+ uni.showTabBar()
248
+ // #endif
249
+ }
250
+
251
+ // 键盘事件处理
252
+ function handleKeyDown(e) {
253
+ // #ifdef H5
254
+ if (e.key === 'Escape' && props.closeOnEsc && showPopup.value) {
255
+ handleClose()
256
+ }
257
+ // #endif
258
+ }
259
+
260
+ // 点击遮罩层
261
+ function handleMaskClick() {
262
+ if (props.closeOnMaskClick) {
263
+ handleClose()
264
+ }
265
+ emit('maskClick')
266
+ }
267
+
268
+ // 点击关闭按钮
269
+ function handleClose() {
270
+ isClosing.value = true
271
+ emit('closeButtonClick')
272
+ }
273
+
274
+ // 动画结束事件处理
275
+ function handleAnimationEnd() {
276
+ if (isClosing.value) {
277
+ isClosing.value = false
278
+ showPopup.value = false
279
+ }
280
+ }
281
+
282
+ // 点击确认按钮
283
+ function handleConfirm() {
284
+ emit('confirm')
285
+ }
286
+
287
+ // 点击取消按钮
288
+ function handleCancel() {
289
+ emit('cancel')
290
+ handleClose()
291
+ }
292
+
293
+ // 生命周期
294
+ onMounted(() => {
295
+ if (showPopup.value) {
296
+ onOpen()
297
+ }
298
+ })
299
+
300
+ onUnmounted(() => {
301
+ if (showPopup.value) {
302
+ onClose()
303
+ }
304
+ })
305
+ </script>
306
+
307
+ <style scoped>
308
+ /* 弹窗容器 */
309
+ .popup-wrapper {
310
+ position: fixed;
311
+ top: 0;
312
+ left: 0;
313
+ right: 0;
314
+ bottom: 0;
315
+ z-index: 1000;
316
+ display: flex;
317
+ align-items: center;
318
+ justify-content: center;
319
+ pointer-events: none;
320
+ }
321
+
322
+ /* 模态模式 */
323
+ .popup-wrapper.modal {
324
+ pointer-events: auto;
325
+ }
326
+
327
+ /* 非模态模式 */
328
+ .popup-wrapper.non-modal {
329
+ pointer-events: none;
330
+ }
331
+
332
+ .popup-wrapper.non-modal .popup-container {
333
+ pointer-events: auto;
334
+ }
335
+
336
+ /* 遮罩层 */
337
+ .popup-mask {
338
+ position: absolute;
339
+ top: 0;
340
+ left: 0;
341
+ right: 0;
342
+ bottom: 0;
343
+ background-color: rgba(0, 0, 0, 0.5);
344
+ transition: opacity 0.3s ease;
345
+ }
346
+
347
+ /* 弹窗主体 */
348
+ .popup-container {
349
+ position: fixed;
350
+ background-color: white;
351
+ border-radius: 16rpx;
352
+ box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15);
353
+ width: var(--popup-width, 100%);
354
+ max-width: var(--popup-max-width, 750rpx);
355
+ max-height: 90vh;
356
+ overflow: hidden;
357
+ pointer-events: auto;
358
+ }
359
+
360
+ /* 头部 */
361
+ .popup-header {
362
+ padding: 32rpx 40rpx;
363
+ border-bottom: 2rpx solid #ebeef5;
364
+ }
365
+
366
+ .popup-title {
367
+ margin: 0;
368
+ font-size: 32rpx;
369
+ font-weight: 600;
370
+ color: #303133;
371
+ }
372
+
373
+ /* 内容 */
374
+ .popup-content {
375
+ padding: 40rpx;
376
+ max-height: calc(90vh - 240rpx);
377
+ overflow-y: auto;
378
+ }
379
+
380
+ /* 底部 */
381
+ .popup-footer {
382
+ padding: 32rpx 40rpx;
383
+ border-top: 2rpx solid #ebeef5;
384
+ display: flex;
385
+ justify-content: flex-end;
386
+ gap: 24rpx;
387
+ }
388
+
389
+ /* 按钮 */
390
+ .popup-button {
391
+ padding: 16rpx 32rpx;
392
+ border: none;
393
+ border-radius: 8rpx;
394
+ font-size: 28rpx;
395
+ cursor: pointer;
396
+ transition: all 0.3s ease;
397
+ text-align: center;
398
+ box-sizing: border-box;
399
+ }
400
+
401
+ .popup-button-cancel {
402
+ background-color: #f5f7fa;
403
+ color: #606266;
404
+ }
405
+
406
+ .popup-button-cancel:hover {
407
+ background-color: #e4e7ed;
408
+ }
409
+
410
+ .popup-button-confirm {
411
+ background-color: #409eff;
412
+ color: white;
413
+ }
414
+
415
+ .popup-button-confirm:hover {
416
+ background-color: #66b1ff;
417
+ }
418
+
419
+ /* 关闭按钮 */
420
+ .popup-close-button {
421
+ position: absolute;
422
+ top: 24rpx;
423
+ right: 24rpx;
424
+ width: 48rpx;
425
+ height: 48rpx;
426
+ background-color: transparent;
427
+ border-radius: 50%;
428
+ cursor: pointer;
429
+ display: flex;
430
+ align-items: center;
431
+ justify-content: center;
432
+ color: #909399;
433
+ transition: all 0.3s ease;
434
+ }
435
+
436
+ .popup-close-button:hover {
437
+ background-color: #f5f7fa;
438
+ color: #606266;
439
+ }
440
+
441
+ /* 关闭图标 */
442
+ .close-icon {
443
+ font-size: 40rpx;
444
+ font-weight: bold;
445
+ line-height: 1;
446
+ }
447
+
448
+ /* 位置样式 */
449
+ /* 居中 */
450
+ .position-center {
451
+ top: 50%;
452
+ left: 50%;
453
+ transform: translate(-50%, -50%);
454
+ }
455
+
456
+ /* 顶部 */
457
+ .position-top {
458
+ top: 0;
459
+ left: 0;
460
+ width: 100%;
461
+ border-radius: 0 0 16rpx 16rpx;
462
+ }
463
+
464
+ /* 底部 */
465
+ .position-bottom {
466
+ bottom: 0;
467
+ left: 0;
468
+ width: 100%;
469
+ border-radius: 16rpx 16rpx 0 0;
470
+ }
471
+
472
+ /* 左侧 */
473
+ .position-left {
474
+ top: 0;
475
+ left: 0;
476
+ height: 100%;
477
+ border-radius: 0 16rpx 16rpx 0;
478
+ }
479
+
480
+ /* 右侧 */
481
+ .position-right {
482
+ top: 0;
483
+ right: 0;
484
+ height: 100%;
485
+ border-radius: 16rpx 0 0 16rpx;
486
+ }
487
+
488
+ /* 动画样式 */
489
+ /* 淡入淡出 */
490
+ .animation-fade {
491
+ animation: fadeIn 0.3s ease;
492
+ }
493
+
494
+ @keyframes fadeIn {
495
+ from {
496
+ opacity: 0;
497
+ }
498
+ to {
499
+ opacity: 1;
500
+ }
501
+ }
502
+
503
+ /* 缩放 */
504
+ .animation-scale {
505
+ animation: scaleIn 0.3s ease;
506
+ }
507
+
508
+ @keyframes scaleIn {
509
+ from {
510
+ opacity: 0;
511
+ transform: translate(-50%, -50%) scale(0.8);
512
+ }
513
+ to {
514
+ opacity: 1;
515
+ transform: translate(-50%, -50%) scale(1);
516
+ }
517
+ }
518
+
519
+ /* 顶部位置的缩放动画 */
520
+ .position-top.animation-scale {
521
+ animation: scaleInTop 0.3s ease;
522
+ }
523
+
524
+ @keyframes scaleInTop {
525
+ from {
526
+ opacity: 0;
527
+ transform: translateY(-100%);
528
+ }
529
+ to {
530
+ opacity: 1;
531
+ transform: translateY(0);
532
+ }
533
+ }
534
+
535
+ /* 底部位置的缩放动画 */
536
+ .position-bottom.animation-scale {
537
+ animation: scaleInBottom 0.3s ease;
538
+ }
539
+
540
+ @keyframes scaleInBottom {
541
+ from {
542
+ opacity: 0;
543
+ transform: translateY(100%);
544
+ }
545
+ to {
546
+ opacity: 1;
547
+ transform: translateY(0);
548
+ }
549
+ }
550
+
551
+ /* 左侧位置的缩放动画 */
552
+ .position-left.animation-scale {
553
+ animation: scaleInLeft 0.3s ease;
554
+ }
555
+
556
+ @keyframes scaleInLeft {
557
+ from {
558
+ opacity: 0;
559
+ transform: translateX(-100%);
560
+ }
561
+ to {
562
+ opacity: 1;
563
+ transform: translateX(0);
564
+ }
565
+ }
566
+
567
+ /* 右侧位置的缩放动画 */
568
+ .position-right.animation-scale {
569
+ animation: scaleInRight 0.3s ease;
570
+ }
571
+
572
+ @keyframes scaleInRight {
573
+ from {
574
+ opacity: 0;
575
+ transform: translateX(100%);
576
+ }
577
+ to {
578
+ opacity: 1;
579
+ transform: translateX(0);
580
+ }
581
+ }
582
+
583
+ /* 滑动动画 */
584
+ .animation-slide {
585
+ animation: slideIn 0.3s ease;
586
+ }
587
+
588
+ @keyframes slideIn {
589
+ from {
590
+ opacity: 0;
591
+ transform: translate(-50%, -100%);
592
+ }
593
+ to {
594
+ opacity: 1;
595
+ transform: translate(-50%, -50%);
596
+ }
597
+ }
598
+
599
+ /* 顶部位置的滑动动画 */
600
+ .position-top.animation-slide {
601
+ animation: slideInTop 0.3s ease;
602
+ }
603
+
604
+ @keyframes slideInTop {
605
+ from {
606
+ opacity: 0;
607
+ transform: translateY(-100%);
608
+ }
609
+ to {
610
+ opacity: 1;
611
+ transform: translateY(0);
612
+ }
613
+ }
614
+
615
+ /* 底部位置的滑动动画 */
616
+ .position-bottom.animation-slide {
617
+ animation: slideInBottom 0.3s ease;
618
+ }
619
+
620
+ @keyframes slideInBottom {
621
+ from {
622
+ opacity: 0;
623
+ transform: translateY(100%);
624
+ }
625
+ to {
626
+ opacity: 1;
627
+ transform: translateY(0);
628
+ }
629
+ }
630
+
631
+ /* 左侧位置的滑动动画 */
632
+ .position-left.animation-slide {
633
+ animation: slideInLeft 0.3s ease;
634
+ }
635
+
636
+ @keyframes slideInLeft {
637
+ from {
638
+ opacity: 0;
639
+ transform: translateX(-100%);
640
+ }
641
+ to {
642
+ opacity: 1;
643
+ transform: translateX(0);
644
+ }
645
+ }
646
+
647
+ /* 右侧位置的滑动动画 */
648
+ .position-right.animation-slide {
649
+ animation: slideInRight 0.3s ease;
650
+ }
651
+
652
+ @keyframes slideInRight {
653
+ from {
654
+ opacity: 0;
655
+ transform: translateX(100%);
656
+ }
657
+ to {
658
+ opacity: 1;
659
+ transform: translateX(0);
660
+ }
661
+ }
662
+
663
+ /* 关闭动画 */
664
+ /* 淡入淡出关闭动画 */
665
+ .animation-fade.closing {
666
+ animation: fadeOut 0.3s ease;
667
+ }
668
+
669
+ @keyframes fadeOut {
670
+ from {
671
+ opacity: 1;
672
+ }
673
+ to {
674
+ opacity: 0;
675
+ }
676
+ }
677
+
678
+ /* 缩放关闭动画 */
679
+ .animation-scale.closing {
680
+ animation: scaleOut 0.3s ease;
681
+ }
682
+
683
+ @keyframes scaleOut {
684
+ from {
685
+ opacity: 1;
686
+ transform: translate(-50%, -50%) scale(1);
687
+ }
688
+ to {
689
+ opacity: 0;
690
+ transform: translate(-50%, -50%) scale(0.8);
691
+ }
692
+ }
693
+
694
+ /* 顶部位置的缩放关闭动画 */
695
+ .position-top.animation-scale.closing {
696
+ animation: scaleOutTop 0.3s ease;
697
+ }
698
+
699
+ @keyframes scaleOutTop {
700
+ from {
701
+ opacity: 1;
702
+ transform: translateY(0);
703
+ }
704
+ to {
705
+ opacity: 0;
706
+ transform: translateY(-100%);
707
+ }
708
+ }
709
+
710
+ /* 底部位置的缩放关闭动画 */
711
+ .position-bottom.animation-scale.closing {
712
+ animation: scaleOutBottom 0.3s ease;
713
+ }
714
+
715
+ @keyframes scaleOutBottom {
716
+ from {
717
+ opacity: 1;
718
+ transform: translateY(0);
719
+ }
720
+ to {
721
+ opacity: 0;
722
+ transform: translateY(100%);
723
+ }
724
+ }
725
+
726
+ /* 左侧位置的缩放关闭动画 */
727
+ .position-left.animation-scale.closing {
728
+ animation: scaleOutLeft 0.3s ease;
729
+ }
730
+
731
+ @keyframes scaleOutLeft {
732
+ from {
733
+ opacity: 1;
734
+ transform: translateX(0);
735
+ }
736
+ to {
737
+ opacity: 0;
738
+ transform: translateX(-100%);
739
+ }
740
+ }
741
+
742
+ /* 右侧位置的缩放关闭动画 */
743
+ .position-right.animation-scale.closing {
744
+ animation: scaleOutRight 0.3s ease;
745
+ }
746
+
747
+ @keyframes scaleOutRight {
748
+ from {
749
+ opacity: 1;
750
+ transform: translateX(0);
751
+ }
752
+ to {
753
+ opacity: 0;
754
+ transform: translateX(100%);
755
+ }
756
+ }
757
+
758
+ /* 滑动关闭动画 */
759
+ .animation-slide.closing {
760
+ animation: slideOut 0.3s ease;
761
+ }
762
+
763
+ @keyframes slideOut {
764
+ from {
765
+ opacity: 1;
766
+ transform: translate(-50%, -50%);
767
+ }
768
+ to {
769
+ opacity: 0;
770
+ transform: translate(-50%, -100%);
771
+ }
772
+ }
773
+
774
+ /* 顶部位置的滑动关闭动画 */
775
+ .position-top.animation-slide.closing {
776
+ animation: slideOutTop 0.3s ease;
777
+ }
778
+
779
+ @keyframes slideOutTop {
780
+ from {
781
+ opacity: 1;
782
+ transform: translateY(0);
783
+ }
784
+ to {
785
+ opacity: 0;
786
+ transform: translateY(-100%);
787
+ }
788
+ }
789
+
790
+ /* 底部位置的滑动关闭动画 */
791
+ .position-bottom.animation-slide.closing {
792
+ animation: slideOutBottom 0.3s ease;
793
+ }
794
+
795
+ @keyframes slideOutBottom {
796
+ from {
797
+ opacity: 1;
798
+ transform: translateY(0);
799
+ }
800
+ to {
801
+ opacity: 0;
802
+ transform: translateY(100%);
803
+ }
804
+ }
805
+
806
+ /* 左侧位置的滑动关闭动画 */
807
+ .position-left.animation-slide.closing {
808
+ animation: slideOutLeft 0.3s ease;
809
+ }
810
+
811
+ @keyframes slideOutLeft {
812
+ from {
813
+ opacity: 1;
814
+ transform: translateX(0);
815
+ }
816
+ to {
817
+ opacity: 0;
818
+ transform: translateX(-100%);
819
+ }
820
+ }
821
+
822
+ /* 右侧位置的滑动关闭动画 */
823
+ .position-right.animation-slide.closing {
824
+ animation: slideOutRight 0.3s ease;
825
+ }
826
+
827
+ @keyframes slideOutRight {
828
+ from {
829
+ opacity: 1;
830
+ transform: translateX(0);
831
+ }
832
+ to {
833
+ opacity: 0;
834
+ transform: translateX(100%);
835
+ }
836
+ }
837
+ </style>