feat(live): 实现直播间最小化窗口功能
- 新增最小化窗口组件 liveMinWindow - 在 liveRoom 页面中集成最小化窗口功能 - 优化 liveStore,增加相关状态和方法支持最小化窗口 - 调整
This commit is contained in:
parent
9334819414
commit
3c69236caa
189
app/components/liveMinWindow/index.vue
Normal file
189
app/components/liveMinWindow/index.vue
Normal file
@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<div ref="dragRef"
|
||||
:style="style"
|
||||
class="fixed rounded-5px overflow-hidden shadow-lg cursor-move z-50"
|
||||
@mousedown.stop="handleDragStart"
|
||||
@touchstart.stop="handleDragStart">
|
||||
<div class="relative">
|
||||
<img :src="props.snapshot"
|
||||
class="w-80px object-cover"
|
||||
alt="直播画面">
|
||||
|
||||
<div class="absolute inset-0 bg-black/40 flex items-center justify-center"
|
||||
@click.stop="handleClick">
|
||||
<span class="text-white text-12px">点击回到直播</span>
|
||||
</div>
|
||||
<button @click.stop="handleClose"
|
||||
class="absolute top-10px right-10px text-white bg-black/40 rounded-full w-5 h-5 flex items-center justify-center hover:bg-black/60 cursor-pointer">
|
||||
<van-icon name="cross" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onBeforeUnmount, shallowRef, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useThrottleFn } from '@vueuse/core'
|
||||
|
||||
const props = defineProps({
|
||||
snapshot: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
},
|
||||
initialPosition: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
top: '80px',
|
||||
right: '16px'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const dragRef = shallowRef(null)
|
||||
|
||||
// 拖动相关状态
|
||||
const isDragging = ref(false)
|
||||
const startX = ref(0)
|
||||
const startY = ref(0)
|
||||
const left = ref(0)
|
||||
const top = ref(0)
|
||||
|
||||
// 初始化位置
|
||||
onMounted(() => {
|
||||
const rect = dragRef.value.getBoundingClientRect()
|
||||
// 将 right 值转换为 left 值
|
||||
left.value = window.innerWidth - rect.width - parseInt(props.initialPosition.right)
|
||||
top.value = parseInt(props.initialPosition.top)
|
||||
})
|
||||
|
||||
// 计算样式
|
||||
const style = computed(() => ({
|
||||
left: left.value ? `${left.value}px` : 'auto',
|
||||
top: top.value ? `${top.value}px` : 'auto',
|
||||
right: !left.value ? props.initialPosition.right : 'auto',
|
||||
transition: isDragging.value ? 'none' : 'all 0.3s ease',
|
||||
}))
|
||||
|
||||
// 开始拖动
|
||||
const handleDragStart = (event) => {
|
||||
event.stopPropagation()
|
||||
isDragging.value = true
|
||||
|
||||
const point = event.touches ? event.touches[0] : event
|
||||
|
||||
// 获取元素当前位置
|
||||
const rect = dragRef.value.getBoundingClientRect()
|
||||
left.value = rect.left
|
||||
top.value = rect.top
|
||||
|
||||
// 记录起始点击位置
|
||||
startX.value = point.clientX - left.value
|
||||
startY.value = point.clientY - top.value
|
||||
|
||||
if (event.type === 'mousedown') {
|
||||
document.addEventListener('mousemove', handleDragMove)
|
||||
document.addEventListener('mouseup', handleDragEnd)
|
||||
} else {
|
||||
document.addEventListener('touchmove', handleDragMove, { passive: false })
|
||||
document.addEventListener('touchend', handleDragEnd)
|
||||
}
|
||||
}
|
||||
|
||||
// 拖动中
|
||||
const handleDragMove = useThrottleFn((event) => {
|
||||
if (!isDragging.value) return
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const point = event.touches ? event.touches[0] : event
|
||||
const rect = dragRef.value.getBoundingClientRect()
|
||||
const maxX = window.innerWidth - rect.width
|
||||
const maxY = window.innerHeight - rect.height
|
||||
|
||||
left.value = Math.min(Math.max(0, point.clientX - startX.value), maxX)
|
||||
top.value = Math.min(Math.max(0, point.clientY - startY.value), maxY)
|
||||
}, 16)
|
||||
|
||||
// 添加防抖处理边缘吸附
|
||||
const handleEdgeSnap = useThrottleFn(() => {
|
||||
const rect = dragRef.value.getBoundingClientRect()
|
||||
const centerX = rect.left + rect.width / 2
|
||||
|
||||
left.value = centerX > window.innerWidth / 2
|
||||
? window.innerWidth - rect.width - 16
|
||||
: 16
|
||||
}, 100)
|
||||
|
||||
// 结束拖动
|
||||
const handleDragEnd = () => {
|
||||
if (!isDragging.value) return
|
||||
|
||||
isDragging.value = false
|
||||
handleEdgeSnap()
|
||||
|
||||
document.removeEventListener('mousemove', handleDragMove)
|
||||
document.removeEventListener('mouseup', handleDragEnd)
|
||||
document.removeEventListener('touchmove', handleDragMove)
|
||||
document.removeEventListener('touchend', handleDragEnd)
|
||||
}
|
||||
|
||||
// 处理点击事件
|
||||
const handleClick = (event) => {
|
||||
event.stopPropagation()
|
||||
if (!isDragging.value) {
|
||||
handleReturnLive()
|
||||
}
|
||||
}
|
||||
|
||||
const handleReturnLive = () => {
|
||||
router.push('/')
|
||||
props.onClose()
|
||||
}
|
||||
|
||||
const handleClose = (event) => {
|
||||
event?.stopPropagation()
|
||||
props.onClose()
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('mousemove', handleDragMove)
|
||||
document.removeEventListener('mouseup', handleDragEnd)
|
||||
document.removeEventListener('touchmove', handleDragMove)
|
||||
document.removeEventListener('touchend', handleDragEnd)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fixed {
|
||||
will-change: transform, opacity;
|
||||
touch-action: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
transform: translateZ(0); /* 开启GPU加速 */
|
||||
}
|
||||
|
||||
/* 添加动画类 */
|
||||
.min-window-enter-active,
|
||||
.min-window-leave-active {
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.min-window-enter-from,
|
||||
.min-window-leave-to {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
pointer-events: none;
|
||||
-webkit-user-drag: none;
|
||||
user-drag: none;
|
||||
}
|
||||
</style>
|
@ -36,11 +36,11 @@ const headItem=(statusCode)=>{
|
||||
</template>
|
||||
<template v-else-if="auctionData.auctionPriceList?.buys?.length>0">
|
||||
<div v-for="(item, index) in auctionData.auctionPriceList?.buys" :key="index" class="flex flex-shrink-0 h-25px">
|
||||
<div class="flex-grow-1 text-start shrink-0" :style="`color: ${headItem(item.statusCode).color}`" >{{ headItem(item.statusCode).label }}</div>
|
||||
<div class="flex-grow-1 text-center shrink-0">{{ item.auctionType==='local'?'现场竞价':'网络竞价' }}</div>
|
||||
<div class="flex-grow-1 text-center shrink-0">{{ item.createdAt }}</div>
|
||||
<div class="flex-grow-1 text-center shrink-0">{{item.baseCurrency}}{{ item.baseMoney }}</div>
|
||||
<div class="flex-grow-1 text-center text-#2B53AC shrink-0 w-20px">{{ item.userId===userInfo.ID?'我':'' }}</div>
|
||||
<div class="text-start shrink-0 w-60px" :style="`color: ${headItem(item.statusCode).color}`" >{{ headItem(item.statusCode).label }}</div>
|
||||
<div class="text-start shrink-0 w-80px">{{ item.auctionType==='local'?'现场竞价':'网络竞价' }}</div>
|
||||
<div class="text-start shrink-0 w-80px">{{ item.createdAt }}</div>
|
||||
<div class="text-start shrink-0 w-80px">{{item.baseCurrency}}{{ item.baseMoney }}</div>
|
||||
<div class="text-start text-#2B53AC shrink-0 w-20px">{{ item.userId===userInfo.ID?'我':'' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="auctionData.wsType==='newArtwork'">
|
||||
|
@ -1,4 +1,7 @@
|
||||
<script setup>
|
||||
import {liveStore} from "~/stores/live/index.js";
|
||||
import { showMinWindow, hideMinWindow } from '@/components/liveMinWindow/createMinWindow.js'
|
||||
const {lastSnapshot} = liveStore()
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
@ -16,18 +19,47 @@ const changePayStatus=()=>{
|
||||
payStatus.value=payStatus.value===0?1:0
|
||||
}
|
||||
const close=()=>{
|
||||
console.log('close')
|
||||
emit('update:show',false)
|
||||
}
|
||||
const confirm=()=>{
|
||||
router.push('/signature/protocol')
|
||||
handleCapture()
|
||||
emit('update:show',false)
|
||||
}
|
||||
const captureVideoFrame = () => {
|
||||
try {
|
||||
const video = document.querySelector('#J_prismPlayer video')
|
||||
if (!video) {
|
||||
console.error('未找到视频元素')
|
||||
return null
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = video.videoWidth
|
||||
canvas.height = video.videoHeight
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
|
||||
return canvas.toDataURL('image/jpeg', 0.9)
|
||||
} catch (error) {
|
||||
console.error('获取视频截图失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
const handleCapture = () => {
|
||||
const imageUrl = captureVideoFrame()
|
||||
if (imageUrl) {
|
||||
lastSnapshot.value=imageUrl
|
||||
showMinWindow(lastSnapshot.value)
|
||||
console.log('截图成功:', imageUrl)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<van-dialog :show="show" show-cancel-button @close="close" @confirm="confirm">
|
||||
<van-dialog :show="show" show-cancel-button @cancel="close" @confirm="confirm">
|
||||
<div class="flex flex-col pt-18px pb-13px justify-between items-center h-144px">
|
||||
<template v-if="payStatus===0">
|
||||
<div class="text-#000 text-16px font-600 ">支付全部</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import {ref, onMounted, onBeforeUnmount, computed, watch} from 'vue'
|
||||
import {ref, onMounted, onBeforeUnmount, watch} from 'vue'
|
||||
import Aliplayer from 'aliyun-aliplayer'
|
||||
import 'aliyun-aliplayer/build/skins/default/aliplayer-min.css'
|
||||
import sideButton from '@/pages/liveRoom/components/SideButton/index.vue'
|
||||
@ -7,80 +7,39 @@ import broadcast from '@/pages/liveRoom/components/Broadcast/index.vue'
|
||||
import {liveStore} from "@/stores/live/index.js"
|
||||
import paymentResults from '@/pages/liveRoom/components/PaymentResults/index.vue'
|
||||
import paymentInput from '@/pages/liveRoom/components/PaymentInput/index.vue'
|
||||
import xButton from '@/components/x-button/index.vue'
|
||||
import {goodStore} from "@/stores/goods/index.js"
|
||||
import {message} from "~/components/x-message/useMessage.js"
|
||||
import {artworkBuy} from "@/api/goods/index.js"
|
||||
import CryptoJS from 'crypto-js'
|
||||
|
||||
|
||||
const {auctionDetail, getAuctionDetail} = goodStore()
|
||||
const player = ref(null)
|
||||
const {quoteStatus, changeStatus, show, playerId, show1, auctionData, getSocketData,getLiveLink} = liveStore()
|
||||
const {quoteStatus, show, playerId, show1, auctionData, getSocketData, getLiveLink} = liveStore()
|
||||
const isPlayerReady = ref(false)
|
||||
const pullLink = ref('')
|
||||
|
||||
const props = defineProps({
|
||||
fullLive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
const isPiPActive = ref(false)
|
||||
const videoRef = ref(null)
|
||||
|
||||
// 检查浏览器是否支持画中画
|
||||
const isPiPSupported = computed(() => {
|
||||
return document.pictureInPictureEnabled ||
|
||||
document.webkitPictureInPictureEnabled
|
||||
})
|
||||
|
||||
// 进入画中画模式
|
||||
const enterPiP = async () => {
|
||||
try {
|
||||
if (!videoRef.value) return
|
||||
|
||||
if (document.pictureInPictureElement) {
|
||||
await document.exitPictureInPicture()
|
||||
}
|
||||
|
||||
await videoRef.value.requestPictureInPicture()
|
||||
isPiPActive.value = true
|
||||
} catch (error) {
|
||||
console.error('进入画中画模式失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 退出画中画模式
|
||||
const exitPiP = async () => {
|
||||
try {
|
||||
if (document.pictureInPictureElement) {
|
||||
await document.exitPictureInPicture()
|
||||
isPiPActive.value = false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('退出画中画模式失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
definePageMeta({
|
||||
title: '主页',
|
||||
i18n: 'login.title',
|
||||
})
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
|
||||
const handlePlayerError = (error) => {
|
||||
console.error('播放器错误:', error)
|
||||
if (player.value) {
|
||||
player.value?.play()
|
||||
}
|
||||
}
|
||||
|
||||
const initializePlayer = () => {
|
||||
const initializePlayer = async () => {
|
||||
try {
|
||||
if (player.value) {
|
||||
player.value?.dispose()
|
||||
player.value.dispose()
|
||||
}
|
||||
|
||||
const playerConfig = {
|
||||
id: playerId.value,
|
||||
source: pullLink.value,
|
||||
@ -89,15 +48,13 @@ const initializePlayer = () => {
|
||||
autoplayPolicy: {fallbackToMute: true},
|
||||
controlBarVisibility: 'never',
|
||||
}
|
||||
|
||||
player.value = new Aliplayer(playerConfig, (playerInstance) => {
|
||||
isPlayerReady.value = true
|
||||
playerInstance?.play()
|
||||
})
|
||||
player.value?.on('error', handlePlayerError)
|
||||
player.value?.on('rtsTraceId', (event) => {
|
||||
})
|
||||
player.value?.on('rtsFallback', (event) => {
|
||||
})
|
||||
|
||||
player.value.on('error', handlePlayerError)
|
||||
} catch (error) {
|
||||
console.error('播放器初始化失败:', error)
|
||||
}
|
||||
@ -109,10 +66,8 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (player.value) {
|
||||
player.value?.dispose()
|
||||
player.value = null
|
||||
}
|
||||
})
|
||||
|
||||
const goPay = () => {
|
||||
@ -120,9 +75,7 @@ const goPay = () => {
|
||||
}
|
||||
|
||||
const fullLive1 = ref(false)
|
||||
watch(() => {
|
||||
return props.fullLive
|
||||
}, (newVal) => {
|
||||
watch(() => props.fullLive, (newVal) => {
|
||||
if (newVal) {
|
||||
getSocketData()
|
||||
setTimeout(() => {
|
||||
@ -145,6 +98,9 @@ const goBuy = async () => {
|
||||
|
||||
const tipOpen = () => {
|
||||
message.warning('出价状态未开启')
|
||||
}
|
||||
const updateShow=()=>{
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -171,8 +127,7 @@ const tipOpen = () => {
|
||||
<van-button @click="goBuy" color="#FFB25F" class="w-344px !h-[40px]">
|
||||
<div>{{
|
||||
`确认出价 ${auctionData?.nowAuctionPrice?.currency} ${auctionData?.nowAuctionPrice?.nextPrice ?? 0}`
|
||||
}}
|
||||
</div>
|
||||
}}</div>
|
||||
</van-button>
|
||||
</div>
|
||||
<div v-else class="mt-10px mb-10px">
|
||||
@ -183,7 +138,7 @@ const tipOpen = () => {
|
||||
|
||||
<broadcast></broadcast>
|
||||
</div>
|
||||
<paymentInput v-model:show="show"/>
|
||||
<paymentInput v-model:show="show" @update:show="updateShow"/>
|
||||
<div>
|
||||
</div>
|
||||
<paymentResults v-model:show="show1" type="error"/>
|
||||
@ -197,16 +152,19 @@ const tipOpen = () => {
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
#J_prismPlayer {
|
||||
width: 100%;
|
||||
height: 100% !important;
|
||||
|
||||
& > video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.v-enter-active,
|
||||
.v-leave-active {
|
||||
|
@ -1,6 +1,9 @@
|
||||
<script setup>
|
||||
const activeNames = ref(['1']);
|
||||
|
||||
definePageMeta({
|
||||
layout: 'default',
|
||||
title: '签署内容'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -19,6 +19,31 @@ export const liveStore = createGlobalState(() => {
|
||||
const socket=ref(null)
|
||||
const config = useRuntimeConfig()
|
||||
const pullLink=ref('')
|
||||
const isMinWindow = ref(false)
|
||||
const lastSnapshot = ref('')
|
||||
const liveInfo = ref(null)
|
||||
|
||||
// 设置最小化状态
|
||||
const setMinWindow = (status) => {
|
||||
isMinWindow.value = status
|
||||
}
|
||||
|
||||
// 设置截图
|
||||
const setSnapshot = (snapshot) => {
|
||||
lastSnapshot.value = snapshot
|
||||
}
|
||||
|
||||
// 设置直播信息
|
||||
const setLiveInfo = (info) => {
|
||||
liveInfo.value = info
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
const reset = () => {
|
||||
isMinWindow.value = false
|
||||
lastSnapshot.value = ''
|
||||
liveInfo.value = null
|
||||
}
|
||||
// 解密工具函数
|
||||
const decryptUtils = {
|
||||
// 解密配置
|
||||
@ -229,6 +254,13 @@ export const liveStore = createGlobalState(() => {
|
||||
|
||||
}
|
||||
return{
|
||||
isMinWindow,
|
||||
lastSnapshot,
|
||||
liveInfo,
|
||||
setMinWindow,
|
||||
setSnapshot,
|
||||
setLiveInfo,
|
||||
reset,
|
||||
pullLink,
|
||||
getLiveLink,
|
||||
auctionData,
|
||||
|
Loading…
Reference in New Issue
Block a user