feat(components): 重构浮动面板组件并添加新功能

- 重写了 floating2 组件,增加了更多
This commit is contained in:
xingyy 2025-03-06 11:09:12 +08:00
parent 4d358d130b
commit e0853e1416
5 changed files with 294 additions and 82 deletions

View File

@ -1,93 +1,238 @@
<template> <template>
<div <div ref="floatPanel" :style="panelStyle" class="float-panel"
ref="bubbleRef" @mousedown="handleMouseDown"
class="floating-bubble" @touchstart="handleTouchStart">
:style="bubbleStyle"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
>
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed, onMounted, onUnmounted } from 'vue';
import { useThrottleFn } from '@vueuse/core'
const props = defineProps({ const props = defineProps({
offset: { position: {
type: Number, type: Object,
default: 0 default: () => ({})
},
fixedX: Boolean,
fixedY: Boolean,
snapEdge: {
type: Boolean,
default: false
} }
}) });
const bubbleRef = ref(null) const floatPanel = ref(null);
const position = ref({ const panelPos = ref({ x: 0, y: 0 });
right: props.offset, const panelSize = ref({ width: 0, height: 0 });
bottom: 100 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 } const updateSizes = () => {
let isDragging = false if (floatPanel.value) {
panelSize.value = {
const bubbleStyle = computed(() => ({ width: floatPanel.value.offsetWidth,
position: 'fixed', height: floatPanel.value.offsetHeight
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)
} }
}, 16) windowSize.value = {
width: window.innerWidth,
height: window.innerHeight
};
};
const onTouchEnd = () => { //
if (!isDragging) return const initPosition = () => {
isDragging = false updateSizes();
const { left, right, top, bottom } = props.position;
const { clientWidth } = document.documentElement if (right !== undefined) {
const rect = bubbleRef.value.getBoundingClientRect() panelPos.value.x = windowSize.value.width - parseInt(right || 0) - panelSize.value.width;
const left = clientWidth - position.value.right - rect.width } else if (left !== undefined) {
panelPos.value.x = parseInt(left || 0);
}
// if (bottom !== undefined) {
position.value.right = left < clientWidth / 2 panelPos.value.y = windowSize.value.height - parseInt(bottom || 0) - panelSize.value.height;
? clientWidth - rect.width - props.offset } else if (top !== undefined) {
: props.offset 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> </script>
<style scoped> <style scoped>
.floating-bubble { .float-panel {
touch-action: none; position: fixed;
z-index: 1000;
cursor: move;
user-select: none; user-select: none;
touch-action: none;
transition: left 0.2s ease-out;
} }
</style> </style>

View File

@ -6,7 +6,7 @@ import Cescribe from './components/Cescribe/index.vue'
import FloatingBubble from '~/components/floating2/index.vue' import FloatingBubble from '~/components/floating2/index.vue'
import {liveStore} from "~/stores/live/index.js"; import {liveStore} from "~/stores/live/index.js";
const {auctionDetail,getArtworkList} = goodStore(); const {auctionDetail,getArtworkList,getAuctionDetail} = goodStore();
const {fullLive} = liveStore() const {fullLive} = liveStore()
const changeLive = () => { const changeLive = () => {
if (!fullLive.value){ if (!fullLive.value){
@ -16,7 +16,7 @@ const changeLive = () => {
} }
} }
} }
await getAuctionDetail()
</script> </script>
<template> <template>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import {ref, onMounted, onBeforeUnmount, watch} from 'vue' import {ref, onMounted, onBeforeUnmount, watch,useTemplateRef } from 'vue'
import AliyunPlayer from 'aliyun-aliplayer' import AliyunPlayer from 'aliyun-aliplayer'
import 'aliyun-aliplayer/build/skins/default/aliplayer-min.css' import 'aliyun-aliplayer/build/skins/default/aliplayer-min.css'
import sideButton from '@/pages/liveRoom/components/SideButton/index.vue' 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 {artworkBuy} from "@/api/goods/index.js"
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import floating2 from '@/components/floating2/index.vue' import floating2 from '@/components/floating2/index.vue'
import { UseDraggable } from '@vueuse/components'
const { t } = useI18n() const { t } = useI18n()
const { auctionDetail,getAuctionDetail} = goodStore(); const { auctionDetail,getAuctionDetail} = goodStore();
const player = ref(null) const player = ref(null)
@ -127,7 +130,7 @@ const goBuy = async () => {
message.success(t('live_room.success_mess')) message.success(t('live_room.success_mess'))
} }
} }
const sideButtonRef = useTemplateRef('sideButtonRef')
const tipOpen = () => { const tipOpen = () => {
message.warning(t('live_room.warn_mess')) message.warning(t('live_room.warn_mess'))
} }
@ -139,11 +142,14 @@ const tipOpen = () => {
<div v-if="loading1" class="absolute left-1/2 transform translate-x--1/2 top-1/2 translate-y--1/2"> <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> <van-loading type="spinner" >直播加载中...</van-loading>
</div> </div>
<client-only>
</client-only>
<transition name="fade"> <transition name="fade">
<div v-if="fullLive"> <div v-if="fullLive">
<floating2
<floating2 top="200px"> :snap-edge="true"
:position="{right:0,top:300}"
>
<sideButton></sideButton> <sideButton></sideButton>
</floating2> </floating2>
<div class="absolute left-1/2 transform -translate-x-1/2 flex flex-col items-center" <div class="absolute left-1/2 transform -translate-x-1/2 flex flex-col items-center"
@ -190,6 +196,21 @@ const tipOpen = () => {
<style scoped lang="scss"> <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 { .fade-enter-active {
transition: opacity 1s ease; transition: opacity 1s ease;

View File

@ -20,6 +20,7 @@
"@fingerprintjs/fingerprintjs": "^4.5.1", "@fingerprintjs/fingerprintjs": "^4.5.1",
"@nuxtjs/i18n": "^9.1.1", "@nuxtjs/i18n": "^9.1.1",
"@vue-office/pdf": "^2.0.10", "@vue-office/pdf": "^2.0.10",
"@vueuse/components": "^12.8.2",
"@vueuse/core": "^12.4.0", "@vueuse/core": "^12.4.0",
"aliyun-aliplayer": "^2.28.5", "aliyun-aliplayer": "^2.28.5",
"axios": "^1.7.9", "axios": "^1.7.9",

View File

@ -20,6 +20,9 @@ importers:
'@vue-office/pdf': '@vue-office/pdf':
specifier: ^2.0.10 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)) 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': '@vueuse/core':
specifier: ^12.4.0 specifier: ^12.4.0
version: 12.6.1(typescript@5.7.3) version: 12.6.1(typescript@5.7.3)
@ -1378,6 +1381,9 @@ packages:
'@types/web-bluetooth@0.0.20': '@types/web-bluetooth@0.0.20':
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
'@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
'@types/yauzl@2.10.3': '@types/yauzl@2.10.3':
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
@ -1617,15 +1623,27 @@ packages:
'@vue/shared@3.5.13': '@vue/shared@3.5.13':
resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
'@vueuse/components@12.8.2':
resolution: {integrity: sha512-Nj27u1KsDWzoTthlChzVndJ9g0sW5APCXO3EJkSxlG11nN/ANTUlPPeoJOFvtbdDRnvsMJalboJyE0rRyg7yNg==}
'@vueuse/core@12.6.1': '@vueuse/core@12.6.1':
resolution: {integrity: sha512-FpgM1tXGAHsAC5n4Tflyg0vSoJUmdevfKaAhKFdxiK9BTIdHOHOiWmo+xivwdzjYFIvI8cEeJWYuqs646jOM2w==} resolution: {integrity: sha512-FpgM1tXGAHsAC5n4Tflyg0vSoJUmdevfKaAhKFdxiK9BTIdHOHOiWmo+xivwdzjYFIvI8cEeJWYuqs646jOM2w==}
'@vueuse/core@12.8.2':
resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==}
'@vueuse/metadata@12.6.1': '@vueuse/metadata@12.6.1':
resolution: {integrity: sha512-2094HNXGdsU3aqRbad0vmlRgGncMC4u2f6nFdW1mUn7b7ym4hORrDZfyeq8G5BfGvX4y0zZynWfCdtB2WwpyVw==} resolution: {integrity: sha512-2094HNXGdsU3aqRbad0vmlRgGncMC4u2f6nFdW1mUn7b7ym4hORrDZfyeq8G5BfGvX4y0zZynWfCdtB2WwpyVw==}
'@vueuse/metadata@12.8.2':
resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==}
'@vueuse/shared@12.6.1': '@vueuse/shared@12.6.1':
resolution: {integrity: sha512-ukTb2na19KT1/YVjj4CYBDOgiV/xmsSJRL6TcKeiz2db+P5bT3I0OJxy38eRR3WSN8CmSnt7MdVJ16vX6VZFxg==} resolution: {integrity: sha512-ukTb2na19KT1/YVjj4CYBDOgiV/xmsSJRL6TcKeiz2db+P5bT3I0OJxy38eRR3WSN8CmSnt7MdVJ16vX6VZFxg==}
'@vueuse/shared@12.8.2':
resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==}
'@webassemblyjs/ast@1.14.1': '@webassemblyjs/ast@1.14.1':
resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
@ -6016,6 +6034,8 @@ snapshots:
'@types/web-bluetooth@0.0.20': {} '@types/web-bluetooth@0.0.20': {}
'@types/web-bluetooth@0.0.21': {}
'@types/yauzl@2.10.3': '@types/yauzl@2.10.3':
dependencies: dependencies:
'@types/node': 22.13.4 '@types/node': 22.13.4
@ -6449,6 +6469,14 @@ snapshots:
'@vue/shared@3.5.13': {} '@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)': '@vueuse/core@12.6.1(typescript@5.7.3)':
dependencies: dependencies:
'@types/web-bluetooth': 0.0.20 '@types/web-bluetooth': 0.0.20
@ -6458,14 +6486,31 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- typescript - 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.6.1': {}
'@vueuse/metadata@12.8.2': {}
'@vueuse/shared@12.6.1(typescript@5.7.3)': '@vueuse/shared@12.6.1(typescript@5.7.3)':
dependencies: dependencies:
vue: 3.5.13(typescript@5.7.3) vue: 3.5.13(typescript@5.7.3)
transitivePeerDependencies: transitivePeerDependencies:
- typescript - 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': '@webassemblyjs/ast@1.14.1':
dependencies: dependencies:
'@webassemblyjs/helper-numbers': 1.13.2 '@webassemblyjs/helper-numbers': 1.13.2