1132 lines
28 KiB
Vue
1132 lines
28 KiB
Vue
<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="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 :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 } from 'vue'
|
||
import { onLoad, onShow, onHide } 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', videoUrl: '', savedVideoUrl: '', iconType: 0 },
|
||
{ preview: '/static/image/other/video-call/defualt/video-img2.png', videoUrl: '', savedVideoUrl: '', iconType: 1 },
|
||
{ preview: '/static/image/other/video-call/defualt/video-img3.png', videoUrl: '', savedVideoUrl: '', iconType: 2 }
|
||
]
|
||
},
|
||
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)
|
||
})
|
||
|
||
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: 1
|
||
}
|
||
console.log('✅ 已替换索引', data.currentEditIndex, '的视频')
|
||
} else {
|
||
// 新增模式:添加新项
|
||
data.videoData.videoList.push({
|
||
preview: '',
|
||
videoUrl: videoUrl,
|
||
savedVideoUrl: savedVideoUrl,
|
||
iconType: 1
|
||
})
|
||
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: 1
|
||
}
|
||
console.log('✅ 已替换索引', data.currentEditIndex, '的图片')
|
||
} else {
|
||
// 新增模式:添加新项
|
||
data.videoData.videoList.push(...res.tempFilePaths.map((item) => ({
|
||
preview: item,
|
||
videoUrl: '',
|
||
savedVideoUrl: '',
|
||
iconType: 1
|
||
})))
|
||
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
|
||
|
||
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>
|