|
@@ -0,0 +1,227 @@
|
|
|
+<template>
|
|
|
+ <div
|
|
|
+ class="float-service"
|
|
|
+ @touchstart="startDrag"
|
|
|
+ @touchmove="onDrag"
|
|
|
+ @touchend="endDrag"
|
|
|
+ @click="handleServiceClick"
|
|
|
+ :style="serviceStyle"
|
|
|
+ >
|
|
|
+ <div class="service-icon">
|
|
|
+ <svg t="1751785432156" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4282" width="24" height="24"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z" fill="#ffffff" p-id="4283"></path><path d="M464 336h-64c-4.4 0-8 3.6-8 8v384c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V592h56c17.7 0 32 14.3 32 32v104c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V624c0-57.3-46.7-104-104-104h-40V344c0-4.4-3.6-8-8-8z" fill="#ffffff" p-id="4284"></path><path d="M672 464c26.5 0 48-21.5 48-48s-21.5-48-48-48-48 21.5-48 48 21.5 48 48 48z m0 160c26.5 0 48-21.5 48-48s-21.5-48-48-48-48 21.5-48 48 21.5 48 48 48z" fill="#ffffff" p-id="4285"></path></svg>
|
|
|
+ </div>
|
|
|
+ <div class="service-text">
|
|
|
+ <span>在</span>
|
|
|
+ <span>线</span>
|
|
|
+ <span>客</span>
|
|
|
+ <span>服</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+export default {
|
|
|
+ name: 'FloatService',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ servicePosition: {
|
|
|
+ x: 0,
|
|
|
+ y: window.innerHeight / 2
|
|
|
+ },
|
|
|
+ isDragging: false,
|
|
|
+ startX: 0,
|
|
|
+ startY: 0,
|
|
|
+ lastValidX: 0,
|
|
|
+ lastValidY: 0,
|
|
|
+ dragStartTime: 0,
|
|
|
+ isDragMove: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ serviceStyle() {
|
|
|
+ return {
|
|
|
+ transform: `translate(${this.servicePosition.x}px, ${this.servicePosition.y}px)`,
|
|
|
+ transition: this.isDragging ? 'none' : 'all 0.3s ease'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ startDrag(event) {
|
|
|
+ this.isDragging = true;
|
|
|
+ this.dragStartTime = Date.now();
|
|
|
+ this.isDragMove = false;
|
|
|
+ const touch = event.touches[0];
|
|
|
+ this.startX = touch.clientX - this.servicePosition.x;
|
|
|
+ this.startY = touch.clientY - this.servicePosition.y;
|
|
|
+ this.lastValidX = this.servicePosition.x;
|
|
|
+ this.lastValidY = this.servicePosition.y;
|
|
|
+
|
|
|
+ // 阻止默认事件,防止触发click
|
|
|
+ event.preventDefault();
|
|
|
+ },
|
|
|
+ onDrag(event) {
|
|
|
+ if (!this.isDragging) return;
|
|
|
+ this.isDragMove = true;
|
|
|
+ const touch = event.touches[0];
|
|
|
+
|
|
|
+ // 计算新位置
|
|
|
+ let newX = touch.clientX - this.startX;
|
|
|
+ let newY = touch.clientY - this.startY;
|
|
|
+
|
|
|
+ // 限制Y轴范围
|
|
|
+ const maxY = window.innerHeight - 100;
|
|
|
+ newY = Math.max(50, Math.min(newY, maxY));
|
|
|
+
|
|
|
+ // 更新位置
|
|
|
+ this.servicePosition.x = newX;
|
|
|
+ this.servicePosition.y = newY;
|
|
|
+
|
|
|
+ // 阻止页面滚动
|
|
|
+ event.preventDefault();
|
|
|
+ },
|
|
|
+ endDrag(event) {
|
|
|
+ if (!this.isDragging) return;
|
|
|
+
|
|
|
+ // 如果拖动时间小于200ms且移动距离小于10px,认为是点击
|
|
|
+ const dragTime = Date.now() - this.dragStartTime;
|
|
|
+ const dragDistance = Math.abs(this.servicePosition.x - this.lastValidX) +
|
|
|
+ Math.abs(this.servicePosition.y - this.lastValidY);
|
|
|
+
|
|
|
+ if (dragTime < 200 && dragDistance < 10 && !this.isDragMove) {
|
|
|
+ this.$emit('service-click');
|
|
|
+ }
|
|
|
+
|
|
|
+ this.isDragging = false;
|
|
|
+ this.isDragMove = false;
|
|
|
+
|
|
|
+ // 计算是否吸附到左边还是右边
|
|
|
+ const windowWidth = window.innerWidth;
|
|
|
+ const halfWidth = windowWidth / 2;
|
|
|
+
|
|
|
+ // 如果在左半边,吸附到左边,否则吸附到右边
|
|
|
+ // 考虑按钮宽度(22px),确保完全贴边
|
|
|
+ this.servicePosition.x = this.servicePosition.x < halfWidth ? 0 : (windowWidth - 30);
|
|
|
+
|
|
|
+ // 保存位置
|
|
|
+ localStorage.setItem('servicePosition', JSON.stringify(this.servicePosition));
|
|
|
+
|
|
|
+ // 阻止默认事件,防止触发click
|
|
|
+ event.preventDefault();
|
|
|
+ },
|
|
|
+ handleServiceClick(event) {
|
|
|
+ // 如果是通过触摸操作触发的,忽略click事件
|
|
|
+ if (this.isDragging || this.isDragMove) {
|
|
|
+ event.preventDefault();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 只处理非触摸设备的点击
|
|
|
+ if (!('ontouchstart' in window)) {
|
|
|
+ this.$emit('service-click');
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleResize() {
|
|
|
+ // 确保按钮在窗口大小改变时仍然可见
|
|
|
+ const windowWidth = window.innerWidth;
|
|
|
+ const windowHeight = window.innerHeight;
|
|
|
+
|
|
|
+ // 限制Y轴范围
|
|
|
+ const maxY = windowHeight - 100;
|
|
|
+ this.servicePosition.y = Math.max(50, Math.min(this.servicePosition.y, maxY));
|
|
|
+
|
|
|
+ // 确保X轴吸附正确,考虑按钮宽度
|
|
|
+ this.servicePosition.x = this.servicePosition.x > windowWidth / 2 ? (windowWidth - 22) : 0;
|
|
|
+
|
|
|
+ // 保存新位置
|
|
|
+ localStorage.setItem('servicePosition', JSON.stringify(this.servicePosition));
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ // 从localStorage获取上次保存的位置
|
|
|
+ const savedPosition = localStorage.getItem('servicePosition');
|
|
|
+ if (savedPosition) {
|
|
|
+ try {
|
|
|
+ const position = JSON.parse(savedPosition);
|
|
|
+ // 确保位置在有效范围内
|
|
|
+ const windowWidth = window.innerWidth;
|
|
|
+ const windowHeight = window.innerHeight;
|
|
|
+ this.servicePosition = {
|
|
|
+ x: position.x > windowWidth / 2 ? windowWidth - 60 : 0,
|
|
|
+ y: Math.max(50, Math.min(position.y, windowHeight - 100))
|
|
|
+ };
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Failed to parse saved position:', e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 监听窗口大小变化
|
|
|
+ window.addEventListener('resize', this.handleResize);
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ // 保存位置到localStorage
|
|
|
+ localStorage.setItem('servicePosition', JSON.stringify(this.servicePosition));
|
|
|
+ window.removeEventListener('resize', this.handleResize);
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.float-service {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ background-color: #1989fa;
|
|
|
+ border-radius: 20px;
|
|
|
+ padding: 10px 5px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ cursor: pointer;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
|
+ z-index: 999;
|
|
|
+ touch-action: none;
|
|
|
+ user-select: none;
|
|
|
+ width: 22px;
|
|
|
+}
|
|
|
+
|
|
|
+.service-icon {
|
|
|
+ margin-bottom: 5px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.service-text {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ color: white;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+
|
|
|
+.service-text span {
|
|
|
+ display: block;
|
|
|
+ text-align: center;
|
|
|
+ margin: 2px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.float-service:active {
|
|
|
+ background-color: #0e7bfa;
|
|
|
+}
|
|
|
+
|
|
|
+/* 移动端适配 */
|
|
|
+@media (max-width: 375px) {
|
|
|
+ .float-service {
|
|
|
+ padding: 8px 4px;
|
|
|
+ width: 40px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .service-icon svg {
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .service-text {
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|