1692 lines
45 KiB
Vue
1692 lines
45 KiB
Vue
<template>
|
||
<view class="shadow-up"></view>
|
||
<view class="shadow-down"></view>
|
||
<!-- 水印 -->
|
||
<view v-if="$isVip()">
|
||
<watermark dark="light" source="uni_alipay_other_videoChat" />
|
||
<liu-drag-button :canDocking="false" @clickBtn="$goRechargePage('watermark', 'uni_alipay_other_videoChat')">
|
||
<c-lottie ref="cLottieRef" :src='$watermark()' width="94px" height='74px' :loop="true"></c-lottie>
|
||
</liu-drag-button>
|
||
</view>
|
||
<!-- 未接通内容 -->
|
||
<view v-if="!isAnswered" class="container">
|
||
<view style="height: 100vh;position: fixed;width: 100vw;object-fit: cover;overflow: hidden;">
|
||
<image v-if="!videoData.cameraOn" :src="videoData.chat.other.avatar" mode="aspectFill"
|
||
style="position: absolute; width: 100%; height: 100%; filter: blur(20px); transform: scale(1.2);">
|
||
</image>
|
||
<DomVideoPlayer v-if="videoData.cameraOn" ref="mainVideoRef"
|
||
style="position: absolute; width: 100vw;height: 100vh;object-fit: cover;display: block;"
|
||
:src="getVideoUrl(videoData.chat.me.videoUrl)" class="bg-image" mode="aspectFill" autoplay loop
|
||
objectFit="cover" :controls="false" :show-play-btn="false" :muted="true">
|
||
</DomVideoPlayer>
|
||
<view v-if="!videoData.cameraOn"
|
||
style="position: absolute; background: rgba(0,0,0,0.8); width: 100%; height: 100%;">
|
||
</view>
|
||
<view class="top-view">
|
||
|
||
<image style="height: 136rpx;width:136rpx;object-fit: cover;border-radius: 12rpx;"
|
||
:src="videoData.chat.other.avatar" mode="aspectFill">
|
||
</image>
|
||
<text class="nickname">{{ videoData.chat.other.name }}</text>
|
||
<view class="dot-view">
|
||
<text class="dot" :style="{ animationDelay: i === 1 ? '0s' : i === 2 ? '-1s' : '-0.5s' }"
|
||
v-for="i in 3" :key="i"></text>
|
||
</view>
|
||
</view>
|
||
|
||
</view>
|
||
|
||
|
||
|
||
<!-- 导航栏 placeholder -->
|
||
<NavBar bgColor="transparent" tipLayerType="video-group-chat-tip" isTipLayer tipLayerText="修改聊天信息"
|
||
@button-click="util.clickTitlePopupButton" :buttonGroup="buttonGroup">
|
||
|
||
<!-- 通过作用域插槽接管按钮渲染 -->
|
||
<template #button="{ button }">
|
||
<!-- 如果是开关按钮,拦截点击事件冒泡,防止触发顶层关闭弹窗,并避免两次触发 click -->
|
||
<view v-if="button.isSwitch" @click.stop
|
||
style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;">
|
||
<text>{{ button.name }}</text>
|
||
<switch :checked="button.value" @change="button.click"
|
||
style="transform: scale(0.7); margin-left: 10rpx;"></switch>
|
||
</view>
|
||
<!-- 普通按钮,不拦截冒泡,直接走外层 button-box 的 click -->
|
||
<view v-else
|
||
style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;">
|
||
<text>{{ button.name }}</text>
|
||
</view>
|
||
</template>
|
||
|
||
<view class="nav-content">
|
||
<view class="left">
|
||
<image class="icon" style="width: 152rpx;height: 68rpx;"
|
||
src="/static/image/other/video-call/hulue.png">
|
||
</image>
|
||
</view>
|
||
|
||
</view>
|
||
</NavBar>
|
||
|
||
<!-- 底部控制栏 -->
|
||
<view class="control-bar" style="padding-left: 48rpx;padding-right: 48rpx;">
|
||
<view class="control-buttons" style="display: flex;justify-content: space-between;">
|
||
<!-- 麦克风 -->
|
||
<view class="control-item">
|
||
<image class="control-btn" :class="{ active: videoData.micOn }" @click="changeInfo('micOn')"
|
||
:src="videoData.micOn ? '/static/image/other/video-call/mic-on.png' : '/static/image/other/video-call/unMic.png'">
|
||
</image>
|
||
<text class="control-label">{{ videoData.micOn ? '麦克风已开' : '麦克风已关' }}</text>
|
||
</view>
|
||
|
||
<!-- 摄像头 -->
|
||
<view class="control-item">
|
||
<image class="control-btn" :class="{ active: videoData.cameraOn }" @click="changeInfo('cameraOn')"
|
||
:src="videoData.cameraOn ? '/static/image/other/video-call/camera-on.png' : '/static/image/other/video-call/unCamera.png'">
|
||
</image>
|
||
<text class="control-label">{{ videoData.cameraOn ? '摄像头已开' : '摄像头已关' }}</text>
|
||
</view>
|
||
<!-- 背景模糊 -->
|
||
<view class="control-item">
|
||
<image class="control-btn" src="/static/image/other/video-call/xvnibeijing.png">
|
||
</image>
|
||
<text class="control-label">背景模糊</text>
|
||
</view>
|
||
<!-- 旋转摄像头 -->
|
||
<view class="control-item">
|
||
<image class="control-btn" src="/static/image/other/video-call/xuanzhuan.png">
|
||
</image>
|
||
<text class="control-label">翻转</text>
|
||
</view>
|
||
</view>
|
||
<view class="control-buttons" style="margin-bottom: 0;display: flex;justify-content: space-between;">
|
||
|
||
<!-- 挂断按钮 -->
|
||
<image class="hangup-btn" style="margin: 0;" @click="hangup"
|
||
src="/static/image/other/video-call/hangup.png">
|
||
</image>
|
||
|
||
<image class="hangup-btn" style="margin: 0;" src="/static/image/other/video-call/jieting.png"
|
||
@click="answerCall">
|
||
</image>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 接通电话后 -->
|
||
<view v-else class="container">
|
||
<view style="height: 100vh;position: fixed;width: 100vw;object-fit: cover;overflow: hidden;">
|
||
<image v-if="!(data.isSwapped ? videoData.chat.other.isShowVideo : videoData.cameraOn)"
|
||
:src="data.isSwapped ? videoData.chat.other.avatar : videoData.chat.me.avatar" mode="aspectFill"
|
||
style="position: absolute; width: 100%; height: 100%; filter: blur(20px); transform: scale(1.2);">
|
||
</image>
|
||
<DomVideoPlayer ref="mainVideoRef"
|
||
v-if="data.isSwapped ? videoData.chat.other.isShowVideo : videoData.cameraOn"
|
||
style="position: absolute; width: 100vw;height: 100vh;object-fit: cover;display: block;"
|
||
:src="data.isSwapped ? getVideoUrl(videoData.chat.other.videoUrl) : getVideoUrl(videoData.chat.me.videoUrl)"
|
||
class="bg-image" mode="aspectFill" autoplay loop :muted="data.isSwapped ? !videoData.speakerOn : true"
|
||
objectFit="cover" :controls="false" :show-play-btn="false">
|
||
</DomVideoPlayer>
|
||
|
||
<template v-if="!data.isSwapped ? !videoData.cameraOn : !videoData.chat.other.isShowVideo">
|
||
<view style="position: absolute; background: rgba(0,0,0,0.8); width: 100%; height: 100%;">
|
||
</view>
|
||
<image class="avatar-1" style="height: 136rpx;width:136rpx;object-fit: cover;border-radius: 12rpx;"
|
||
:src="!data.isSwapped ? videoData.chat.me.avatar : videoData.chat.other.avatar" mode="aspectFill">
|
||
</image>
|
||
</template>
|
||
</view>
|
||
|
||
|
||
|
||
<!-- 导航栏 placeholder -->
|
||
<NavBar bgColor="transparent" tipLayerType="video-group-chat-tip" isTipLayer tipLayerText="修改聊天信息"
|
||
@button-click="util.clickTitlePopupButton" :buttonGroup="buttonGroup">
|
||
|
||
<!-- 通过作用域插槽接管按钮渲染 -->
|
||
<template #button="{ button }">
|
||
<!-- 如果是开关按钮,拦截点击事件冒泡,防止触发顶层关闭弹窗,并避免两次触发 click -->
|
||
<view v-if="button.isSwitch" @click.stop
|
||
style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;">
|
||
<text>{{ button.name }}</text>
|
||
<switch :checked="button.value" @change="button.click"
|
||
style="transform: scale(0.7); margin-left: 10rpx;"></switch>
|
||
</view>
|
||
<!-- 普通按钮,不拦截冒泡,直接走外层 button-box 的 click -->
|
||
<view v-else
|
||
style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;">
|
||
<text>{{ button.name }}</text>
|
||
</view>
|
||
</template>
|
||
|
||
<view class="nav-content">
|
||
<view class="left">
|
||
<image class="icon" style="width: 30rpx;height: 30rpx;"
|
||
src="/static/image/other/video-call/xuanfu.png">
|
||
</image>
|
||
</view>
|
||
<view class="center">
|
||
<text class="time">{{ data.timing }}</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/suoding.png"></image>
|
||
<image style="margin-left: 30rpx;" class="icon" src="/static/image/other/video-call/add-user.png">
|
||
</image>
|
||
</view>
|
||
|
||
</view>
|
||
</NavBar>
|
||
|
||
<!-- 视频画面区域 -->
|
||
<!-- :style="{ background: !(data.isSwapped ? videoData.cameraOn : videoData.chat.other.isShowVideo) ? 'url(' + (data.isSwapped ? videoData.chat.me.avatar : videoData.chat.other.avatar) + ') no-repeat center/cover' : 'transparent' }" -->
|
||
<movable-area class="video-container">
|
||
<movable-view class="bg-2-movable" direction="all" :x="data.bg2X" :y="data.bg2Y" :animation="true"
|
||
@change="onBg2Change" @touchend="onBg2TouchEnd" @click="swapVideo"
|
||
style="height: 408rpx; width: 188rpx; z-index: 10;border-radius: 20rpx;overflow: hidden;">
|
||
|
||
<image v-if="!(data.isSwapped ? videoData.cameraOn : videoData.chat.other.isShowVideo)"
|
||
:src="data.isSwapped ? videoData.chat.me.avatar : videoData.chat.other.avatar" mode="aspectFill"
|
||
style="position: absolute; width: 100%; height: 100%; filter: blur(15px); transform: scale(1.2);">
|
||
</image>
|
||
<DomVideoPlayer ref="subVideoRef"
|
||
v-if="data.isSwapped ? videoData.cameraOn : videoData.chat.other.isShowVideo" class="bg-2"
|
||
style="height: 100%;width: 100%;object-fit: cover;border-radius: 20rpx;display: block;"
|
||
:src="data.isSwapped ? getVideoUrl(videoData.chat.me.videoUrl) : getVideoUrl(videoData.chat.other.videoUrl)"
|
||
mode="aspectFill" autoplay loop :muted="data.isSwapped ? true : !videoData.speakerOn"
|
||
objectFit="cover" :controls="false" :show-play-btn="false">
|
||
</DomVideoPlayer>
|
||
|
||
<template v-if="data.isSwapped ? !videoData.cameraOn : !videoData.chat.other.isShowVideo">
|
||
<view class="mask" style="position: absolute;">
|
||
</view>
|
||
<image class="avatar-2" style="height: 92rpx;width:92rpx;object-fit: cover;"
|
||
:src="data.isSwapped ? videoData.chat.me.avatar : videoData.chat.other.avatar"
|
||
mode="aspectFill">
|
||
</image>
|
||
</template>
|
||
|
||
|
||
</movable-view>
|
||
</movable-area>
|
||
|
||
<!-- 底部控制栏 -->
|
||
<view class="control-bar">
|
||
<view class="control-buttons">
|
||
<!-- 麦克风 -->
|
||
<view class="control-item">
|
||
<image class="control-btn" :class="{ active: videoData.micOn }" @click="changeInfo('micOn')"
|
||
:src="videoData.micOn ? '/static/image/other/video-call/mic-on.png' : '/static/image/other/video-call/unMic.png'">
|
||
</image>
|
||
<!-- <view class="" :class="{ active: videoData.micOn }" >
|
||
|
||
</view> -->
|
||
<text class="control-label">{{ videoData.micOn ? '麦克风已开' : '麦克风已关' }}</text>
|
||
</view>
|
||
|
||
<!-- 扬声器 -->
|
||
<view class="control-item">
|
||
<image class="control-btn" :class="{ active: videoData.speakerOn }" @click="changeInfo('speakerOn')"
|
||
:src="videoData.speakerOn ? '/static/image/other/video-call/speaker-on.png' : '/static/image/other/video-call/unSpeaker.png'">
|
||
</image>
|
||
<text class="control-label">{{ videoData.speakerOn ? '扬声器已开' : '扬声器已关' }}</text>
|
||
</view>
|
||
|
||
<!-- 摄像头 -->
|
||
<view class="control-item">
|
||
<image class="control-btn" :class="{ active: videoData.cameraOn }" @click="changeInfo('cameraOn')"
|
||
:src="videoData.cameraOn ? '/static/image/other/video-call/camera-on.png' : '/static/image/other/video-call/unCamera.png'">
|
||
</image>
|
||
<text class="control-label">{{ videoData.cameraOn ? '摄像头已开' : '摄像头已关' }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="control-buttons" style="margin-bottom: 0;">
|
||
<image class="hangup-btn" src="/static/image/other/video-call/xvnibeijing.png">
|
||
</image>
|
||
|
||
<!-- 挂断按钮 -->
|
||
<image class="hangup-btn" @click="hangup" src="/static/image/other/video-call/hangup.png"></image>
|
||
|
||
<image class="hangup-btn" src="/static/image/other/video-call/xuanzhuan.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.showCallInfoPopup" class="popup-overlay" @click="closeCallInfoPopup">
|
||
<view class="popup-content" style="width: 600rpx;" @click.stop>
|
||
<view class="popup-header">
|
||
<text class="popup-title">编辑通话信息</text>
|
||
</view>
|
||
<view class="popup-body" style="padding: 20rpx 40rpx; max-height: 60vh; overflow-y: auto;">
|
||
|
||
<view style="margin-bottom: 30rpx;">
|
||
<view style="font-size: 28rpx; color: #333; margin-bottom: 10rpx; font-weight: bold;">对方昵称</view>
|
||
<view style="display: flex; align-items: center; justify-content: space-between;">
|
||
<input
|
||
style="flex: 1; border: 1px solid #eee; padding: 10rpx 20rpx; border-radius: 8rpx; font-size: 28rpx;height: 40px;"
|
||
v-model="videoData.chat.other.name" placeholder="请输入对方昵称" />
|
||
</view>
|
||
</view>
|
||
|
||
<view style="margin-bottom: 30rpx;">
|
||
<view style="font-size: 28rpx; color: #333; margin-bottom: 10rpx; font-weight: bold;">我方通话头像</view>
|
||
<view style="display: flex; align-items: center; justify-content: space-between;">
|
||
<image :src="videoData.chat.me.avatar"
|
||
style="width: 80rpx; height: 80rpx; border-radius: 10rpx;" mode="aspectFill"></image>
|
||
<button size="mini" style="margin: 0; background: #187AFF; color: #fff;"
|
||
@click="uploadMedia('me', 'avatar')">上传</button>
|
||
</view>
|
||
</view>
|
||
|
||
<view style="margin-bottom: 30rpx;">
|
||
<view style="font-size: 28rpx; color: #333; margin-bottom: 10rpx; font-weight: bold;">对方通话头像</view>
|
||
<view style="display: flex; align-items: center; justify-content: space-between;">
|
||
<image :src="videoData.chat.other.avatar"
|
||
style="width: 80rpx; height: 80rpx; border-radius: 10rpx;" mode="aspectFill"></image>
|
||
<button size="mini" style="margin: 0; background: #187AFF; color: #fff;"
|
||
@click="uploadMedia('other', 'avatar')">上传</button>
|
||
</view>
|
||
</view>
|
||
|
||
<view style="margin-bottom: 30rpx;">
|
||
<view style="font-size: 28rpx; color: #333; margin-bottom: 10rpx; font-weight: bold;">我方通话视频</view>
|
||
<view style="display: flex; align-items: center; justify-content: space-between;">
|
||
<text
|
||
style="font-size: 24rpx; color: #999; flex: 1; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; margin-right: 20rpx;">{{
|
||
getFileName(videoData.chat.me.videoUrl) }}</text>
|
||
<button size="mini" style="margin: 0; background: #187AFF; color: #fff;"
|
||
@click="uploadMedia('me', 'video')">上传</button>
|
||
</view>
|
||
</view>
|
||
|
||
<view style="margin-bottom: 30rpx;">
|
||
<view style="font-size: 28rpx; color: #333; margin-bottom: 10rpx; font-weight: bold;">对方通话视频</view>
|
||
<view style="display: flex; align-items: center; justify-content: space-between;">
|
||
<text
|
||
style="font-size: 24rpx; color: #999; flex: 1; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; margin-right: 20rpx;">{{
|
||
getFileName(videoData.chat.other.videoUrl) }}</text>
|
||
<button size="mini" style="margin: 0; background: #187AFF; color: #fff;"
|
||
@click="uploadMedia('other', 'video')">上传</button>
|
||
</view>
|
||
</view>
|
||
|
||
</view>
|
||
<view class="popup-footer">
|
||
<button class="btn-save" style="width: 100%; border-radius: 0 0 16rpx 16rpx;"
|
||
@click="closeCallInfoPopup">完成</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 { toRefs, onMounted, onUnmounted, reactive, computed, getCurrentInstance, ref } from 'vue'
|
||
import { onLoad, onShow, onHide } from '@dcloudio/uni-app'
|
||
import { util } from '@/utils/common.js'
|
||
const {
|
||
proxy
|
||
} = getCurrentInstance();
|
||
|
||
const mainVideoRef = ref(null)
|
||
const subVideoRef = ref(null)
|
||
|
||
const autoPlayVideos = () => {
|
||
setTimeout(() => {
|
||
if (mainVideoRef.value) mainVideoRef.value.play()
|
||
if (subVideoRef.value) subVideoRef.value.play()
|
||
}, 200)
|
||
}
|
||
|
||
const getVideoUrl = (url) => {
|
||
// #ifdef APP-PLUS
|
||
if (url && !url.startsWith('http') && !url.startsWith('blob:')) {
|
||
return plus.io.convertLocalFileSystemURL(url)
|
||
}
|
||
// #endif
|
||
return url
|
||
}
|
||
|
||
// 提取完整路径中的文件名
|
||
const getFileName = (path) => {
|
||
if (!path) return ''
|
||
// 兼容处理正反斜杠
|
||
return path.split('/').pop().split('\\').pop()
|
||
}
|
||
|
||
const buttonGroup = computed(() => [
|
||
{
|
||
name: "编辑时间",
|
||
click: () => {
|
||
openTimeEditPopup()
|
||
}
|
||
},
|
||
{
|
||
name: "编辑通话信息",
|
||
click: () => {
|
||
data.showCallInfoPopup = true
|
||
}
|
||
},
|
||
{
|
||
name: "对方视频",
|
||
isSwitch: true,
|
||
value: data.videoData.chat.other.isShowVideo,
|
||
click: () => {
|
||
data.videoData.chat.other.isShowVideo = !data.videoData.chat.other.isShowVideo
|
||
uni.setStorageSync('videoChatData', data.videoData)
|
||
}
|
||
}
|
||
])
|
||
|
||
const data = reactive({
|
||
videoData: {
|
||
micOn: true,
|
||
speakerOn: true,
|
||
cameraOn: true,
|
||
timeText: '25:22',
|
||
chat: {
|
||
me: {
|
||
videoUrl: "",
|
||
imagePath: "",
|
||
avatar: "/static/image/shopping/pdd/avatars/avatars1.jpg",
|
||
},
|
||
other: {
|
||
videoUrl: "",
|
||
imagePath: "",
|
||
name: "大宝贝",
|
||
avatar: "/static/image/shopping/pdd/avatars/avatars2.jpg",
|
||
isShowVideo: true,
|
||
}
|
||
}
|
||
},
|
||
timing: 0,
|
||
videoDataBackup: null, // 编辑模式备份
|
||
isEdit: false,
|
||
showCallInfoPopup: 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 // 是否正在拖动
|
||
},
|
||
// bg-2拖动状态
|
||
bg2X: 9999,
|
||
bg2Y: 0,
|
||
isSwapped: true // 是否交换了展示画面
|
||
})
|
||
|
||
// 非响应式变量用于记录拖动位置,防止高频触发页面重绘
|
||
let currentDragPos = { x: 9999, y: 0 }
|
||
|
||
const isAnswered = ref(false)
|
||
const answerCall = () => {
|
||
isAnswered.value = true
|
||
autoPlayVideos()
|
||
}
|
||
|
||
// 定时器引用,用于页面卸载时清理
|
||
let statusBarTimer = null
|
||
let timingTimer = 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 {}
|
||
}
|
||
|
||
const onBg2Change = (e) => {
|
||
if (e.detail.source === 'touch') {
|
||
currentDragPos.x = e.detail.x
|
||
currentDragPos.y = e.detail.y
|
||
}
|
||
}
|
||
|
||
const swapVideo = () => {
|
||
// 仅切换展示状态标志位,不修改原始数据属性
|
||
data.isSwapped = !data.isSwapped
|
||
// 切换源后强制触发起播
|
||
autoPlayVideos()
|
||
}
|
||
|
||
const onBg2TouchEnd = () => {
|
||
const systemInfo = uni.getSystemInfoSync()
|
||
const windowWidth = systemInfo.windowWidth
|
||
const rpx2px = windowWidth / 750
|
||
const containerWidthPx = windowWidth - 32 * rpx2px
|
||
const imageWidthPx = 188 * rpx2px
|
||
const maxXPx = containerWidthPx - imageWidthPx
|
||
|
||
let targetX = currentDragPos.x > maxXPx / 2 ? maxXPx : 0
|
||
|
||
data.bg2X = currentDragPos.x
|
||
data.bg2Y = currentDragPos.y
|
||
setTimeout(() => {
|
||
data.bg2X = targetX
|
||
}, 50)
|
||
}
|
||
|
||
onMounted(() => {
|
||
const systemInfo = uni.getSystemInfoSync()
|
||
statusBarHeight.value = systemInfo.statusBarHeight || 0
|
||
|
||
const windowWidth = systemInfo.windowWidth
|
||
const rpx2px = windowWidth / 750
|
||
const containerWidthPx = windowWidth - 32 * rpx2px
|
||
const imageWidthPx = 188 * rpx2px
|
||
data.bg2X = containerWidthPx - imageWidthPx
|
||
currentDragPos.x = data.bg2X
|
||
})
|
||
|
||
onLoad(() => {
|
||
// 强制干掉旧缓存,确保能顺利读取您代码中新写的初始数据
|
||
// uni.removeStorageSync('videoChatData')
|
||
|
||
const videoData = uni.getStorageSync('videoChatData') || data.videoData
|
||
|
||
const config = uni.getStorageSync('config')
|
||
|
||
console.log("---config---", config);
|
||
let videoPath = config.config['client.uniapp.video_path'].video_call;
|
||
if (videoData.chat.me.videoUrl == '') {
|
||
videoData.chat.me.videoUrl = videoPath + 'video_call_boy_001.mp4'
|
||
}
|
||
if (videoData.chat.other.videoUrl == '') {
|
||
videoData.chat.other.videoUrl = videoPath + 'video_call_girl_001.mp4'
|
||
}
|
||
|
||
console.log('videoData1', videoData)
|
||
const videoDataNew = {
|
||
...videoData
|
||
}
|
||
data.timing = videoData.timeText
|
||
data.videoData = videoDataNew
|
||
console.log('videoData2', data.videoData)
|
||
|
||
|
||
|
||
|
||
|
||
// 进入视频群聊页面埋点
|
||
proxy.$apiUserEvent('all', {
|
||
type: 'event',
|
||
key: 'video_chat',
|
||
prefix: '.uni.other.',
|
||
value: "视频单聊"
|
||
})
|
||
})
|
||
|
||
onShow(() => {
|
||
// 恢复计时
|
||
const videoData = uni.getStorageSync('videoChatData')
|
||
if (videoData && videoData.timeText) {
|
||
data.timing = videoData.timeText
|
||
}
|
||
startTimer()
|
||
|
||
// 触发自动播放
|
||
autoPlayVideos()
|
||
|
||
// #ifdef APP-PLUS
|
||
util.setAndroidSystemBarColor('#FFFFFF')
|
||
// 保存定时器引用,以便在页面卸载时清理
|
||
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
|
||
|
||
|
||
stopTimer() // 停止计时
|
||
console.log('🚪 页面隐藏,已清理定时器和状态')
|
||
})
|
||
|
||
// 页面卸载时清理所有定时器和资源
|
||
onUnmounted(() => {
|
||
// 清理状态栏定时器
|
||
if (statusBarTimer) {
|
||
clearTimeout(statusBarTimer)
|
||
statusBarTimer = null
|
||
}
|
||
stopTimer() // 停止计时
|
||
|
||
// 清理长按定时器
|
||
if (data.dragState.longPressTimer) {
|
||
clearTimeout(data.dragState.longPressTimer)
|
||
data.dragState.longPressTimer = null
|
||
}
|
||
|
||
console.log('🧹 页面卸载,已清理所有定时器')
|
||
})
|
||
|
||
// 打开时间编辑弹窗
|
||
const openTimeEditPopup = () => {
|
||
// 解析当前时间 (格式: "125:22")
|
||
const timingStr = data.timing ? String(data.timing) : '0:00'
|
||
const parts = timingStr.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('videoChatData', data.videoData)
|
||
|
||
// 更新当前计时显示并重启计时器以确保同步
|
||
data.timing = data.videoData.timeText
|
||
startTimer()
|
||
|
||
// 关闭弹窗
|
||
data.showTimeEditPopup = false
|
||
}
|
||
|
||
const uploadMedia = (type, mediaType) => {
|
||
if (mediaType === 'avatar') {
|
||
uni.chooseImage({
|
||
count: 1,
|
||
sizeType: ['original'], // 防止 iOS 压缩时丢弃图片旋转方向
|
||
success: (res) => {
|
||
const tempPath = res.tempFilePaths[0]
|
||
uni.saveFile({
|
||
tempFilePath: tempPath,
|
||
success: (saveRes) => {
|
||
data.videoData.chat[type].avatar = saveRes.savedFilePath
|
||
uni.setStorageSync('videoChatData', data.videoData)
|
||
}
|
||
})
|
||
}
|
||
})
|
||
} else if (mediaType === 'video') {
|
||
uni.chooseVideo({
|
||
count: 1,
|
||
compressed: false, // 核心修复:禁止 iOS 强制重新编码,保留原视频竖向元数据(Matrix属性)
|
||
success: (res) => {
|
||
let tempPath = res.tempFilePath
|
||
// #ifdef APP-PLUS
|
||
tempPath = plus.io.convertLocalFileSystemURL(tempPath)
|
||
// #endif
|
||
uni.saveFile({
|
||
tempFilePath: tempPath,
|
||
success: (saveRes) => {
|
||
data.videoData.chat[type].videoUrl = saveRes.savedFilePath
|
||
uni.setStorageSync('videoChatData', data.videoData)
|
||
autoPlayVideos()
|
||
}
|
||
})
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// 关闭时间编辑弹窗
|
||
const closeTimeEditPopup = () => {
|
||
data.showTimeEditPopup = false
|
||
}
|
||
|
||
// 关闭并保存通话信息编辑弹窗
|
||
const closeCallInfoPopup = () => {
|
||
// 保存所有更改(包括刚输入的对方昵称)到缓存
|
||
uni.setStorageSync('videoChatData', data.videoData)
|
||
data.showCallInfoPopup = 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 && !data.videoData.micOn) {
|
||
return
|
||
}
|
||
item.iconType = item.iconType == 2 ? 0 : 2
|
||
uni.setStorageSync('videoChatData', 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('videoChatData', 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('videoChatData', 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('videoChatData', 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: () => {
|
||
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) => {
|
||
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 = () => {
|
||
// 清除长按定时器
|
||
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('videoChatData', 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()
|
||
}
|
||
|
||
// 开始计时
|
||
const startTimer = () => {
|
||
stopTimer()
|
||
timingTimer = setInterval(() => {
|
||
if (!data.timing) return
|
||
let timeStr = String(data.timing)
|
||
if (timeStr.indexOf(':') === -1) return
|
||
|
||
let parts = timeStr.split(':')
|
||
let minutes = parseInt(parts[0]) || 0
|
||
let seconds = parseInt(parts[1]) || 0
|
||
|
||
seconds++
|
||
if (seconds >= 60) {
|
||
seconds = 0
|
||
minutes++
|
||
}
|
||
|
||
data.timing = `${minutes}:${seconds.toString().padStart(2, '0')}`
|
||
}, 1000)
|
||
}
|
||
|
||
// 停止计时
|
||
const stopTimer = () => {
|
||
if (timingTimer) {
|
||
clearInterval(timingTimer)
|
||
timingTimer = null
|
||
}
|
||
}
|
||
</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: 60rpx;
|
||
height: 60rpx;
|
||
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: 32rpx;
|
||
color: #ffffff;
|
||
}
|
||
}
|
||
|
||
.right {
|
||
width: 80px;
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
|
||
.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;
|
||
width: calc(100vw - 32rpx);
|
||
margin: 24rpx 16rpx;
|
||
margin-bottom: 0;
|
||
|
||
.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;
|
||
position: relative;
|
||
z-index: 9;
|
||
|
||
.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%;
|
||
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: 22rpx;
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
.hangup-btn {
|
||
width: 120rpx;
|
||
height: 120rpx;
|
||
border-radius: 50%;
|
||
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;
|
||
}
|
||
|
||
.bg-2-movable {
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
width: 188rpx;
|
||
height: 408rpx;
|
||
border-radius: 20rpx !important;
|
||
}
|
||
|
||
.avatar-2 {
|
||
width: 92rpx;
|
||
height: 92rpx;
|
||
position: absolute;
|
||
border-radius: 12rpx;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
|
||
.avatar-1 {
|
||
width: 136rpx;
|
||
height: 136rpx;
|
||
position: absolute;
|
||
top: 350rpx;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
|
||
.top-view {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: absolute;
|
||
top: 350rpx;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
|
||
.nickname {
|
||
font-size: 20px;
|
||
color: #FFFFFF;
|
||
line-height: 40rpx;
|
||
margin-top: 24rpx;
|
||
}
|
||
|
||
.dot {
|
||
flex-shrink: 0;
|
||
display: inline-block;
|
||
width: 18rpx;
|
||
height: 18rpx;
|
||
max-height: 18rpx;
|
||
max-width: 18rpx;
|
||
background-color: #FFFFFF;
|
||
border-radius: 50%;
|
||
margin: 16px 6rpx;
|
||
animation: dotBlink 0.9s infinite;
|
||
}
|
||
|
||
@keyframes dotBlink {
|
||
|
||
0%,
|
||
33.32% {
|
||
opacity: 1;
|
||
}
|
||
|
||
33.33%,
|
||
66.65% {
|
||
opacity: 0.5;
|
||
}
|
||
|
||
66.66%,
|
||
100% {
|
||
opacity: 0.3;
|
||
}
|
||
}
|
||
|
||
.mask {
|
||
background: rgba(0, 0, 0, 0.8);
|
||
width: 190rpx;
|
||
height: 410rpx;
|
||
border-radius: 20rpx;
|
||
}
|
||
|
||
.shadow-up {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 750rpx;
|
||
height: 240rpx;
|
||
background: linear-gradient(180deg, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%);
|
||
border-radius: 0rpx 0rpx 0rpx 0rpx;
|
||
z-index: 2;
|
||
}
|
||
|
||
.shadow-down {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
width: 750rpx;
|
||
height: 240rpx;
|
||
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.5) 100%);
|
||
border-radius: 0rpx 0rpx 0rpx 0rpx;
|
||
z-index: 2;
|
||
}
|
||
</style>
|