421 lines
11 KiB
Vue
421 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;
|
|||
|
|
// #ifdef H5
|
|||
|
|
const reader = new FileReader();
|
|||
|
|
// #endif
|
|||
|
|
// #ifndef H5
|
|||
|
|
const 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) => {
|
|||
|
|
// #ifdef H5
|
|||
|
|
const reader = new FileReader();
|
|||
|
|
// #endif
|
|||
|
|
// #ifndef H5
|
|||
|
|
const 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>
|