对接后端 hgmjapi.mfcg.me: - 接口解密 key 更新为 resKey360 - 登录 sysType 使用原生 getDevType(后端已放开 pc/android) - 修正榜单接口路径 /vid/rangking → /vid/ranking,方法改为 GET Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
348 lines
9.8 KiB
Vue
348 lines
9.8 KiB
Vue
<template>
|
|
<div class="upload-video" @click.stop>
|
|
<van-uploader accept="video/*" :max-count="1" :after-read="afterVideo" v-if="!state.startUploadVideo">
|
|
<div class="upload-box">
|
|
<!-- <SvgIcon name="upload_add" height="15px" width="15px"></SvgIcon>-->
|
|
<img class="icon" src="@/assets/images/png/upload_add.png" alt="" />
|
|
<span>添加视频</span>
|
|
</div>
|
|
</van-uploader>
|
|
<!-- 上传中样式 -->
|
|
<div v-if="state.startUploadVideo" class="uploading">
|
|
<video class="previewVideo" id="video" style="object-fit: cover"></video>
|
|
<div class="video-mask"></div>
|
|
<van-circle
|
|
v-model:current-rate="state.currentRate"
|
|
:rate="state.uploadStatue.percent"
|
|
:speed="100"
|
|
color="#00b95b"
|
|
:text="state.currentRate + '%'"
|
|
size="50px"
|
|
class="circle"
|
|
/>
|
|
<p :class="state.uploadStatue.statusText == '上传成功' ? 'green' : 'F5164E'">
|
|
{{ state.uploadStatue.statusText }}
|
|
</p>
|
|
<div
|
|
class="close"
|
|
v-if="state.uploadStatue.statusText == '上传成功' || state.uploadStatue.statusText == '上传失败'"
|
|
@click="uploadDelete"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<script lang="ts" setup>
|
|
import SparkMD5 from 'spark-md5';
|
|
import { Buffer } from 'buffer';
|
|
import { reactive, nextTick } from 'vue';
|
|
import CommunityApi from '@/service/community';
|
|
|
|
const state = reactive({
|
|
requestAll: <any>[], //总请求
|
|
requestNum: 0, //正在请求的数量
|
|
requestMaxNum: 6, //同时最大请求数量
|
|
chunk_size: 1024 * 1024 * 2, //分片大小
|
|
startUploadVideo: false, //视频是否上传中
|
|
currentRate: 0,
|
|
uploadStatue: {
|
|
statusText: '上传中,请勿退出',
|
|
percent: 0, //当前上传文件执行的百分比
|
|
},
|
|
totalProgress: <any>{}, //总进度
|
|
sendVideoSucNum: 0, //分片上传视频,成功收到的返回
|
|
videoHeight: 0,
|
|
uploadData: {
|
|
newsType: 'SP', //上传类型 COVER-图集 SP-视频
|
|
tags: [], //视频标签数组
|
|
playTime: 0, //视频时长
|
|
cover: '', //封面
|
|
coverThumb: '', //封面小图
|
|
seriesCover: [], // 图集图片地址
|
|
via: '', //视频来源
|
|
coins: 0, //观看金币
|
|
size: 0, //文件大小
|
|
mimeType: '', //文件类型
|
|
actor: '', //演员名字
|
|
sourceURL: '', //资源url 为SP时 必传
|
|
sourceID: '', //上��视频成功后 返回���ID 为SP时必传
|
|
filename: '', //文件名
|
|
resolution: '', //分辨率
|
|
ratio: 1, //视频比例
|
|
md5: '', //文件摘要
|
|
freeTime: 0, //免费观影时长
|
|
location: {
|
|
city: '',
|
|
}, //地理位置信息
|
|
isActivity: false, //是否参加大赛
|
|
},
|
|
newUpload: {
|
|
sourceID: '', //上��视频成功后 返回���ID
|
|
md5: '', //文件摘要
|
|
type: 'vid',
|
|
src: '',
|
|
height: '',
|
|
width: '',
|
|
},
|
|
});
|
|
const emit = defineEmits(['uploadVideoSuccess']);
|
|
const fileReaderPromisify = (file: any) => {
|
|
return new Promise((resolve, reject) => {
|
|
let reader = new FileReader();
|
|
let Spark = new SparkMD5.ArrayBuffer();
|
|
reader.readAsArrayBuffer(file);
|
|
reader.onload = function (e: any) {
|
|
Spark.append(e.target.result);
|
|
let md5 = Spark.end();
|
|
resolve(md5);
|
|
};
|
|
reader.onerror = (err) => {
|
|
reject(err);
|
|
};
|
|
});
|
|
};
|
|
// 上传视频
|
|
const afterVideo = async (e: any) => {
|
|
state.startUploadVideo = true;
|
|
let file = e.file;
|
|
let fileMd5: any = await fileReaderPromisify(file);
|
|
let file_chunks = getFileChunks(file);
|
|
let length = file_chunks.length;
|
|
for (let index in file_chunks) {
|
|
let chunk = file_chunks[Number(index)];
|
|
let opts = {
|
|
pos: Number(index) + 1,
|
|
totalPos: length,
|
|
data: Buffer.from(await chunk.arrayBuffer(), 'utf8').toString('base64'),
|
|
id: fileMd5,
|
|
};
|
|
state.uploadData.md5 = fileMd5;
|
|
state.newUpload.md5 = fileMd5;
|
|
state.requestAll.push(opts);
|
|
}
|
|
getVideoPlayerInfo(file); //获取视频宽高
|
|
requestConcurrency();
|
|
};
|
|
//限制请求数量
|
|
const requestConcurrency = () => {
|
|
while (state.requestAll.length > 0 && state.requestNum <= state.requestMaxNum && state.requestNum >= 0) {
|
|
let opts = state.requestAll.shift();
|
|
upload(opts);
|
|
}
|
|
};
|
|
// 将视频分片
|
|
const getFileChunks = (file: any) => {
|
|
let { size } = file;
|
|
let total_chunks = Math.ceil(size / state.chunk_size);
|
|
let file_chunks = [];
|
|
if (size > state.chunk_size) {
|
|
for (let i = 0; i < total_chunks; i++) {
|
|
let start = i * state.chunk_size;
|
|
let end = (i + 1) * state.chunk_size;
|
|
let chunk = file.slice(start, end);
|
|
state.totalProgress[i] = 0;
|
|
file_chunks.push(chunk);
|
|
}
|
|
} else {
|
|
file_chunks.push(file);
|
|
state.totalProgress[0] = 0;
|
|
}
|
|
return file_chunks;
|
|
};
|
|
// 开始上传
|
|
const upload = async (options: any) => {
|
|
try {
|
|
state.requestNum++;
|
|
let callback = (processCallback: any) => {
|
|
let percent = parseInt(((processCallback.loaded / processCallback.total) * 100).toString());
|
|
state.totalProgress[options.pos - 1] = percent.toFixed(2);
|
|
let strArr = Object.values(state.totalProgress);
|
|
let numberArr = strArr.map(Number);
|
|
state.uploadStatue.percent = Number(average(numberArr).toFixed(1));
|
|
};
|
|
let ret: any = await CommunityApi.uploadVideo(options, callback);
|
|
state.sendVideoSucNum++;
|
|
if (state.uploadStatue.percent == 100) {
|
|
// 所有分片请求发送完毕,等待返回
|
|
state.uploadStatue.statusText = '处理中,请勿退出';
|
|
}
|
|
if (state.uploadStatue.percent == 100 && state.sendVideoSucNum == options.totalPos) {
|
|
state.uploadStatue.statusText = '上传成功';
|
|
state.uploadData.sourceURL = ret.videoUri;
|
|
state.uploadData.sourceID = ret.id;
|
|
state.newUpload.sourceID = ret.id;
|
|
state.newUpload.src = ret.videoUri;
|
|
emit('uploadVideoSuccess', state.uploadData);
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
state.currentRate = 0;
|
|
state.uploadStatue.percent = 0;
|
|
state.uploadStatue.statusText = '上传失败';
|
|
state.sendVideoSucNum = 0;
|
|
} finally {
|
|
state.requestNum--;
|
|
requestConcurrency();
|
|
}
|
|
};
|
|
// 获取视频信息
|
|
const getVideoPlayerInfo = (file: any) => {
|
|
state.uploadData.size = file.size;
|
|
state.uploadData.filename = file.name;
|
|
state.uploadData.mimeType = file.type;
|
|
// state.newUpload.type = file.type;
|
|
nextTick(() => {
|
|
const videoElement: any = document.getElementById('video');
|
|
videoElement.src = URL.createObjectURL(file);
|
|
videoElement.addEventListener('loadedmetadata', () => {
|
|
state.uploadData.playTime = parseInt(videoElement.duration);
|
|
state.videoHeight = videoElement.videoHeight;
|
|
state.uploadData.ratio = Number((videoElement.videoWidth / videoElement.videoHeight).toFixed(2));
|
|
state.uploadData.resolution = videoElement.videoWidth + 'x' + videoElement.videoHeight;
|
|
state.newUpload.height = videoElement.videoHeight;
|
|
state.newUpload.width = videoElement.videoWidth;
|
|
});
|
|
});
|
|
};
|
|
// 还原默认数据
|
|
const reductionData = () => {
|
|
state.uploadData = {
|
|
newsType: 'SP', //上传类型 COVER-图集 SP-视频
|
|
tags: [], //视频标签数组
|
|
playTime: 0, //视频时长
|
|
cover: '', //封面
|
|
coverThumb: '', //封面小图
|
|
seriesCover: [], // 图集图片地址
|
|
via: '', //视频来源
|
|
coins: 0, //观看金币
|
|
size: 0, //文件大小
|
|
mimeType: '', //文件类型
|
|
actor: '', //演员名字
|
|
sourceURL: '', //资源url 为SP时 必传
|
|
sourceID: '', //上��视频成功后 返回���ID 为SP时必传
|
|
filename: '', //文件名
|
|
resolution: '', //分辨率
|
|
ratio: 1, //视频比例
|
|
md5: '', //文件摘要
|
|
freeTime: 0, //免费观影时长
|
|
location: {
|
|
city: '',
|
|
}, //地理位置信息
|
|
isActivity: false, //是否参加大赛
|
|
};
|
|
state.newUpload = {
|
|
sourceID: '',
|
|
md5: '', //文件摘要
|
|
type: 'vid',
|
|
src: '',
|
|
height: '',
|
|
width: '',
|
|
};
|
|
};
|
|
// 删除视频
|
|
const uploadDelete = () => {
|
|
state.newUpload.src = 'vid';
|
|
state.uploadData.sourceURL = '';
|
|
state.uploadStatue.statusText = '上传中,请勿退出';
|
|
state.uploadStatue.percent = 0;
|
|
state.currentRate = 0;
|
|
state.sendVideoSucNum = 0;
|
|
state.startUploadVideo = false;
|
|
};
|
|
// 平均数
|
|
const average = (nums: any) => {
|
|
return nums.reduce((a: any, b: any) => a + b) / nums.length;
|
|
};
|
|
defineExpose({
|
|
reductionData,
|
|
uploadDelete,
|
|
});
|
|
</script>
|
|
<style lang="scss" scoped>
|
|
.upload-video {
|
|
margin: 0 3px 0 0;
|
|
.upload-box {
|
|
width: 111px;
|
|
height: 111px;
|
|
border-radius: 6px;
|
|
border: dashed 1px rgb(154, 154, 154);
|
|
background: #262626;
|
|
display: flex;
|
|
align-items: center;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
position: relative;
|
|
box-sizing: border-box;
|
|
|
|
span {
|
|
font-size: 12px;
|
|
color: #9a9a9a;
|
|
margin-top: 9px;
|
|
}
|
|
|
|
.icon {
|
|
width: 30px;
|
|
height: 30px;
|
|
}
|
|
}
|
|
|
|
.upload-box span {
|
|
font-size: 10px;
|
|
color: rgb(129, 129, 129);
|
|
margin-top: 5.5px;
|
|
}
|
|
|
|
.uploadCover {
|
|
padding-left: 0.16rem;
|
|
}
|
|
/* 上传中 */
|
|
.uploading {
|
|
width: 111px;
|
|
height: 111px;
|
|
border-radius: 6px;
|
|
color: #fff;
|
|
background: #fff;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
position: relative;
|
|
font-size: 10px;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.previewVideo {
|
|
width: 111px;
|
|
height: 111px;
|
|
border-radius: 6px;
|
|
position: absolute;
|
|
}
|
|
|
|
.video-mask {
|
|
width: 111px;
|
|
height: 111px;
|
|
border-radius: 6px;
|
|
position: absolute;
|
|
background: rgba(0, 0, 0, 0.6);
|
|
}
|
|
|
|
.circle {
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.uploading .close {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
width: 20px;
|
|
height: 20px;
|
|
background: url('../../assets/images/png/community_close.png') center center no-repeat;
|
|
background-size: 100%;
|
|
}
|
|
|
|
:deep(.van-circle__text) {
|
|
color: #fff;
|
|
font-size: 12px;
|
|
}
|
|
}
|
|
</style>
|