alipay-emulator/pages/other/video-group-chat/video-group-chat.vue

777 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- 水印 -->
<view v-if="$isVip()">
<watermark :dark="data.dark" />
<liu-drag-button :canDocking="false" @clickBtn="$goRechargePage('watermark')">
<c-lottie ref="cLottieRef" :src='$watermark()' width="94px" height='74px' :loop="true"></c-lottie>
</liu-drag-button>
</view>
<view class="container">
<!-- 导航栏 placeholder -->
<NavBar bgColor="transparent" tipLayerType="video-group-chat-tip" isTipLayer tipLayerText="修改聊天信息"
@button-click="util.clickTitlePopupButton" :buttonGroup="buttonGroup">
<view class="nav-content">
<view class="left">
<image class="icon" src="/static/image/other/video-call/float.png"></image>
</view>
<view class="center">
<text class="time">{{ videoData.timeText }}</text>
</view>
<view v-if="data.isEdit" class="right" @click.stop>
<view class="button" @click="confirmEdit">完成</view>
</view>
<view v-else class="right">
<image class="icon" src="/static/image/other/video-call/screen-mirroring.png"></image>
<image style="margin-left: 30px;" class="icon" src="/static/image/other/video-call/add.png"></image>
</view>
</view>
</NavBar>
<!-- 视频画面区域 -->
<view class="video-container">
<view class="video-grid" :class="videoGridClass">
<view class="video-item" v-for="(item, index) in videoData.videoList" :key="index"
:class="{ 'dragging': data.dragState.draggingIndex === index }" :style="getItemStyle(index)"
@touchstart="data.isEdit ? handleTouchStart($event, index) : null"
@touchmove.prevent="data.isEdit ? handleTouchMove($event, index) : null"
@touchend="data.isEdit ? handleTouchEnd($event, index) : null" @click.stop="changeIconType(item)">
<image class="video-preview" :src="item.preview" mode="aspectFill"></image>
<view class="video-overlay">
<image class="mute-icon" v-if="item.iconType > 0"
:src="`/static/image/other/video-call/${item.iconType == 1 ? 'mute' : 'unmute'}.png`">
</image>
</view>
<view v-if="data.isEdit" class="close-btn" @click.stop="deleteVideo(index)">
<image style="width: 40rpx;height: 40rpx;" src="/static/image/common/tipLayer-close.png">
</image>
</view>
</view>
<view v-if="videoData.videoList.length < 9 && data.isEdit" class="video-item"
style="background-color: #F3F3F3;" @click="addVideo">
<image style="width: 54rpx;height: 54rpx;" src="/static/image/other/video-call/add-image.png"
mode="aspectFill">
</image>
</view>
</view>
</view>
<!-- 底部控制栏 -->
<view class="control-bar">
<view class="control-buttons">
<!-- 麦克风 -->
<view class="control-item">
<view class="control-btn" :class="{ active: videoData.micOn }" @click="changeInfo('micOn')">
<image class="control-icon"
:src="videoData.micOn ? '/static/image/other/video-call/mic-on.png' : '/static/image/other/video-call/mic-off.png'">
</image>
</view>
<text class="control-label">{{ videoData.micOn ? '麦克风已开' : '麦克风已关' }}</text>
</view>
<!-- 扬声器 -->
<view class="control-item">
<view class="control-btn" :class="{ active: videoData.speakerOn }" @click="changeInfo('speakerOn')">
<image class="control-icon"
:src="videoData.speakerOn ? '/static/image/other/video-call/speaker-on.png' : '/static/image/other/video-call/speaker-off.png'">
</image>
</view>
<text class="control-label">{{ videoData.speakerOn ? '扬声器已开' : '扬声器已关' }}</text>
</view>
<!-- 摄像头 -->
<view class="control-item">
<view class="control-btn" :class="{ active: videoData.cameraOn }" @click="changeInfo('cameraOn')">
<image class="control-icon"
:src="videoData.cameraOn ? '/static/image/other/video-call/camera-on.png' : '/static/image/other/video-call/camera-off.png'">
</image>
</view>
<text class="control-label">{{ videoData.cameraOn ? '摄像头已开' : '摄像头已关' }}</text>
</view>
</view>
<!-- 挂断按钮 -->
<view class="hangup-btn" @click="hangup">
<image class="hangup-icon" src="/static/image/other/video-call/hangup.png"></image>
</view>
</view>
</view>
<!-- 时间编辑弹窗 -->
<view v-if="data.showTimeEditPopup" class="popup-overlay" @click="closeTimeEditPopup">
<view class="popup-content" @click.stop>
<view class="popup-header">
<text class="popup-title">通话时长</text>
</view>
<view class="popup-body">
<view class="time-edit-row">
<text class="time-label">分钟</text>
<input class="time-input" type="number" v-model="data.tempMinutes" placeholder="00" maxlength="3" />
<text class="time-separator">:</text>
<text class="time-label">秒钟</text>
<input class="time-input" type="number" v-model="data.tempSeconds" placeholder="00" maxlength="2" />
</view>
</view>
<view class="popup-footer">
<button class="btn-cancel" @click="closeTimeEditPopup">取消</button>
<button class="btn-save" @click="saveTimeEdit">保存</button>
</view>
</view>
</view>
</template>
<script setup>
import NavBar from "@/components/nav-bar/nav-bar.vue"
import { ref, toRefs, onMounted, onUnmounted, reactive, computed } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { util } from '@/utils/common.js'
const buttonGroup = [
{
name: "编辑时间",
click: () => {
openTimeEditPopup()
}
}, {
name: "编辑聊天人数",
click: () => {
// 进入编辑模式前备份数据
data.videoDataBackup = JSON.parse(JSON.stringify(data.videoData))
data.isEdit = true
}
}]
const data = reactive({
videoData: {
micOn: true,
speakerOn: true,
cameraOn: false,
mainVideoIndex: 0,
timeText: '125:22',
videoList: [
{ preview: '/static/image/other/video-call/defualt/video-img1.png', iconType: 0 },
{ preview: '/static/image/other/video-call/defualt/video-img2.png', iconType: 1 },
{ preview: '/static/image/other/video-call/defualt/video-img3.png', iconType: 2 }
]
},
videoDataBackup: null, // 编辑模式备份
isEdit: false,
statusBarHeight: 0,
// 时间编辑弹窗
showTimeEditPopup: false,
tempMinutes: '0',
tempSeconds: '00',
// 拖动状态管理
dragState: {
draggingIndex: -1, // 当前拖动的索引
startY: 0, // 触摸起始Y坐标
startX: 0, // 触摸起始X坐标
offsetX: 0, // 当前X偏移量
offsetY: 0, // 当前Y偏移量
longPressTimer: null, // 长按定时器
isDragging: false // 是否正在拖动
}
})
let { videoData, statusBarHeight } = toRefs(data)
// 视频网格样式计算属性
const videoGridClass = computed(() => {
const count = videoData.value.videoList.length
if (data.isEdit) {
if (count <= 1) return 'video-grid-2'
if (count <= 3) return 'video-grid-3'
} else {
if (count <= 2) return 'video-grid-2'
if (count <= 4) return 'video-grid-3'
}
return 'video-grid-5'
})
// 获取当前网格的列数
const getGridCols = () => {
const count = videoData.value.videoList.length
if (data.isEdit) {
if (count <= 1) return 2
if (count <= 3) return 2
} else {
if (count <= 2) return 2
if (count <= 4) return 2
}
return 3
}
// 获取网格尺寸
const getGridSize = () => {
const cols = getGridCols()
return uni.getSystemInfoSync().windowWidth / cols
}
// 获取每个video-item的样式用于拖动跟随
const getItemStyle = (index) => {
if (data.dragState.draggingIndex === index && data.dragState.isDragging) {
return {
transform: `translate(${data.dragState.offsetX}px, ${data.dragState.offsetY}px)`,
transition: 'none'
}
}
return {}
}
onMounted(() => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0
})
onLoad(() => {
data.videoData = uni.getStorageSync('videoData') || data.videoData
})
onShow(() => {
// #ifdef APP-PLUS
util.setAndroidSystemBarColor('#232323')
setTimeout(() => {
plus.navigator.setStatusBarStyle("light");
}, 500)
// #endif
})
// 打开时间编辑弹窗
const openTimeEditPopup = () => {
// 解析当前时间 (格式: "125:22")
const parts = data.videoData.timeText.split(':')
data.tempMinutes = parts[0] || '0'
data.tempSeconds = parts[1] || '00'
data.showTimeEditPopup = true
}
// 保存时间编辑
const saveTimeEdit = () => {
// 格式化分钟和秒钟
const minutes = parseInt(data.tempMinutes) || 0
const seconds = parseInt(data.tempSeconds) || 0
// 秒钟不能超过59
const validSeconds = Math.min(59, Math.max(0, seconds))
// 格式化为两位数
const formattedSeconds = validSeconds.toString().padStart(2, '0')
// 更新时间文本
data.videoData.timeText = `${minutes}:${formattedSeconds}`
// 保存到storage
uni.setStorageSync('videoData', data.videoData)
// 关闭弹窗
data.showTimeEditPopup = false
}
// 关闭时间编辑弹窗
const closeTimeEditPopup = () => {
data.showTimeEditPopup = false
}
const confirmEdit = async () => {
// 保存临时图片为永久路径
try {
// #ifdef APP-PLUS
const savePromises = data.videoData.videoList.map(async (item) => {
// 检查是否为临时路径(通常包含 tmp 或 temp
if (item.preview && (item.preview.includes('tmp') || item.preview.includes('temp'))) {
try {
const savedFile = await new Promise((resolve, reject) => {
uni.saveFile({
tempFilePath: item.preview,
success: (res) => resolve(res.savedFilePath),
fail: (err) => reject(err)
})
})
// 更新为永久路径
item.preview = savedFile
console.log('✅ 图片已保存:', savedFile)
} catch (error) {
console.error('❌ 保存图片失败:', item.preview, error)
// 保存失败时保持原路径
}
}
})
// 等待所有图片保存完成
await Promise.all(savePromises)
// #endif
// 保存数据
uni.setStorageSync('videoData', data.videoData)
console.log('✅ 视频数据已保存')
} catch (error) {
console.error('❌ 保存过程出错:', error)
// 即使出错也保存数据
uni.setStorageSync('videoData', data.videoData)
}
// 退出编辑模式
data.isEdit = false
data.videoDataBackup = null
}
const changeIconType = (item) => {
item.iconType = item.iconType == 2 ? 0 : item.iconType + 1
// 编辑模式下不立即保存,点击完成后统一保存
if (!data.isEdit) {
uni.setStorageSync('videoData', data.videoData)
}
}
const addVideo = () => {
if (data.videoData.videoList.length >= 9) {
return
}
uni.chooseImage({
count: 9 - data.videoData.videoList.length,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
data.videoData.videoList.push(...res.tempFilePaths.map((item) => ({ preview: item, iconType: 1 })))
}
})
}
// 改变信息
const changeInfo = (key) => {
data.videoData[key] = !data.videoData[key]
uni.setStorageSync('videoData', data.videoData)
}
const deleteVideo = (index) => {
data.videoData.videoList.splice(index, 1)
}
// 触摸开始 - 启动长按检测
const handleTouchStart = (event, index) => {
if (!data.isEdit) return
const touch = event.touches[0]
data.dragState.startX = touch.clientX
data.dragState.startY = touch.clientY
// 长按300ms后启动拖动模式
data.dragState.longPressTimer = setTimeout(() => {
data.dragState.isDragging = true
data.dragState.draggingIndex = index
// 震动反馈(如果支持)
// #ifdef APP-PLUS || MP-WEIXIN
uni.vibrateShort({ type: 'heavy' })
// #endif
}, 300)
}
// 触摸移动 - 实时更新位置偏移
const handleTouchMove = (event, index) => {
if (!data.isEdit || !data.dragState.isDragging) return
const touch = event.touches[0]
const deltaX = touch.clientX - data.dragState.startX
const deltaY = touch.clientY - data.dragState.startY
// 更新偏移量,让元素跟随手指移动
data.dragState.offsetX = deltaX
data.dragState.offsetY = deltaY
}
// 触摸结束 - 完成拖动并保存
const handleTouchEnd = (event, index) => {
if (!data.isEdit) return
// 清除长按定时器
if (data.dragState.longPressTimer) {
clearTimeout(data.dragState.longPressTimer)
data.dragState.longPressTimer = null
}
// 如果发生了拖动,计算最终位置并交换
if (data.dragState.isDragging) {
const deltaX = data.dragState.offsetX
const deltaY = data.dragState.offsetY
// 计算目标索引
const cols = getGridCols()
const gridSize = getGridSize()
console.log('🔍 拖动结束 - cols:', cols, 'gridSize:', gridSize, 'deltaX:', deltaX, 'deltaY:', deltaY)
const moveX = Math.round(deltaX / gridSize)
const moveY = Math.round(deltaY / gridSize)
console.log('🔍 移动格数 - moveX:', moveX, 'moveY:', moveY)
const currentRow = Math.floor(data.dragState.draggingIndex / cols)
const currentCol = data.dragState.draggingIndex % cols
const targetRow = currentRow + moveY
const targetCol = currentCol + moveX
console.log('🔍 位置计算 - 当前:', `(${currentRow},${currentCol})`, '目标:', `(${targetRow},${targetCol})`)
if (targetCol >= 0 && targetCol < cols && targetRow >= 0) {
const targetIndex = targetRow * cols + targetCol
console.log('🔍 索引计算 - draggingIndex:', data.dragState.draggingIndex, 'targetIndex:', targetIndex, 'listLength:', videoData.value.videoList.length)
if (targetIndex >= 0 && targetIndex < videoData.value.videoList.length && targetIndex !== data.dragState.draggingIndex) {
// 交换数组元素
const list = videoData.value.videoList
const temp = list[data.dragState.draggingIndex]
list[data.dragState.draggingIndex] = list[targetIndex]
list[targetIndex] = temp
console.log('✅ 交换成功!')
} else {
console.log('❌ 目标索引无效,不交换 - 原因:', targetIndex < 0 ? '索引<0' : targetIndex >= videoData.value.videoList.length ? '索引超出' : '索引相同')
}
} else {
console.log('❌ 目标位置超出范围,不交换 - targetCol:', targetCol, 'targetRow:', targetRow)
}
}
// 重置拖动状态
data.dragState.isDragging = false
data.dragState.draggingIndex = -1
data.dragState.offsetX = 0
data.dragState.offsetY = 0
}
const hangup = () => {
uni.navigateBack()
}
</script>
<style lang="less" scoped>
.container {
width: 100%;
height: 100vh;
background-color: #232323;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.nav-content {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
padding: 0 16px;
.icon {
width: 24px;
height: 24px;
flex-shrink: 0;
}
.left {
height: 100%;
width: 80px;
display: flex;
align-items: center;
}
.center {
height: 100%;
flex: 1;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
.time {
font-size: 16px;
color: #ffffff;
}
}
.right {
width: 80px;
height: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
.button {
font-size: 28rpx;
color: #ffffff;
background-color: #07C160;
padding: 10rpx 20rpx;
text-align: center;
border-radius: 12rpx;
white-space: nowrap;
}
}
}
/* 视频区域 */
.video-container {
flex: 1;
display: flex;
justify-content: center;
.video-grid-2 {
margin-top: 200rpx;
display: flex;
justify-content: center;
width: 100%;
.video-item {
width: 50vw;
height: 50vw;
}
}
.video-grid-3 {
display: flex;
justify-content: center;
align-content: flex-start;
width: 100%;
flex-wrap: wrap;
.video-item {
width: 50vw;
height: 50vw;
display: flex;
}
}
.video-grid-5 {
display: flex;
justify-content: flex-start;
align-content: flex-start;
width: 100%;
flex-wrap: wrap;
.video-item {
width: calc(100vw / 3);
height: calc(100vw / 3);
}
}
.video-item {
position: relative;
display: flex;
align-items: center;
justify-content: center;
.video-preview {
width: 100%;
height: 100%;
}
.video-overlay {
position: absolute;
bottom: 8px;
left: 8px;
width: 26px;
height: 26px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn {
position: absolute;
top: 8px;
right: 8px;
width: 40rpx;
height: 40rpx;
border-radius: 50%;
}
// 拖动时的样式
&.dragging {
opacity: 0.7;
transform: scale(1.05);
z-index: 999;
transition: transform 0.2s ease, opacity 0.2s ease;
}
}
}
.mute-icon {
width: 100%;
height: 100%;
}
/* 控制栏 */
.control-bar {
flex-shrink: 0;
padding: 40rpx 32rpx 80rpx;
background-color: transparent;
.control-buttons {
display: flex;
justify-content: space-around;
margin-bottom: 30px;
}
.control-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.control-btn {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background-color: #4a4a4a;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
&.active {
background-color: #ffffff;
}
}
.control-icon {
width: 100%·;
height: 100%;
}
.control-label {
font-size: 12px;
color: #999999;
}
.hangup-btn {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background-color: #ff3b30;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
}
.hangup-icon {
width: 100%;
height: 100%;
}
}
/* 时间编辑弹窗样式 */
.popup-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
}
.popup-content {
background-color: #fff;
border-radius: 20rpx;
width: 600rpx;
overflow: hidden;
}
.popup-header {
display: flex;
justify-content: center;
align-items: center;
padding: 30rpx 16rpx;
}
.popup-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.popup-close {
font-size: 60rpx;
color: #999;
line-height: 1;
padding: 0 10rpx;
}
.popup-body {
padding: 20rpx 0;
padding-bottom: 40rpx;
}
.time-edit-row {
display: flex;
align-items: center;
justify-content: center;
gap: 20rpx;
}
.time-label {
font-size: 28rpx;
color: #666;
}
.time-input {
width: 120rpx;
height: 60rpx;
border-radius: 10rpx;
text-align: center;
font-size: 30rpx;
font-weight: 500;
color: #333;
}
.time-separator {
font-size: 36rpx;
font-weight: 500;
color: #333;
margin: 0 8px;
}
.popup-footer {
display: flex;
border-top: 1rpx solid #eee;
::v-deep uni-button:after {
border: none !important;
}
}
.btn-cancel,
.btn-save {
flex: 1;
height: 100rpx;
line-height: 100rpx;
text-align: center;
font-size: 32rpx;
border: none;
background: none;
}
.btn-cancel {
color: #666;
border-right: 1px solid #eee;
border-radius: 0;
}
.btn-save {
color: #07C160;
font-weight: bold;
}
</style>