424 lines
11 KiB
Vue
424 lines
11 KiB
Vue
<template>
|
||
<!-- 上传短视频 -->
|
||
<view class="upload-content">
|
||
<view class="progress-box" v-if="showProgress">
|
||
<text class="progress-text">{{progress}}%</text>
|
||
<text class="progress-lable">上传中</text>
|
||
<progress :percent="progress"></progress>
|
||
</view>
|
||
<view v-if="video" class="video-preview">
|
||
<video
|
||
:src="'https://app.mrrweb.com.cn' + video"
|
||
ref="videoPlayer"
|
||
v-if="video"
|
||
class="preview-video"
|
||
:controls="false"
|
||
object-fit="cover"
|
||
:autoplay="true"
|
||
:muted="true"
|
||
:initial-time="1"
|
||
:show-center-play-btn="false"
|
||
:show-loading="false"
|
||
:enable-progress-gesture="false"
|
||
:show-fullscreen-btn="false"
|
||
:show-play-btn="false"
|
||
@play="onPlay"
|
||
></video>
|
||
<!-- 备用提示 -->
|
||
<view v-if="showFallback" class="video-fallback">
|
||
<text>视频加载失败,请尝试刷新或更换视频源</text>
|
||
</view>
|
||
<view class="delete-btn" @click="deleteVideo">×</view>
|
||
</view>
|
||
<view v-else class="upload-btn" @click="chooseVideo">
|
||
<text class="plus-icon">+</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import request from '../../utils/request';
|
||
import sparkMD5 from 'spark-md5'; // 引入MD5计算库
|
||
export default {
|
||
name:"upload-video",
|
||
props: {
|
||
video: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
videoNum: {
|
||
type: Number,
|
||
default: 1
|
||
},
|
||
},
|
||
data() {
|
||
return {
|
||
autoplay: false,
|
||
showProgress: false,
|
||
progress: 0,
|
||
showFallback: false,
|
||
finalVideoUrl: '',
|
||
platformAutoplay: true,
|
||
autoplayMuted: true,
|
||
};
|
||
},
|
||
// watch: {
|
||
// video: {
|
||
// handler(newValue) {
|
||
// this.video = newValue
|
||
// },
|
||
// deep: true
|
||
// }
|
||
// },
|
||
methods: {
|
||
// 获取文件 (兼容各平台)
|
||
getFile() {
|
||
return new Promise((resolve, reject) => {
|
||
// #ifdef H5
|
||
const input = document.createElement('input')
|
||
input.type = 'file'
|
||
input.accept = this.accept
|
||
input.onchange = (e) => {
|
||
// resolve(e.target.files[0])
|
||
resolve(e)
|
||
}
|
||
input.click()
|
||
// #endif
|
||
|
||
// #ifndef H5
|
||
uni.chooseVideo({
|
||
count: 1,
|
||
sourceType: ['album', 'camera'],
|
||
compressed: false,
|
||
maxDuration: 180,
|
||
camera: 'back',
|
||
success: (res) => {
|
||
// resolve(res.tempFiles[0])
|
||
resolve(res)
|
||
},
|
||
fail: (err) => {
|
||
reject(err)
|
||
}
|
||
})
|
||
// #endif
|
||
})
|
||
},
|
||
// 上传视频区域
|
||
async chooseVideo() {
|
||
// 选择文件(添加类型校验)
|
||
// const res = await uni.chooseVideo({
|
||
// sourceType: ['album', 'camera'],
|
||
// compressed: false,
|
||
// maxDuration: 180,
|
||
// camera: 'back'
|
||
// });
|
||
const res = await this.getFile();
|
||
console.log(res,';;;;;;;;;;;;;;;;;;;;;;;;;;;')
|
||
this.showProgress = true;
|
||
let size = res.size;
|
||
if(size > 5*1024*1024) {
|
||
// 统一获取文件列表
|
||
const files = res.tempFile || res;
|
||
const file = res.tempFile || res;
|
||
console.log('file',file)
|
||
// if (!files || files.length === 0) {
|
||
// uni.showToast({ title: '文件选择取消', icon: 'none' });
|
||
// return;
|
||
// }
|
||
|
||
if (!file) {
|
||
uni.showToast({ title: '视频选择取消', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
progress: (res) => {
|
||
console.log(`分片${index}进度: ${res.progress}%`);
|
||
}
|
||
|
||
// 通过 tempFiles 获取文件(UniApp规范)
|
||
// const file = files[0];
|
||
|
||
|
||
// 计算文件MD5(优化大文件计算)
|
||
const md5 = await this.calculateFileMD5(file);
|
||
|
||
// 分片大小(2MB)
|
||
const chunkSize = 1 * 1024 * 1024;
|
||
const totalChunks = Math.ceil(file.size / chunkSize);
|
||
|
||
// 分片上传
|
||
for (let index = 0; index < totalChunks; index++) {
|
||
const start = index * chunkSize;
|
||
const end = Math.min(file.size, start + chunkSize);
|
||
// #ifdef H5
|
||
// 浏览器环境使用标准的 slice 方法
|
||
const chunk = file.slice(start, end);
|
||
// 上传分片
|
||
await this.uploadChunk(chunk, index, totalChunks, md5);
|
||
// #endif
|
||
// #ifndef H5
|
||
// 非浏览器环境使用 Uniapp API
|
||
const fs = uni.getFileSystemManager();
|
||
const arrayBuffer = fs.readFileSync({
|
||
filePath: file,
|
||
position: start,
|
||
length: end - start
|
||
});
|
||
// 上传分片
|
||
await this.uploadChunk(arrayBuffer, index, totalChunks, md5);
|
||
// #endif
|
||
// 上传分片
|
||
// await this.uploadChunk(chunk, index, totalChunks, md5);
|
||
this.progress = parseInt((index+1)/totalChunks*100)
|
||
}
|
||
|
||
// 合并请求
|
||
await this.mergeChunks(md5, file.name);
|
||
}else {
|
||
let item = {
|
||
filePath: res.tempFilePath,
|
||
type: 'video'
|
||
}
|
||
this.uploadVideo(item)
|
||
}
|
||
},
|
||
uploadVideo(item) {
|
||
request.uploadVideo('/user/uploadvideo',item.filePath,'video',{type: item.type}).then(res=>{
|
||
this.$emit('addVideo',res.path)
|
||
// this.initVideo();
|
||
this.showProgress = false;
|
||
})
|
||
},
|
||
// 计算文件MD5(分块计算)
|
||
calculateFileMD5(file) {
|
||
return new Promise((resolve) => {
|
||
const chunkSize = 1 * 1024 * 1024;
|
||
const chunks = Math.ceil(file.size / chunkSize);
|
||
const spark = new sparkMD5.ArrayBuffer();
|
||
let currentChunk = 0;
|
||
|
||
const loadNext = () => {
|
||
const start = currentChunk * chunkSize;
|
||
const end = start + chunkSize >= file.size ? file.size : start + chunkSize;
|
||
let reader;
|
||
// #ifdef H5
|
||
reader = new FileReader();
|
||
// #endif
|
||
// #ifndef H5
|
||
reader = uni.getfilesystem;
|
||
// #endif
|
||
reader.onload = (e) => {
|
||
spark.append(e.target.result);
|
||
currentChunk++;
|
||
if (currentChunk < chunks) {
|
||
loadNext();
|
||
} else {
|
||
resolve(spark.end());
|
||
}
|
||
};
|
||
reader.readAsArrayBuffer(file.slice(start, end));
|
||
};
|
||
loadNext();
|
||
});
|
||
},
|
||
|
||
async uploadChunk(chunk, index, totalChunks, md5) {
|
||
let that = this;
|
||
try {
|
||
// 获取ArrayBuffer
|
||
const arrayBuffer = await new Promise((resolve) => {
|
||
let reader;
|
||
// #ifdef H5
|
||
reader = new FileReader();
|
||
// #endif
|
||
// #ifndef H5
|
||
reader = uni.getfilesystem;
|
||
// #endif
|
||
reader.onload = () => resolve(reader.result);
|
||
reader.readAsArrayBuffer(chunk);
|
||
});
|
||
|
||
// 平台差异化处理
|
||
let tempFilePath = '';
|
||
|
||
// 微信小程序/App端写入临时文件
|
||
// #ifdef MP-WEIXIN || APP-PLUS
|
||
const fs = uni.getFileSystemManager();
|
||
tempFilePath = `${getTempDir()}${md5}_${Date.now()}_${index}.tmp`;
|
||
|
||
await new Promise((resolve, reject) => {
|
||
fs.writeFile({
|
||
filePath: tempFilePath,
|
||
data: arrayBuffer,
|
||
encoding: 'binary',
|
||
success: resolve,
|
||
fail: (err) => reject(`文件写入失败: ${JSON.stringify(err)}`)
|
||
});
|
||
});
|
||
// #endif
|
||
|
||
// H5端特殊处理
|
||
// #ifdef H5
|
||
const blob = new Blob([arrayBuffer], { type: chunk.type });
|
||
const file = new File([blob], `${md5}_${index}.bin`, {
|
||
type: chunk.type,
|
||
lastModified: Date.now()
|
||
});
|
||
tempFilePath = file;
|
||
// #endif
|
||
const uploadTask = await request.uploadTask('/user/uploadvideoother',tempFilePath,'file',{chunkIndex: index,totalChunks: totalChunks,md5: md5});
|
||
|
||
// 上传后清理临时文件(非H5)
|
||
// #ifdef MP-WEIXIN || APP-PLUS
|
||
fs.unlink({ filePath: tempFilePath });
|
||
// #endif
|
||
|
||
return uploadTask;
|
||
} catch (error) {
|
||
console.error('分片上传失败:', error);
|
||
throw new Error(`分片${index}上传失败: ${error.message}`);
|
||
}
|
||
},
|
||
// 合并请求
|
||
mergeChunks(md5, fileName) {
|
||
let that = this;
|
||
request.post('/user/uploadvideomerge',{md5: md5,filename: fileName}).then(res=>{
|
||
that.$emit('addVideo',res.path)
|
||
// that.initVideo();
|
||
that.showProgress = false;
|
||
})
|
||
|
||
},
|
||
// 上传视频结束区域
|
||
// 删除视频
|
||
deleteVideo() {
|
||
this.$emit('deleteVideo')
|
||
},
|
||
initVideo() {
|
||
// 平台自动播放处理
|
||
this.platformAutoplay = this.autoplay;
|
||
|
||
// iOS自动播放必须静音
|
||
// #ifdef APP-PLUS
|
||
if (plus.os.name === 'iOS' && this.autoplay) {
|
||
this.autoplayMuted = true;
|
||
}
|
||
// #endif
|
||
|
||
// 路径处理
|
||
this.processVideoUrl();
|
||
},
|
||
processVideoUrl() {
|
||
if (!this.video) return;
|
||
|
||
// 处理APP本地路径
|
||
if (this.video.startsWith('file://') || this.video.startsWith('/storage')) {
|
||
// #ifdef APP-PLUS
|
||
this.finalVideoUrl = plus.io.convertLocalFileSystemURL(this.video);
|
||
// #endif
|
||
// #ifdef MP-WEIXIN || H5
|
||
this.finalVideoUrl = this.video;
|
||
// #endif
|
||
} else {
|
||
this.finalVideoUrl = this.video;
|
||
}
|
||
},
|
||
onPlay() {
|
||
console.log('视频开始播放');
|
||
this.platformAutoplay = true;
|
||
this.showFallback = false;
|
||
},
|
||
onError(e) {
|
||
console.error('视频播放错误:', e);
|
||
this.showFallback = true;
|
||
|
||
// 尝试备用方案
|
||
this.tryFallbackSource();
|
||
},
|
||
tryFallbackSource() {
|
||
// 可以在这里实现备用视频源逻辑
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.upload-content {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
position: relative;
|
||
}
|
||
.progress-box {
|
||
width: 204rpx;
|
||
height: 204rpx;
|
||
background-color: rgba(0 ,0 ,0 , 0.5);
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 99;
|
||
display: flex;
|
||
flex-flow: column nowrap;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
.progress-text {
|
||
font-size: 42rpx;
|
||
color: #FFFFFF;
|
||
font-weight: 500;
|
||
}
|
||
.progress-lable {
|
||
font-size: 28rpx;
|
||
color: #FFFFFF;
|
||
}
|
||
.progress-box progress {
|
||
/* width: 204rpx; */
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
}
|
||
.video-preview,
|
||
.upload-btn {
|
||
width: 200rpx;
|
||
height: 200rpx;
|
||
margin-right: 10rpx;
|
||
margin-bottom: 20rpx;
|
||
position: relative;
|
||
}
|
||
|
||
.preview-video {
|
||
width: 200rpx;
|
||
height: 200rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
.upload-btn {
|
||
border: 2rpx dashed #DDDDDD;
|
||
border-radius: 8rpx;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.plus-icon {
|
||
font-size: 60rpx;
|
||
color: #999999;
|
||
}
|
||
|
||
.delete-btn {
|
||
position: absolute;
|
||
top: -20rpx;
|
||
right: -20rpx;
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
border-radius: 50%;
|
||
color: #FFFFFF;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
font-size: 32rpx;
|
||
}
|
||
|
||
</style>
|