吴树波 3 дней назад
Родитель
Сommit
060bba089e
2 измененных файлов с 117 добавлено и 38 удалено
  1. BIN
      src/assets/来电铃声.mp3
  2. 117 38
      src/components/FloatingSoftPhone/index.vue

BIN
src/assets/来电铃声.mp3


+ 117 - 38
src/components/FloatingSoftPhone/index.vue

@@ -301,6 +301,10 @@ const UI_STATE = {
   TALKING: 'talking'
 };
 
+const FAB_SIZE = 56;
+const FAB_EDGE_OFFSET = 8;
+const FAB_MIN_VISIBLE = 28;
+
 export default {
   name: 'FloatingSoftPhone',
   directives: {
@@ -336,6 +340,9 @@ export default {
       fabDragOffsetY: 0,
       fabIsCollapsed: false,  // 是否折叠到边缘(半隐藏)
       fabDragMoved: false,  // 拖拽过程中是否产生了位移(区分点击和拖拽)
+      fabEdge: 'right',     // 贴边方向:left | right
+      fabYRatio: 0.85,      // 相对视口高度的纵向位置(0~1)
+      _resizeTimer: null,
 
       // ===== 拨号与显示 =====
       dialNumber: '',
@@ -500,8 +507,10 @@ export default {
       return this.callStatus !== 'idle' ? 'call_end' : 'phone';
     },
     fabOnLeftEdge() {
+      if (this.fabEdge === 'left') return true;
+      if (this.fabEdge === 'right') return false;
       if (this.fabX === null) return false;
-      return this.fabX + 28 < window.innerWidth / 2;
+      return this.fabX + FAB_SIZE / 2 < this.getViewportSize().width / 2;
     },
     /** 来电振铃(左右接听/拒接,隐藏中间按钮) */
     isIncomingRinging() {
@@ -576,7 +585,18 @@ export default {
     this.loadPosition();
     // 加载FAB按钮位置
     this.loadFabPosition();
-    window.addEventListener('resize', this.handleWindowResize);
+    this._boundViewportResize = this.handleWindowResize.bind(this);
+    window.addEventListener('resize', this._boundViewportResize);
+    if (window.visualViewport) {
+      window.visualViewport.addEventListener('resize', this._boundViewportResize);
+      window.visualViewport.addEventListener('scroll', this._boundViewportResize);
+    }
+    this.$nextTick(() => {
+      if (this.$el && this.$el.parentNode && this.$el.parentNode !== document.body) {
+        document.body.appendChild(this.$el);
+      }
+      this.adjustFabForViewport();
+    });
 
     // 如果不在原softPhone页面,初始化连接
     if (!this.isOnSoftPhonePage) {
@@ -599,7 +619,17 @@ export default {
   beforeDestroy() {
     this.destroyAllConnections();
     window.removeEventListener('beforeunload', this.handleBeforeUnload);
-    window.removeEventListener('resize', this.handleWindowResize);
+    if (this._boundViewportResize) {
+      window.removeEventListener('resize', this._boundViewportResize);
+      if (window.visualViewport) {
+        window.visualViewport.removeEventListener('resize', this._boundViewportResize);
+        window.visualViewport.removeEventListener('scroll', this._boundViewportResize);
+      }
+    }
+    if (this._resizeTimer) {
+      clearTimeout(this._resizeTimer);
+      this._resizeTimer = null;
+    }
     this.clearAllTimers();
     this.removeEventListeners();
     this.$root.$off('floating-softphone-dial', this.dialExternal);
@@ -611,6 +641,43 @@ export default {
     this.$root.$off('cc-phonebar-reconnect-requested', this.onCCPhoneBarReconnectRequested);
   },
   methods: {
+    getViewportSize() {
+      const vv = window.visualViewport;
+      return {
+        width: vv ? vv.width : window.innerWidth,
+        height: vv ? vv.height : window.innerHeight
+      };
+    },
+    applyFabFromRelative(edge, yRatio) {
+      const { width, height } = this.getViewportSize();
+      this.fabEdge = edge === 'left' ? 'left' : 'right';
+      this.fabYRatio = Math.max(0, Math.min(1, yRatio));
+      this.fabX = this.fabEdge === 'left'
+        ? -FAB_EDGE_OFFSET
+        : width - FAB_SIZE + FAB_EDGE_OFFSET;
+      this.fabY = Math.max(0, Math.min(this.fabYRatio * height, height - FAB_SIZE));
+    },
+    syncFabRelativeFromAbsolute() {
+      const { width, height } = this.getViewportSize();
+      this.fabEdge = this.fabX + FAB_SIZE / 2 < width / 2 ? 'left' : 'right';
+      this.fabYRatio = height > 0 ? Math.max(0, Math.min(1, this.fabY / height)) : 0.85;
+    },
+    adjustFabForViewport() {
+      if (this.fabX === null && !this.fabEdge) return;
+      const { width, height } = this.getViewportSize();
+      if (this.fabIsCollapsed && !this.panelVisible) {
+        const edge = this.fabEdge || (this.fabOnLeftEdge() ? 'left' : 'right');
+        const yRatio = this.fabYRatio != null ? this.fabYRatio : (height > 0 ? this.fabY / height : 0.85);
+        this.applyFabFromRelative(edge, yRatio);
+        return;
+      }
+      const minX = -(FAB_SIZE - FAB_MIN_VISIBLE);
+      const maxX = width - FAB_MIN_VISIBLE;
+      this.fabX = Math.max(minX, Math.min(this.fabX, maxX));
+      this.fabY = Math.max(0, Math.min(this.fabY, height - FAB_SIZE));
+      this.syncFabRelativeFromAbsolute();
+    },
+
     // ==================== FAB按钮拖拽与吸边 ====================
     onFabDragStart(e) {
       if (e.button !== 0) return;
@@ -633,12 +700,12 @@ export default {
     onFabDrag(e) {
       if (!this.fabIsDragging) return;
       this.fabDragMoved = true;
-      const fabSize = 56;
+      const { width, height } = this.getViewportSize();
       let x = e.clientX - this.fabDragOffsetX;
       let y = e.clientY - this.fabDragOffsetY;
       // 允许超出边缘一半(折叠效果)
-      x = Math.max(-fabSize / 2, Math.min(x, window.innerWidth - fabSize / 2));
-      y = Math.max(0, Math.min(y, window.innerHeight - fabSize));
+      x = Math.max(-FAB_SIZE / 2, Math.min(x, width - FAB_SIZE / 2));
+      y = Math.max(0, Math.min(y, height - FAB_SIZE));
       this.fabX = x;
       this.fabY = y;
       // 拖拽时取消折叠状态
@@ -654,15 +721,12 @@ export default {
       this.saveFabPosition();
     },
     snapFabToEdge() {
-      if (this.fabX === null) return;
-      const fabSize = 56;
-      const centerX = this.fabX + fabSize / 2;
-      // 吸附到左边或右边
-      if (centerX < window.innerWidth / 2) {
-        this.fabX = -8;  // 左侧折叠偏移
-      } else {
-        this.fabX = window.innerWidth - fabSize + 8;  // 右侧折叠偏移
-      }
+      if (this.fabX === null && !this.fabEdge) return;
+      const { width, height } = this.getViewportSize();
+      const centerX = this.fabX != null ? this.fabX + FAB_SIZE / 2 : (this.fabEdge === 'left' ? 0 : width);
+      const edge = centerX < width / 2 ? 'left' : 'right';
+      const yRatio = height > 0 ? this.fabY / height : (this.fabYRatio || 0.85);
+      this.applyFabFromRelative(edge, yRatio);
       // 面板打开时不折叠,避免影响操作
       this.fabIsCollapsed = !this.panelVisible;
     },
@@ -676,39 +740,47 @@ export default {
       if (this.fabIsCollapsed) {
         this.fabIsCollapsed = false;
         this.panelVisible = true;
+        this.$nextTick(() => {
+          const { width, height } = this.getViewportSize();
+          this.fabX = this.fabEdge === 'left' ? 16 : width - FAB_SIZE - 16;
+          this.fabY = Math.max(0, Math.min(this.fabYRatio * height, height - FAB_SIZE));
+        });
         return;
       }
       this.togglePanel();
     },
     saveFabPosition() {
-      if (this.fabX !== null) {
-        localStorage.setItem('FloatingSoftPhoneFabPosition', JSON.stringify({
-          x: this.fabX, y: this.fabY
-        }));
-      }
+      if (this.fabX === null) return;
+      this.syncFabRelativeFromAbsolute();
+      localStorage.setItem('FloatingSoftPhoneFabPosition', JSON.stringify({
+        edge: this.fabEdge,
+        yRatio: this.fabYRatio,
+        x: this.fabX,
+        y: this.fabY
+      }));
     },
     loadFabPosition() {
       try {
         const saved = JSON.parse(localStorage.getItem('FloatingSoftPhoneFabPosition'));
-        if (saved && saved.x !== null && saved.x !== undefined) {
-          this.fabX = saved.x;
-          this.fabY = saved.y;
+        if (saved) {
+          if (saved.edge != null && saved.yRatio != null) {
+            this.applyFabFromRelative(saved.edge, saved.yRatio);
+          } else if (saved.x != null && saved.y != null) {
+            const { width, height } = this.getViewportSize();
+            const edge = (saved.x + FAB_SIZE / 2) < width / 2 ? 'left' : 'right';
+            const yRatio = height > 0 ? Math.max(0, Math.min(1, saved.y / height)) : 0.85;
+            this.applyFabFromRelative(edge, yRatio);
+          }
         }
       } catch (e) { /* ignore */ }
-      // 如果没有保存的位置,初始化到右下角
       if (this.fabX === null) {
-        this.fabX = window.innerWidth - 56 + 8;  // 右侧贴边偏移
-        this.fabY = window.innerHeight - 56 - 24;
+        this.applyFabFromRelative('right', 0.85);
+      } else {
+        this.adjustFabForViewport();
       }
       // 初始加载时折叠贴边
       this.fabIsCollapsed = true;
     },
-    clampFabPosition() {
-      if (this.fabX === null) return;
-      const fabSize = 56;
-      this.fabX = Math.max(-fabSize / 2, Math.min(this.fabX, window.innerWidth - fabSize / 2));
-      this.fabY = Math.max(0, Math.min(this.fabY, window.innerHeight - fabSize));
-    },
 
     // ==================== 外部拨号接口 ====================
     /**
@@ -736,6 +808,7 @@ export default {
       this.panelVisible = !this.panelVisible;
       // 面板关闭后自动折叠FAB,面板打开时展开FAB
       this.fabIsCollapsed = !this.panelVisible && this.fabX !== null;
+      this.$nextTick(() => this.adjustFabForViewport());
     },
     hidePanel() {
       if (this.callStatus !== 'idle') {
@@ -744,6 +817,7 @@ export default {
       }
       this.panelVisible = false;
       this.fabIsCollapsed = this.fabX !== null;
+      this.$nextTick(() => this.adjustFabForViewport());
     },
 
     // ==================== 拖拽功能 ====================
@@ -764,10 +838,11 @@ export default {
       if (!this.isDragging) return;
       const panel = this.$refs.panel;
       if (!panel) return;
+      const { width, height } = this.getViewportSize();
       let x = e.clientX - this.dragOffsetX;
       let y = e.clientY - this.dragOffsetY;
-      const maxX = window.innerWidth - panel.offsetWidth;
-      const maxY = window.innerHeight - panel.offsetHeight;
+      const maxX = width - panel.offsetWidth;
+      const maxY = height - panel.offsetHeight;
       x = Math.max(0, Math.min(x, maxX));
       y = Math.max(0, Math.min(y, maxY));
       this.panelX = x;
@@ -802,15 +877,19 @@ export default {
       this.$nextTick(() => {
         const panel = this.$refs.panel;
         if (!panel) return;
-        const maxX = window.innerWidth - panel.offsetWidth;
-        const maxY = window.innerHeight - panel.offsetHeight;
+        const { width, height } = this.getViewportSize();
+        const maxX = width - panel.offsetWidth;
+        const maxY = height - panel.offsetHeight;
         this.panelX = Math.max(0, Math.min(this.panelX, maxX));
         this.panelY = Math.max(0, Math.min(this.panelY, maxY));
       });
     },
     handleWindowResize() {
-      this.clampPosition();
-      this.clampFabPosition();
+      if (this._resizeTimer) clearTimeout(this._resizeTimer);
+      this._resizeTimer = setTimeout(() => {
+        this.clampPosition();
+        this.adjustFabForViewport();
+      }, 80);
     },
 
     // ==================== 号码管理 ====================