feat(components): 重构浮动面板组件并添加新功能
- 重写了 floating2 组件,增加了更多
This commit is contained in:
parent
4d358d130b
commit
e0853e1416
@ -1,93 +1,238 @@
|
||||
<template>
|
||||
<div
|
||||
ref="bubbleRef"
|
||||
class="floating-bubble"
|
||||
:style="bubbleStyle"
|
||||
@touchstart="onTouchStart"
|
||||
@touchmove="onTouchMove"
|
||||
@touchend="onTouchEnd"
|
||||
>
|
||||
<div ref="floatPanel" :style="panelStyle" class="float-panel"
|
||||
@mousedown="handleMouseDown"
|
||||
@touchstart="handleTouchStart">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useThrottleFn } from '@vueuse/core'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
offset: {
|
||||
type: Number,
|
||||
default: 0
|
||||
position: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
fixedX: Boolean,
|
||||
fixedY: Boolean,
|
||||
snapEdge: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const bubbleRef = ref(null)
|
||||
const position = ref({
|
||||
right: props.offset,
|
||||
bottom: 100
|
||||
})
|
||||
const floatPanel = ref(null);
|
||||
const panelPos = ref({ x: 0, y: 0 });
|
||||
const panelSize = ref({ width: 0, height: 0 });
|
||||
const windowSize = ref({ width: 0, height: 0 });
|
||||
const dragOffset = ref({ x: 0, y: 0 });
|
||||
const wasDragged = ref(false);
|
||||
|
||||
let startTouch = { x: 0, y: 0 }
|
||||
let startPos = { right: 0, bottom: 0 }
|
||||
let isDragging = false
|
||||
|
||||
const bubbleStyle = computed(() => ({
|
||||
position: 'fixed',
|
||||
right: `${position.value.right}px`,
|
||||
bottom: `${position.value.bottom}px`,
|
||||
zIndex: 99999,
|
||||
transition: isDragging ? 'none' : 'all 0.3s'
|
||||
}))
|
||||
|
||||
const onTouchStart = (event) => {
|
||||
isDragging = true
|
||||
const touch = event.touches[0]
|
||||
startTouch = { x: touch.clientX, y: touch.clientY }
|
||||
startPos = { ...position.value }
|
||||
}
|
||||
|
||||
const onTouchMove = useThrottleFn((event) => {
|
||||
if (!isDragging) return
|
||||
event.preventDefault()
|
||||
|
||||
const touch = event.touches[0]
|
||||
const { clientWidth, clientHeight } = document.documentElement
|
||||
const rect = bubbleRef.value.getBoundingClientRect()
|
||||
|
||||
// 计算新位置
|
||||
const deltaX = startTouch.x - touch.clientX
|
||||
const deltaY = startTouch.y - touch.clientY
|
||||
const newRight = startPos.right + deltaX
|
||||
const newBottom = startPos.bottom + deltaY
|
||||
|
||||
// 边界检测
|
||||
position.value = {
|
||||
right: Math.min(Math.max(newRight, props.offset),
|
||||
clientWidth - rect.width - props.offset),
|
||||
bottom: Math.min(Math.max(newBottom, props.offset),
|
||||
clientHeight - rect.height - props.offset)
|
||||
// 更新面板和窗口尺寸信息
|
||||
const updateSizes = () => {
|
||||
if (floatPanel.value) {
|
||||
panelSize.value = {
|
||||
width: floatPanel.value.offsetWidth,
|
||||
height: floatPanel.value.offsetHeight
|
||||
};
|
||||
}
|
||||
}, 16)
|
||||
windowSize.value = {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
};
|
||||
};
|
||||
|
||||
const onTouchEnd = () => {
|
||||
if (!isDragging) return
|
||||
isDragging = false
|
||||
// 初始位置计算
|
||||
const initPosition = () => {
|
||||
updateSizes();
|
||||
const { left, right, top, bottom } = props.position;
|
||||
|
||||
const { clientWidth } = document.documentElement
|
||||
const rect = bubbleRef.value.getBoundingClientRect()
|
||||
const left = clientWidth - position.value.right - rect.width
|
||||
if (right !== undefined) {
|
||||
panelPos.value.x = windowSize.value.width - parseInt(right || 0) - panelSize.value.width;
|
||||
} else if (left !== undefined) {
|
||||
panelPos.value.x = parseInt(left || 0);
|
||||
}
|
||||
|
||||
// 吸附
|
||||
position.value.right = left < clientWidth / 2
|
||||
? clientWidth - rect.width - props.offset
|
||||
: props.offset
|
||||
}
|
||||
if (bottom !== undefined) {
|
||||
panelPos.value.y = windowSize.value.height - parseInt(bottom || 0) - panelSize.value.height;
|
||||
} else if (top !== undefined) {
|
||||
panelPos.value.y = parseInt(top || 0);
|
||||
}
|
||||
};
|
||||
|
||||
// 面板样式
|
||||
const panelStyle = computed(() => {
|
||||
return {
|
||||
left: `${panelPos.value.x}px`,
|
||||
top: `${panelPos.value.y}px`
|
||||
};
|
||||
});
|
||||
|
||||
// 处理鼠标按下事件
|
||||
const handleMouseDown = (event) => {
|
||||
// 只有点击浮动面板本身时才启动拖拽
|
||||
if (event.target === floatPanel.value || floatPanel.value.contains(event.target)) {
|
||||
// 判断是否是面板自身,如果是才阻止默认行为
|
||||
if (event.target === floatPanel.value) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
wasDragged.value = false;
|
||||
|
||||
const startX = event.clientX;
|
||||
const startY = event.clientY;
|
||||
|
||||
// 计算点击位置与面板左上角的偏移
|
||||
dragOffset.value = {
|
||||
x: startX - panelPos.value.x,
|
||||
y: startY - panelPos.value.y
|
||||
};
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
wasDragged.value = true;
|
||||
|
||||
const newX = e.clientX - dragOffset.value.x;
|
||||
const newY = e.clientY - dragOffset.value.y;
|
||||
|
||||
// 应用约束
|
||||
if (!props.fixedX) {
|
||||
panelPos.value.x = Math.max(0, Math.min(newX, windowSize.value.width - panelSize.value.width));
|
||||
}
|
||||
|
||||
if (!props.fixedY) {
|
||||
panelPos.value.y = Math.max(0, Math.min(newY, windowSize.value.height - panelSize.value.height));
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = (e) => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
|
||||
if (wasDragged.value && props.snapEdge) {
|
||||
snapToEdge();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理触摸开始事件
|
||||
const handleTouchStart = (event) => {
|
||||
// 只有触摸浮动面板本身时才启动拖拽
|
||||
if (event.target === floatPanel.value || floatPanel.value.contains(event.target)) {
|
||||
// 判断是否是面板自身,如果是才阻止默认行为
|
||||
if (event.target === floatPanel.value) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
wasDragged.value = false;
|
||||
|
||||
const touch = event.touches[0];
|
||||
const startX = touch.clientX;
|
||||
const startY = touch.clientY;
|
||||
|
||||
// 计算点击位置与面板左上角的偏移
|
||||
dragOffset.value = {
|
||||
x: startX - panelPos.value.x,
|
||||
y: startY - panelPos.value.y
|
||||
};
|
||||
|
||||
const handleTouchMove = (e) => {
|
||||
e.preventDefault();
|
||||
wasDragged.value = true;
|
||||
|
||||
const touch = e.touches[0];
|
||||
const newX = touch.clientX - dragOffset.value.x;
|
||||
const newY = touch.clientY - dragOffset.value.y;
|
||||
|
||||
// 应用约束
|
||||
if (!props.fixedX) {
|
||||
panelPos.value.x = Math.max(0, Math.min(newX, windowSize.value.width - panelSize.value.width));
|
||||
}
|
||||
|
||||
if (!props.fixedY) {
|
||||
panelPos.value.y = Math.max(0, Math.min(newY, windowSize.value.height - panelSize.value.height));
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchEnd = (e) => {
|
||||
document.removeEventListener('touchmove', handleTouchMove);
|
||||
document.removeEventListener('touchend', handleTouchEnd);
|
||||
|
||||
if (wasDragged.value && props.snapEdge) {
|
||||
snapToEdge();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||||
document.addEventListener('touchend', handleTouchEnd);
|
||||
}
|
||||
};
|
||||
|
||||
// 执行吸附
|
||||
const snapToEdge = () => {
|
||||
const centerX = windowSize.value.width / 2;
|
||||
const panelCenterX = panelPos.value.x + panelSize.value.width / 2;
|
||||
|
||||
if (panelCenterX < centerX) {
|
||||
panelPos.value.x = 0; // 吸附到左边
|
||||
console.log('吸附到左边边缘');
|
||||
} else {
|
||||
panelPos.value.x = windowSize.value.width - panelSize.value.width; // 吸附到右边
|
||||
console.log('吸附到右边边缘');
|
||||
}
|
||||
};
|
||||
|
||||
// 窗口大小变化处理
|
||||
const handleResize = () => {
|
||||
const oldWidth = windowSize.value.width;
|
||||
const oldHeight = windowSize.value.height;
|
||||
|
||||
updateSizes();
|
||||
|
||||
// 保持相对右边和底部的距离
|
||||
if (props.position.right !== undefined) {
|
||||
const rightDistance = oldWidth - (panelPos.value.x + panelSize.value.width);
|
||||
panelPos.value.x = windowSize.value.width - rightDistance - panelSize.value.width;
|
||||
}
|
||||
|
||||
if (props.position.bottom !== undefined) {
|
||||
const bottomDistance = oldHeight - (panelPos.value.y + panelSize.value.height);
|
||||
panelPos.value.y = windowSize.value.height - bottomDistance - panelSize.value.height;
|
||||
}
|
||||
|
||||
// 确保不超出边界
|
||||
panelPos.value.x = Math.max(0, Math.min(panelPos.value.x, windowSize.value.width - panelSize.value.width));
|
||||
panelPos.value.y = Math.max(0, Math.min(panelPos.value.y, windowSize.value.height - panelSize.value.height));
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
console.log('组件挂载, snapEdge =', props.snapEdge);
|
||||
updateSizes();
|
||||
initPosition();
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
snapToEdge
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.floating-bubble {
|
||||
touch-action: none;
|
||||
.float-panel {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
transition: left 0.2s ease-out;
|
||||
}
|
||||
</style>
|
||||
</style>
|
@ -6,7 +6,7 @@ import Cescribe from './components/Cescribe/index.vue'
|
||||
import FloatingBubble from '~/components/floating2/index.vue'
|
||||
|
||||
import {liveStore} from "~/stores/live/index.js";
|
||||
const {auctionDetail,getArtworkList} = goodStore();
|
||||
const {auctionDetail,getArtworkList,getAuctionDetail} = goodStore();
|
||||
const {fullLive} = liveStore()
|
||||
const changeLive = () => {
|
||||
if (!fullLive.value){
|
||||
@ -16,7 +16,7 @@ const changeLive = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await getAuctionDetail()
|
||||
|
||||
</script>
|
||||
<template>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import {ref, onMounted, onBeforeUnmount, watch} from 'vue'
|
||||
import {ref, onMounted, onBeforeUnmount, watch,useTemplateRef } from 'vue'
|
||||
import AliyunPlayer from 'aliyun-aliplayer'
|
||||
import 'aliyun-aliplayer/build/skins/default/aliplayer-min.css'
|
||||
import sideButton from '@/pages/liveRoom/components/SideButton/index.vue'
|
||||
@ -14,6 +14,9 @@ import {showConfirmDialog} from 'vant';
|
||||
import {artworkBuy} from "@/api/goods/index.js"
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import floating2 from '@/components/floating2/index.vue'
|
||||
import { UseDraggable } from '@vueuse/components'
|
||||
|
||||
|
||||
const { t } = useI18n()
|
||||
const { auctionDetail,getAuctionDetail} = goodStore();
|
||||
const player = ref(null)
|
||||
@ -127,7 +130,7 @@ const goBuy = async () => {
|
||||
message.success(t('live_room.success_mess'))
|
||||
}
|
||||
}
|
||||
|
||||
const sideButtonRef = useTemplateRef('sideButtonRef')
|
||||
const tipOpen = () => {
|
||||
message.warning(t('live_room.warn_mess'))
|
||||
}
|
||||
@ -139,13 +142,16 @@ const tipOpen = () => {
|
||||
<div v-if="loading1" class="absolute left-1/2 transform translate-x--1/2 top-1/2 translate-y--1/2">
|
||||
<van-loading type="spinner" >直播加载中...</van-loading>
|
||||
</div>
|
||||
|
||||
<client-only>
|
||||
</client-only>
|
||||
<transition name="fade">
|
||||
<div v-if="fullLive">
|
||||
|
||||
<floating2 top="200px">
|
||||
<sideButton></sideButton>
|
||||
</floating2>
|
||||
<floating2
|
||||
:snap-edge="true"
|
||||
:position="{right:0,top:300}"
|
||||
>
|
||||
<sideButton></sideButton>
|
||||
</floating2>
|
||||
<div class="absolute left-1/2 transform -translate-x-1/2 flex flex-col items-center"
|
||||
style="bottom:calc(var(--safe-area-inset-bottom) + 26px)">
|
||||
<div class="text-16px text-#FFB25F font-600 flex">
|
||||
@ -190,6 +196,21 @@ const tipOpen = () => {
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
.draggable-card {
|
||||
z-index: 999999;
|
||||
padding: 16px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
position: absolute;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
transition: box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.draggable-card.is-dragging {
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
/* 定义过渡动画 */
|
||||
.fade-enter-active {
|
||||
transition: opacity 1s ease;
|
||||
|
@ -20,6 +20,7 @@
|
||||
"@fingerprintjs/fingerprintjs": "^4.5.1",
|
||||
"@nuxtjs/i18n": "^9.1.1",
|
||||
"@vue-office/pdf": "^2.0.10",
|
||||
"@vueuse/components": "^12.8.2",
|
||||
"@vueuse/core": "^12.4.0",
|
||||
"aliyun-aliplayer": "^2.28.5",
|
||||
"axios": "^1.7.9",
|
||||
|
@ -20,6 +20,9 @@ importers:
|
||||
'@vue-office/pdf':
|
||||
specifier: ^2.0.10
|
||||
version: 2.0.10(vue-demi@0.14.10(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
|
||||
'@vueuse/components':
|
||||
specifier: ^12.8.2
|
||||
version: 12.8.2(typescript@5.7.3)
|
||||
'@vueuse/core':
|
||||
specifier: ^12.4.0
|
||||
version: 12.6.1(typescript@5.7.3)
|
||||
@ -1378,6 +1381,9 @@ packages:
|
||||
'@types/web-bluetooth@0.0.20':
|
||||
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
|
||||
|
||||
'@types/web-bluetooth@0.0.21':
|
||||
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
|
||||
|
||||
'@types/yauzl@2.10.3':
|
||||
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
|
||||
|
||||
@ -1617,15 +1623,27 @@ packages:
|
||||
'@vue/shared@3.5.13':
|
||||
resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
|
||||
|
||||
'@vueuse/components@12.8.2':
|
||||
resolution: {integrity: sha512-Nj27u1KsDWzoTthlChzVndJ9g0sW5APCXO3EJkSxlG11nN/ANTUlPPeoJOFvtbdDRnvsMJalboJyE0rRyg7yNg==}
|
||||
|
||||
'@vueuse/core@12.6.1':
|
||||
resolution: {integrity: sha512-FpgM1tXGAHsAC5n4Tflyg0vSoJUmdevfKaAhKFdxiK9BTIdHOHOiWmo+xivwdzjYFIvI8cEeJWYuqs646jOM2w==}
|
||||
|
||||
'@vueuse/core@12.8.2':
|
||||
resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==}
|
||||
|
||||
'@vueuse/metadata@12.6.1':
|
||||
resolution: {integrity: sha512-2094HNXGdsU3aqRbad0vmlRgGncMC4u2f6nFdW1mUn7b7ym4hORrDZfyeq8G5BfGvX4y0zZynWfCdtB2WwpyVw==}
|
||||
|
||||
'@vueuse/metadata@12.8.2':
|
||||
resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==}
|
||||
|
||||
'@vueuse/shared@12.6.1':
|
||||
resolution: {integrity: sha512-ukTb2na19KT1/YVjj4CYBDOgiV/xmsSJRL6TcKeiz2db+P5bT3I0OJxy38eRR3WSN8CmSnt7MdVJ16vX6VZFxg==}
|
||||
|
||||
'@vueuse/shared@12.8.2':
|
||||
resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==}
|
||||
|
||||
'@webassemblyjs/ast@1.14.1':
|
||||
resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
|
||||
|
||||
@ -6016,6 +6034,8 @@ snapshots:
|
||||
|
||||
'@types/web-bluetooth@0.0.20': {}
|
||||
|
||||
'@types/web-bluetooth@0.0.21': {}
|
||||
|
||||
'@types/yauzl@2.10.3':
|
||||
dependencies:
|
||||
'@types/node': 22.13.4
|
||||
@ -6449,6 +6469,14 @@ snapshots:
|
||||
|
||||
'@vue/shared@3.5.13': {}
|
||||
|
||||
'@vueuse/components@12.8.2(typescript@5.7.3)':
|
||||
dependencies:
|
||||
'@vueuse/core': 12.8.2(typescript@5.7.3)
|
||||
'@vueuse/shared': 12.8.2(typescript@5.7.3)
|
||||
vue: 3.5.13(typescript@5.7.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@vueuse/core@12.6.1(typescript@5.7.3)':
|
||||
dependencies:
|
||||
'@types/web-bluetooth': 0.0.20
|
||||
@ -6458,14 +6486,31 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@vueuse/core@12.8.2(typescript@5.7.3)':
|
||||
dependencies:
|
||||
'@types/web-bluetooth': 0.0.21
|
||||
'@vueuse/metadata': 12.8.2
|
||||
'@vueuse/shared': 12.8.2(typescript@5.7.3)
|
||||
vue: 3.5.13(typescript@5.7.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@vueuse/metadata@12.6.1': {}
|
||||
|
||||
'@vueuse/metadata@12.8.2': {}
|
||||
|
||||
'@vueuse/shared@12.6.1(typescript@5.7.3)':
|
||||
dependencies:
|
||||
vue: 3.5.13(typescript@5.7.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@vueuse/shared@12.8.2(typescript@5.7.3)':
|
||||
dependencies:
|
||||
vue: 3.5.13(typescript@5.7.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@webassemblyjs/ast@1.14.1':
|
||||
dependencies:
|
||||
'@webassemblyjs/helper-numbers': 1.13.2
|
||||
|
Loading…
Reference in New Issue
Block a user