|
@@ -1,417 +1,106 @@
|
|
|
+<!-- BottomPopup.vue -->
|
|
|
<template>
|
|
|
- <!-- 遮罩层 -->
|
|
|
- <div
|
|
|
- v-if="isVisible"
|
|
|
- class="overlay-mask"
|
|
|
- :class="overlayAnimationClass"
|
|
|
- @click.self="handleOverlayClick"
|
|
|
- >
|
|
|
- <!-- 遮罩内容 -->
|
|
|
+ <!-- 最外层用 transition 做遮罩的淡入淡出 -->
|
|
|
+ <transition :name="overlayTransitionName">
|
|
|
+ <!-- 遮罩 -->
|
|
|
<div
|
|
|
- class="overlay-content"
|
|
|
- :class="contentAnimationClass"
|
|
|
- :style="contentStyle"
|
|
|
+ v-show="visible"
|
|
|
+ class="overlay-mask"
|
|
|
+ :class="overlayClass"
|
|
|
+ @click.self="clickOverlay"
|
|
|
>
|
|
|
- <!-- 头部插槽 - 可选 -->
|
|
|
- <div v-if="$slots.header || showDefaultHeader" class="overlay-header">
|
|
|
- <slot name="header">
|
|
|
- <h3 class="overlay-title">
|
|
|
- {{ title }}
|
|
|
- </h3>
|
|
|
- </slot>
|
|
|
-
|
|
|
- <!-- 关闭按钮 -->
|
|
|
- <button
|
|
|
- v-if="closable"
|
|
|
- class="btn-close"
|
|
|
- @click="close"
|
|
|
- >
|
|
|
- ×
|
|
|
- </button>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 主体内容插槽 - 必选 -->
|
|
|
- <div class="overlay-body">
|
|
|
- <slot />
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 底部插槽 - 可选 -->
|
|
|
- <div v-if="$slots.footer" class="overlay-footer">
|
|
|
- <slot name="footer">
|
|
|
- <button class="btn btn-primary" @click="close">
|
|
|
- 关闭
|
|
|
- </button>
|
|
|
- </slot>
|
|
|
- </div>
|
|
|
+ <!-- 内容区用另一个 transition 实现从底部滑出 -->
|
|
|
+ <transition :name="contentTransitionName">
|
|
|
+ <div v-show="visible" class="overlay-content" :class="overlayClass">
|
|
|
+ <!-- 可定制的区域 -->
|
|
|
+ <slot />
|
|
|
+ </div>
|
|
|
+ </transition>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ </transition>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
export default {
|
|
|
- name: 'AnimatedOverlay',
|
|
|
+ name: 'CommonDialog',
|
|
|
props: {
|
|
|
- // 是否显示遮罩
|
|
|
+ // 受控显示/隐藏
|
|
|
visible: {
|
|
|
type: Boolean,
|
|
|
default: false
|
|
|
},
|
|
|
- // 遮罩标题(默认头部使用)
|
|
|
- title: {
|
|
|
+ overlayTransitionName: {
|
|
|
type: String,
|
|
|
- default: '提示'
|
|
|
+ default: 'fade', // fade/slide-up
|
|
|
},
|
|
|
- // 动画类型:fade(淡入淡出)、scale(缩放)、slide(滑动)
|
|
|
- animation: {
|
|
|
+ contentTransitionName: {
|
|
|
type: String,
|
|
|
- default: 'fade',
|
|
|
- validator: value => ['fade', 'scale', 'slide'].includes(value)
|
|
|
+ default: '',
|
|
|
},
|
|
|
- // 是否显示关闭按钮
|
|
|
- closable: {
|
|
|
- type: Boolean,
|
|
|
- default: true
|
|
|
- },
|
|
|
- duration: {
|
|
|
- type: Number,
|
|
|
- default: 300
|
|
|
- },
|
|
|
- // 点击遮罩背景是否关闭
|
|
|
- closeOnClickOverlay: {
|
|
|
- type: Boolean,
|
|
|
- default: true
|
|
|
- },
|
|
|
- // 是否允许按ESC键关闭
|
|
|
- closeOnPressEscape: {
|
|
|
- type: Boolean,
|
|
|
- default: true
|
|
|
- },
|
|
|
- // 是否显示默认头部
|
|
|
- showDefaultHeader: {
|
|
|
- type: Boolean,
|
|
|
- default: true
|
|
|
+ contentContainerClass: {
|
|
|
+ type: String,
|
|
|
+ default: ''
|
|
|
},
|
|
|
- // 内容样式
|
|
|
- contentStyle: {
|
|
|
- type: Object,
|
|
|
- default: () => ({})
|
|
|
+ contentClass: {
|
|
|
+ type: String,
|
|
|
+ default: ''
|
|
|
},
|
|
|
- },
|
|
|
- data() {
|
|
|
- return {
|
|
|
- // 内部可见性状态,用于处理动画
|
|
|
- isVisible: false,
|
|
|
- // 记录动画状态
|
|
|
- isClosing: false
|
|
|
- }
|
|
|
- },
|
|
|
- computed: {
|
|
|
- // 遮罩动画类
|
|
|
- overlayAnimationClass() {
|
|
|
- if (!this.isVisible)
|
|
|
- return ''
|
|
|
-
|
|
|
- return this.isClosing
|
|
|
- ? `animate-overlay-${this.animation}-out`
|
|
|
- : `animate-overlay-${this.animation}-in`
|
|
|
+ overlayClass: {
|
|
|
+ type: String,
|
|
|
+ default: ''
|
|
|
},
|
|
|
- // 内容动画类
|
|
|
- contentAnimationClass() {
|
|
|
- if (!this.isVisible)
|
|
|
- return ''
|
|
|
-
|
|
|
- return this.isClosing
|
|
|
- ? `animate-content-${this.animation}-out`
|
|
|
- : `animate-content-${this.animation}-in`
|
|
|
- }
|
|
|
},
|
|
|
watch: {
|
|
|
- // 监听visible变化,控制显示/隐藏
|
|
|
- visible(newVal) {
|
|
|
- if (newVal) {
|
|
|
- this.open()
|
|
|
+ // 简单处理 body 滚动
|
|
|
+ visible(val) {
|
|
|
+ if (val) {
|
|
|
+ document.body.style.overflow = 'hidden'
|
|
|
}
|
|
|
else {
|
|
|
- this.close()
|
|
|
+ document.body.style.overflow = ''
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
- mounted() {
|
|
|
- // 初始化时如果需要显示
|
|
|
- if (this.visible) {
|
|
|
- this.open()
|
|
|
- }
|
|
|
-
|
|
|
- // 监听ESC键
|
|
|
- document.addEventListener('keydown', this.handleKeydown)
|
|
|
- },
|
|
|
- beforeUnmount() {
|
|
|
- document.removeEventListener('keydown', this.handleKeydown)
|
|
|
- },
|
|
|
methods: {
|
|
|
- // 打开遮罩
|
|
|
- open() {
|
|
|
- this.isClosing = false
|
|
|
- this.isVisible = true
|
|
|
- // 防止背景滚动
|
|
|
- document.body.style.overflow = 'hidden'
|
|
|
- },
|
|
|
- // 关闭遮罩
|
|
|
- close() {
|
|
|
- if (this.isClosing)
|
|
|
- return
|
|
|
-
|
|
|
- this.isClosing = true
|
|
|
-
|
|
|
- // 动画结束后隐藏
|
|
|
- setTimeout(() => {
|
|
|
- this.isVisible = false
|
|
|
- this.isClosing = false
|
|
|
- document.body.style.overflow = ''
|
|
|
- // 通知父组件关闭
|
|
|
- this.$emit('update:visible', false)
|
|
|
- this.$emit('close')
|
|
|
- }, this.duration)
|
|
|
+ clickOverlay() {
|
|
|
+ this.syncVisible(false)
|
|
|
},
|
|
|
- // 点击遮罩背景
|
|
|
- handleOverlayClick() {
|
|
|
- if (this.closeOnClickOverlay) {
|
|
|
- this.close()
|
|
|
- }
|
|
|
+ syncVisible(f = false) {
|
|
|
+ this.$emit('update:visible', f)
|
|
|
},
|
|
|
- // 处理键盘事件
|
|
|
- handleKeydown(e) {
|
|
|
- if (this.closeOnPressEscape && e.key === 'Escape' && this.isVisible) {
|
|
|
- this.close()
|
|
|
- }
|
|
|
- }
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
-<style scoped>
|
|
|
-/* 遮罩基础样式 */
|
|
|
+<style scoped lang="scss">
|
|
|
+/* 遮罩 */
|
|
|
.overlay-mask {
|
|
|
- --animate-duration: 300ms; /* 定义变量 */
|
|
|
position: fixed;
|
|
|
top: 0;
|
|
|
left: 0;
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
- background-color: rgba(0, 0, 0, 0.5);
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- z-index: 1000;
|
|
|
- opacity: 0;
|
|
|
-}
|
|
|
-
|
|
|
-.overlay-content {
|
|
|
- background-color: white;
|
|
|
- border-radius: 8px;
|
|
|
- width: 90%;
|
|
|
- max-width: 500px;
|
|
|
- padding: 25px;
|
|
|
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
|
-}
|
|
|
-
|
|
|
-/* 头部样式 */
|
|
|
-.overlay-header {
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
- margin-bottom: 20px;
|
|
|
-}
|
|
|
-
|
|
|
-.overlay-title {
|
|
|
- font-size: 1.4rem;
|
|
|
- font-weight: 600;
|
|
|
- color: #333;
|
|
|
- margin: 0;
|
|
|
-}
|
|
|
-
|
|
|
-/* 主体样式 */
|
|
|
-.overlay-body {
|
|
|
- margin-bottom: 25px;
|
|
|
- color: #666;
|
|
|
- line-height: 1.6;
|
|
|
-}
|
|
|
-
|
|
|
-/* 底部样式 */
|
|
|
-.overlay-footer {
|
|
|
- display: flex;
|
|
|
- justify-content: flex-end;
|
|
|
- gap: 10px;
|
|
|
-}
|
|
|
-
|
|
|
-/* 按钮样式 */
|
|
|
-.btn {
|
|
|
- padding: 10px 20px;
|
|
|
- border: none;
|
|
|
- border-radius: 6px;
|
|
|
- cursor: pointer;
|
|
|
- font-size: 1rem;
|
|
|
- font-weight: 500;
|
|
|
- transition: all 0.2s ease;
|
|
|
-}
|
|
|
-
|
|
|
-.btn-primary {
|
|
|
- background-color: #3B82F6;
|
|
|
- color: white;
|
|
|
-}
|
|
|
-
|
|
|
-.btn-primary:hover {
|
|
|
- background-color: #2563EB;
|
|
|
- transform: scale(1.05);
|
|
|
-}
|
|
|
-
|
|
|
-.btn-close {
|
|
|
- background: none;
|
|
|
- border: none;
|
|
|
- cursor: pointer;
|
|
|
- color: #666;
|
|
|
- font-size: 1.5rem;
|
|
|
- transition: color 0.2s ease;
|
|
|
- padding: 0 5px;
|
|
|
-}
|
|
|
-
|
|
|
-.btn-close:hover {
|
|
|
- color: #333;
|
|
|
-}
|
|
|
-
|
|
|
-/* 动画定义 - 淡入淡出 */
|
|
|
-@keyframes overlayFadeIn {
|
|
|
- from { opacity: 0; }
|
|
|
- to { opacity: 1; }
|
|
|
-}
|
|
|
-
|
|
|
-@keyframes overlayFadeOut {
|
|
|
- from { opacity: 1; }
|
|
|
- to { opacity: 0; }
|
|
|
-}
|
|
|
-
|
|
|
-@keyframes contentFadeIn {
|
|
|
- from { opacity: 0; }
|
|
|
- to { opacity: 1; }
|
|
|
-}
|
|
|
-
|
|
|
-@keyframes contentFadeOut {
|
|
|
- from { opacity: 1; }
|
|
|
- to { opacity: 0; }
|
|
|
-}
|
|
|
-
|
|
|
-/* 动画定义 - 缩放 */
|
|
|
-@keyframes overlayScaleIn {
|
|
|
- from { opacity: 0; }
|
|
|
- to { opacity: 1; }
|
|
|
-}
|
|
|
-
|
|
|
-@keyframes overlayScaleOut {
|
|
|
- from { opacity: 1; }
|
|
|
- to { opacity: 0; }
|
|
|
-}
|
|
|
-
|
|
|
-@keyframes contentScaleIn {
|
|
|
- from {
|
|
|
- opacity: 0;
|
|
|
- transform: scale(0.95);
|
|
|
- }
|
|
|
- to {
|
|
|
- opacity: 1;
|
|
|
- transform: scale(1);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-@keyframes contentScaleOut {
|
|
|
- from {
|
|
|
- opacity: 1;
|
|
|
- transform: scale(1);
|
|
|
- }
|
|
|
- to {
|
|
|
- opacity: 0;
|
|
|
- transform: scale(0.95);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/* 动画定义 - 滑动 */
|
|
|
-@keyframes overlaySlideIn {
|
|
|
- from { opacity: 0; }
|
|
|
- to { opacity: 1; }
|
|
|
-}
|
|
|
-
|
|
|
-@keyframes overlaySlideOut {
|
|
|
- from { opacity: 1; }
|
|
|
- to { opacity: 0; }
|
|
|
-}
|
|
|
-
|
|
|
-@keyframes contentSlideIn {
|
|
|
- from {
|
|
|
- opacity: 0;
|
|
|
- transform: translateY(30px);
|
|
|
- }
|
|
|
- to {
|
|
|
- opacity: 1;
|
|
|
- transform: translateY(0);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-@keyframes contentSlideOut {
|
|
|
- from {
|
|
|
- opacity: 1;
|
|
|
- transform: translateY(0);
|
|
|
- }
|
|
|
- to {
|
|
|
- opacity: 0;
|
|
|
- transform: translateY(30px);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/* 动画类 */
|
|
|
-.animate-overlay-fade-in {
|
|
|
- animation: overlayFadeIn var(--animate-duration) ease-in-out forwards;
|
|
|
-}
|
|
|
-
|
|
|
-.animate-overlay-fade-out {
|
|
|
- animation: overlayFadeOut var(--animate-duration) ease-in-out forwards;
|
|
|
-}
|
|
|
-
|
|
|
-.animate-content-fade-in {
|
|
|
- animation: contentFadeIn var(--animate-duration) ease-in-out forwards;
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
+ z-index: 999;
|
|
|
}
|
|
|
|
|
|
-.animate-content-fade-out {
|
|
|
- animation: contentFadeOut var(--animate-duration) ease-in-out forwards;
|
|
|
+/* 遮罩淡入淡出 */
|
|
|
+.fade-enter-active,
|
|
|
+.fade-leave-active {
|
|
|
+ transition: opacity 0.3s;
|
|
|
}
|
|
|
-
|
|
|
-.animate-overlay-scale-in {
|
|
|
- animation: overlayScaleIn var(--animate-duration) ease-in-out forwards;
|
|
|
-}
|
|
|
-
|
|
|
-.animate-overlay-scale-out {
|
|
|
- animation: overlayScaleOut var(--animate-duration) ease-in-out forwards;
|
|
|
-}
|
|
|
-
|
|
|
-.animate-content-scale-in {
|
|
|
- animation: contentScaleIn var(--animate-duration) ease-out forwards;
|
|
|
-}
|
|
|
-
|
|
|
-.animate-content-scale-out {
|
|
|
- animation: contentScaleOut var(--animate-duration) ease-in forwards;
|
|
|
-}
|
|
|
-
|
|
|
-.animate-overlay-slide-in {
|
|
|
- animation: overlaySlideIn var(--animate-duration) ease-in-out forwards;
|
|
|
-}
|
|
|
-
|
|
|
-.animate-overlay-slide-out {
|
|
|
- animation: overlaySlideOut var(--animate-duration) ease-in-out forwards;
|
|
|
+.fade-enter,
|
|
|
+.fade-leave-to {
|
|
|
+ opacity: 0;
|
|
|
}
|
|
|
|
|
|
-.animate-content-slide-in {
|
|
|
- animation: contentSlideIn var(--animate-duration) ease-out forwards;
|
|
|
+/* 内容从底部滑出 */
|
|
|
+.slide-up-enter-active,
|
|
|
+.slide-up-leave-active {
|
|
|
+ transition: transform 0.3s ease-out;
|
|
|
}
|
|
|
-
|
|
|
-.animate-content-slide-out {
|
|
|
- animation: contentSlideOut var(--animate-duration) ease-in forwards;
|
|
|
+.slide-up-enter,
|
|
|
+.slide-up-leave-to {
|
|
|
+ transform: translate3d(0, 100%, 0);
|
|
|
}
|
|
|
</style>
|