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

1163 lines
29 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="light" />
<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="handleTouchStart($event, index)" @touchmove.prevent="handleTouchMove($event, index)"
@touchend="handleTouchEnd($event, index)" @click.stop="changeVideoOrImage(item, index)">
<image v-if="item.preview" class="video-preview" :src="item.preview" mode="aspectFill"></image>
<DomVideoPlayer v-else-if="item.videoUrl" :ref="`videoPlayer${index}`" class="video-preview"
:src="item.videoUrl" objectFit="cover" autoplay loop muted :controls="false"
:isLoading="true" />
<view v-else class="video-preview" style="background-color: #F3F3F3;"></view>
<view class="video-overlay" @click.stop="changeIconType(item, index)">
<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>
<!-- 底部选项弹窗 -->
<view v-if="data.showMediaSelector" class="action-sheet-overlay" @click="closeMediaSelector">
<view class="action-sheet" @click.stop>
<view class="action-sheet-item" @click="chooseVideo">
<text class="action-sheet-text">选择视频</text>
</view>
<view class="action-sheet-item" @click="chooseImage">
<text class="action-sheet-text">选择图片</text>
</view>
<view class="action-sheet-divider"></view>
<view class="action-sheet-item" @click="closeMediaSelector">
<text class="action-sheet-text action-sheet-cancel">取消</text>
</view>
</view>
</view>
</template>
<script setup>
import NavBar from "@/components/nav-bar/nav-bar.vue"
import { ref, toRefs, onMounted, onUnmounted, reactive, computed, getCurrentInstance } from 'vue'
import { onLoad, onShow, onHide } from '@dcloudio/uni-app'
import { util } from '@/utils/common.js'
const {
appContext,
proxy
} = getCurrentInstance();
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/voice_chat_1.jpg', videoUrl: '', savedVideoUrl: '', iconType: 0 },
{ preview: '/static/image/other/video-call/defualt/voice_chat_2.jpg', videoUrl: '', savedVideoUrl: '', iconType: 0 },
{ preview: '/static/image/other/video-call/defualt/voice_chat_3.jpg', videoUrl: '', savedVideoUrl: '', iconType: 0 },
{ preview: '/static/image/other/video-call/defualt/voice_chat_4.jpg', videoUrl: '', savedVideoUrl: '', iconType: 0 },
{ preview: '/static/image/other/video-call/defualt/voice_chat_5.jpg', videoUrl: '', savedVideoUrl: '', iconType: 0 },
{ preview: '/static/image/other/video-call/defualt/voice_chat_me.jpg', videoUrl: '', savedVideoUrl: '', iconType: 1 }
]
},
videoDataBackup: null, // 编辑模式备份
isEdit: false,
statusBarHeight: 0,
// 时间编辑弹窗
showTimeEditPopup: false,
tempMinutes: '0',
tempSeconds: '00',
// 底部选项弹窗
showMediaSelector: false,
currentEditIndex: -1, // 当前编辑的项索引 (-1表示新增, >=0表示替换)
// 拖动状态管理
dragState: {
draggingIndex: -1, // 当前拖动的索引
startY: 0, // 触摸起始Y坐标
startX: 0, // 触摸起始X坐标
offsetX: 0, // 当前X偏移量
offsetY: 0, // 当前Y偏移量
longPressTimer: null, // 长按定时器
isDragging: false // 是否正在拖动
}
})
// 定时器引用,用于页面卸载时清理
let statusBarTimer = null
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(() => {
const videoData = uni.getStorageSync('videoData') || data.videoData
console.log('videoData1', videoData)
const videoDataNew = {
...videoData,
videoList: videoData.videoList.map((item) => {
item.videoUrl = plus.io.convertLocalFileSystemURL(item.savedVideoUrl)
return item
})
}
data.videoData = videoDataNew
console.log('videoData2', data.videoData)
// 进入视频群聊页面埋点
proxy.$apiUserEvent('all', {
type: 'event',
key: 'voice_chat',
prefix: '.uni.other.',
value: "视频群聊"
})
})
onShow(() => {
// #ifdef APP-PLUS
util.setAndroidSystemBarColor('#232323')
// 保存定时器引用,以便在页面卸载时清理
statusBarTimer = setTimeout(() => {
plus.navigator.setStatusBarStyle("light");
}, 500)
// #endif
})
// 页面隐藏时清理定时器,防止返回时出错
onHide(() => {
// 清理状态栏定时器
if (statusBarTimer) {
clearTimeout(statusBarTimer)
statusBarTimer = null
}
// 清理长按定时器
if (data.dragState.longPressTimer) {
clearTimeout(data.dragState.longPressTimer)
data.dragState.longPressTimer = null
}
// 重置拖动状态
data.dragState.isDragging = false
data.dragState.draggingIndex = -1
console.log('🚪 页面隐藏,已清理定时器和状态')
})
// 页面卸载时清理所有定时器和资源
onUnmounted(() => {
// 清理状态栏定时器
if (statusBarTimer) {
clearTimeout(statusBarTimer)
statusBarTimer = null
}
// 清理长按定时器
if (data.dragState.longPressTimer) {
clearTimeout(data.dragState.longPressTimer)
data.dragState.longPressTimer = null
}
console.log('🧹 页面卸载,已清理所有定时器')
})
// 打开时间编辑弹窗
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 isTempFilePath = (filePath) => {
if (!filePath) return false
const lowerPath = filePath.toLowerCase()
// 永久路径白名单 - 如果路径包含这些特征,则明确判定为永久路径
const permanentKeywords = [
'/downloads/', // UniApp 永久下载目录
'saved_video_', // 已保存的视频文件
'saved_image_' // 已保存的图片文件
]
// 优先检查是否为永久路径
if (permanentKeywords.some(keyword => lowerPath.includes(keyword.toLowerCase()))) {
return false // 明确不是临时路径
}
// 常见临时路径特征
const tempKeywords = [
'tmp', // 通用临时目录
'temp', // 通用临时目录
'cache', // 缓存目录
'_doc/uniapp_temp', // UniApp Android 临时目录
'_downloads/temp', // 下载临时目录
'/var/mobile/containers/data/application', // iOS 临时路径
'wxfile://tmp', // 微信小程序临时文件
'http://tmp', // 微信小程序临时文件
'/doc/', // Android UniApp 文档临时目录
'compress_video' // UniApp 压缩视频临时文件
]
// 检查是否包含临时路径关键字
return tempKeywords.some(keyword => lowerPath.includes(keyword.toLowerCase()))
}
const confirmEdit = () => {
// 保存图片视频
// saveImageVideo()
// 退出编辑模式
data.isEdit = false
data.videoDataBackup = null
// 保存数据
// uni.setStorageSync('videoData', data.videoData)
}
/**
* 保存图片视频到本地
*/
const saveImage = async () => {
// 保存临时图片为永久路径
try {
// #ifdef APP-PLUS
const savePromises = data.videoData.videoList.map(async (item) => {
// 检查图片是否为临时路径
if (isTempFilePath(item.preview)) {
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
console.log('✅ 视频数据已保存')
} catch (error) {
console.error('❌ 保存过程出错:', error)
}
}
const changeIconType = (item, index) => {
if (index == data.videoData.videoList.length - 1) {
item.iconType = item.iconType == 2 ? 0 : item.iconType + 1
} else {
item.iconType = item.iconType == 2 ? 0 : 2
}
uni.setStorageSync('videoData', data.videoData)
}
const addVideo = () => {
if (data.videoData.videoList.length >= 9) {
return
}
// 重置为新增模式
data.currentEditIndex = -1
// 显示底部选项弹窗
data.showMediaSelector = true
}
// 关闭底部选项弹窗
const closeMediaSelector = () => {
data.showMediaSelector = false
}
/**
* 切换视频或图片
* @param item
* @param index
*/
const changeVideoOrImage = (item, index) => {
console.log('切换媒体:', item, index)
// 记录当前编辑的索引
data.currentEditIndex = index
// 显示媒体选择器
data.showMediaSelector = true
}
// 选择视频
const chooseVideo = () => {
closeMediaSelector()
uni.chooseVideo({
count: 1,
sourceType: ['album', 'camera'],
maxDuration: 60,
camera: 'back',
success: async (res) => {
console.log('选择视频成功:', res)
const videoUrl = plus.io.convertLocalFileSystemURL(res.tempFilePath)
const savedVideoUrl = await new Promise((resolve, reject) => {
uni.saveFile({
tempFilePath: videoUrl,
success: (res) => resolve(res.savedFilePath),
fail: (err) => reject(err)
})
})
// 判断是替换还是新增
if (data.currentEditIndex >= 0) {
// 获取旧数据并删除旧文件
const oldItem = data.videoData.videoList[data.currentEditIndex]
if (oldItem) {
console.log('🔄 替换视频,删除旧文件...')
removeFile(oldItem.preview)
removeFile(oldItem.videoUrl)
removeFile(oldItem.savedVideoUrl)
}
// 替换模式:更新指定索引的项
data.videoData.videoList[data.currentEditIndex] = {
preview: '',
videoUrl: videoUrl,
savedVideoUrl: savedVideoUrl,
iconType: 0
}
console.log('✅ 已替换索引', data.currentEditIndex, '的视频')
} else {
// 新增模式:添加新项
data.videoData.videoList.unshift({
preview: '',
videoUrl: videoUrl,
savedVideoUrl: savedVideoUrl,
iconType: 0
})
console.log('✅ 已添加新视频')
}
// 保存到本地存储
uni.setStorageSync('videoData', data.videoData)
console.log('💾 数据已保存到本地存储')
// 重置编辑索引
data.currentEditIndex = -1
},
fail: (err) => {
console.error('选择视频失败:', err)
// 重置编辑索引
data.currentEditIndex = -1
}
})
}
// 选择图片
const chooseImage = () => {
closeMediaSelector()
// 根据模式设置选择数量
const maxCount = data.currentEditIndex >= 0 ? 1 : (9 - data.videoData.videoList.length)
uni.chooseImage({
count: maxCount,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
console.log('选择图片成功:', res)
// 判断是替换还是新增
if (data.currentEditIndex >= 0) {
// 获取旧数据并删除旧文件
const oldItem = data.videoData.videoList[data.currentEditIndex]
if (oldItem) {
console.log('🔄 替换图片,删除旧文件...')
removeFile(oldItem.preview)
removeFile(oldItem.videoUrl)
}
// 替换模式:更新指定索引的项
data.videoData.videoList[data.currentEditIndex] = {
preview: res.tempFilePaths[0],
videoUrl: '',
savedVideoUrl: '',
iconType: 0
}
console.log('✅ 已替换索引', data.currentEditIndex, '的图片')
} else {
// 新增模式:添加新项
data.videoData.videoList.unshift(...res.tempFilePaths.map((item) => ({
preview: item,
videoUrl: '',
savedVideoUrl: '',
iconType: 0
})))
console.log('✅ 已添加', res.tempFilePaths.length, '张图片')
}
saveImage()
// 保存到本地存储
uni.setStorageSync('videoData', data.videoData)
console.log('💾 数据已保存到本地存储')
// 重置编辑索引
data.currentEditIndex = -1
},
fail: (err) => {
console.error('选择图片失败:', err)
// 重置编辑索引
data.currentEditIndex = -1
}
})
}
// 改变信息
const changeInfo = (key) => {
data.videoData[key] = !data.videoData[key]
uni.setStorageSync('videoData', data.videoData)
}
// 删除本地保存的文件
const removeFile = (filePath) => {
if (!filePath) return
// 如果是静态资源或临时文件,跳过
if (filePath.startsWith('/') && !filePath.startsWith('file://') && !filePath.includes('saved_')) {
// 简单的判断,保留 static 目录下的文件
if (filePath.includes('/static/')) return
}
console.log('🗑️ 准备删除文件:', filePath)
// #ifdef APP-PLUS
// 尝试使用 plus.io 删除 (支持 file:// 协议和 _downloads 等路径)
if (filePath.startsWith('file://') || filePath.includes('_downloads') || filePath.includes('saved_video_')) {
plus.io.resolveLocalFileSystemURL(filePath, (entry) => {
entry.remove(() => {
console.log('✅ plus.io 删除文件成功:', filePath)
}, (e) => {
console.log('⚠️ plus.io 删除文件失败:', e.message)
})
}, (e) => {
// 文件可能不存在
console.log('⚠️ plus.io 解析路径失败 (可能文件已不存在):', e.message)
})
return
}
// #endif
// 尝试使用 uni.removeSavedFile (主要针对 uni.saveFile 保存的文件)
uni.removeSavedFile({
filePath: filePath,
success: (res) => {
console.log('✅ uni.removeSavedFile 删除成功:', filePath)
},
fail: (err) => {
console.log('⚠️ uni.removeSavedFile 删除失败:', err)
}
})
}
const deleteVideo = (index) => {
const item = data.videoData.videoList[index]
if (item) {
removeFile(item.preview)
removeFile(item.videoUrl)
removeFile(item.savedVideoUrl)
}
data.videoData.videoList.splice(index, 1)
}
// 触摸开始 - 启动长按检测
const handleTouchStart = (event, index) => {
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.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.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
// 判断是否涉及最后一个元素的交换
const lastIndex = list.length - 1
if (data.dragState.draggingIndex === lastIndex || targetIndex === lastIndex) {
// 确保最后一个位置是 iconType 1其他位置是 iconType 0
// list[lastIndex].iconType = 1
// 另一个交换的位置设为 0
const otherIndex = data.dragState.draggingIndex === lastIndex ? targetIndex : data.dragState.draggingIndex
if (list[otherIndex].iconType == 1) {
list[otherIndex].iconType = 0
}
}
// 保存到本地存储
uni.setStorageSync('videoData', data.videoData)
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%;
z-index: 999;
}
// 拖动时的样式
&.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;
}
/* 底部选项弹窗样式 */
.action-sheet-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: flex-end;
z-index: 10001;
}
.action-sheet {
width: 100%;
background-color: #fff;
border-radius: 24rpx 24rpx 0 0;
overflow: hidden;
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
.action-sheet-item {
padding: 32rpx 0;
text-align: center;
background-color: #fff;
cursor: pointer;
transition: background-color 0.2s;
&:active {
background-color: #f5f5f5;
}
}
.action-sheet-text {
font-size: 32rpx;
color: #333;
}
.action-sheet-cancel {
color: #666;
}
.action-sheet-divider {
height: 16rpx;
background-color: #f5f5f5;
}
</style>