|
|
@@ -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);
|
|
|
},
|
|
|
|
|
|
// ==================== 号码管理 ====================
|