Эх сурвалжийг харах

feat: leave-source弹窗逻辑优化

cuiyalong 3 долоо хоног өмнө
parent
commit
f60a162f8b

+ 1 - 0
plugins/leave-source/src/assets/style/common.scss

@@ -1,5 +1,6 @@
 @import './mixin';
 @import './variables.scss';
+@import './layout.scss';
 
 html {
   height: 100%;

+ 13 - 0
plugins/leave-source/src/assets/style/layout.scss

@@ -0,0 +1,13 @@
+.j-container {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+}
+.j-main {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  overflow-y: scroll;
+  overflow-x: hidden;
+}

+ 58 - 369
plugins/leave-source/src/components/dialog/AnimatedOverlay.vue

@@ -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>

+ 7 - 6
plugins/leave-source/src/App.vue → plugins/leave-source/src/example.vue

@@ -7,9 +7,9 @@
       mobile弹框
     </button>
 
-    <!-- <PCLeaveDialog :visible.sync="pcDialogVisible" /> -->
-    <!-- <MobileLeavePopup /> -->
-    <PCContentCard />
+    <PCLeaveDialog :visible.sync="pcVisible" />
+    <MobileLeavePopup :visible.sync="mobileVisible" />
+    <!-- <PCContentCard /> -->
     <!-- <MobileContentCard /> -->
   </div>
 </template>
@@ -30,7 +30,8 @@ export default {
   },
   data() {
     return {
-      pcDialogVisible: false,
+      pcVisible: false,
+      mobileVisible: false,
     }
   },
   computed: {},
@@ -39,10 +40,10 @@ export default {
   methods: {
     handle(type) {
       if (type === 'pc') {
-        this.pcDialogVisible = true
+        this.pcVisible = true
       }
       else {
-        console.log(type)
+        this.mobileVisible = true
       }
     }
   }

+ 66 - 0
plugins/leave-source/src/lib/mobile/components/PopupLayout.vue

@@ -0,0 +1,66 @@
+<template>
+  <div class="popup-layout j-container">
+    <div class="j-header">
+      <slot name="header">
+        <div class="header-nav">
+          <div class="header-nav-title">
+            <slot name="title">
+              {{ title }}
+            </slot>
+          </div>
+          <div
+            class="header-nav-close"
+            name="clear"
+            @click="closeIconClick"
+          />
+        </div>
+      </slot>
+    </div>
+    <div class="j-main">
+      <slot name="default" />
+    </div>
+    <div class="j-footer">
+      <slot name="footer" />
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'PopupLayout',
+  props: {
+    title: {
+      type: String,
+      default: '选择'
+    }
+  },
+  methods: {
+    closeIconClick() {
+      this.$emit('closeIconClick')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.header-nav {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  height: 64px;
+  padding: 0 16px;
+  .header-nav-title {
+    font-size: 20px;
+    color: #171826;
+  }
+  .header-nav-close {
+    font-size: 22px;
+    color: #c8c9cc;
+  }
+}
+.popup-layout {
+  .j-main > * {
+    max-height: 60vh;
+  }
+}
+</style>

+ 42 - 6
plugins/leave-source/src/lib/mobile/content-popup.vue

@@ -1,24 +1,60 @@
 <template>
-  <div>
-    <ContentCard />
-  </div>
+  <AnimatedOverlay
+    class="mobile-leave-dialog"
+    :visible="visible"
+    content-transition-name="slide-up"
+    @update:visible="updateVisible"
+    @close="close"
+  >
+    <PopupLayout>
+      <ContentCard />
+    </PopupLayout>
+  </AnimatedOverlay>
 </template>
 
 <script>
+import AnimatedOverlay from '../../components/dialog/AnimatedOverlay.vue'
+import PopupLayout from './components/PopupLayout.vue'
 import ContentCard from './content-card.vue'
 
 export default {
   name: 'MobileContentPopover',
   components: {
+    AnimatedOverlay,
+    PopupLayout,
     ContentCard
   },
-  data() {
-    return {}
+  props: {
+    visible: {
+      type: Boolean,
+      default: false
+    }
+  },
+  methods: {
+    updateVisible(e) {
+      this.$emit('update:visible', e)
+    },
+    close() {
+      this.$emit('close')
+    },
   },
-  methods: {}
 }
 </script>
 
 <style scoped lang="scss">
+.mobile-leave-dialog {
+  display: flex;
+  align-items: flex-end; /* 内容置底 */
+}
+::v-deep {
 
+  .overlay-content {
+    width: 100%;
+    max-height: 70vh;
+    background: #fff;
+    border-radius: 16px 16px 0 0;
+    overflow-y: auto;
+    box-sizing: border-box;
+  }
+}
 </style>

+ 0 - 1
plugins/leave-source/src/lib/pc/content-card.vue

@@ -1,6 +1,5 @@
 <template>
   <div>
-    content-card-pc
     <LeaveCommon />
   </div>
 </template>

+ 3 - 2
plugins/leave-source/src/lib/pc/content-dialog.vue

@@ -1,5 +1,6 @@
 <template>
   <AnimatedOverlay
+    class="pc-leave-dialog"
     :visible="visible"
     @update:visible="updateVisible"
     @close="close"
@@ -30,8 +31,8 @@ export default {
     },
     close() {
       this.$emit('close')
-    }
-  }
+    },
+  },
 }
 </script>
 

+ 2 - 2
plugins/leave-source/src/main.js

@@ -1,10 +1,10 @@
 import Vue from 'vue'
-import App from './App.vue'
+import example from './example.vue'
 import Toast from './components/toast/index'
 import './assets/style/common.scss'
 
 Vue.use(Toast)
 
 new Vue({
-  render: h => h(App)
+  render: h => h(example)
 }).$mount('#app')