Initial commit

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 12:02:35 +08:00
co-authored by Claude Opus 5
commit d999a5c0e6
643 changed files with 178681 additions and 0 deletions
@@ -0,0 +1,503 @@
<template>
<el-dialog
title="添加子媒体任务"
:visible.sync="isShow"
width="80%"
center
@close="$emit('close')"
:show-close="false"
:close-on-click-modal="false"
>
<div class="add-video-warp-box">
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<div class="flex-item">
<el-form-item label="父媒体ID:" prop="mediaId">
<el-input v-model="form.mediaId" size="small"></el-input>
<el-button type="primary" size="small" @click="findFatherInfo()"
>查询父级媒体</el-button
>
</el-form-item>
<el-form-item label="父媒体标题:" prop="mediaTitle">
<el-input
:disabled="true"
v-model="mediaTitle"
size="small"
></el-input>
</el-form-item>
</div>
<div class="flex-item">
<el-form-item label="上传类型:" prop="mediaType">
<el-select
:disabled="true"
v-model="form.mediaType"
size="small"
placeholder="请选择上传类型"
clearable
>
<el-option value="video" label="视频"></el-option>
<el-option value="video_adv" label="视频广告"></el-option>
<el-option value="image" label="漫画"></el-option>
<el-option value="text" label="小说"></el-option>
<el-option value="drama" label="短剧"></el-option>
</el-select>
</el-form-item>
<el-form-item label="封面上传:" prop="cover" style="display: flex;">
<base-upload
:cover="form.cover"
:action="action"
@successCallback="successCallback"
:id="form.sourceID"
:hide="true"
:width="'90px'"
:height="'120px'"
></base-upload>
</el-form-item>
</div>
<div class="flex-item">
<el-form-item label="集数:" prop="episodeNumber">
<el-input
:disabled="isPermission"
v-model.number="form.episodeNumber"
size="small"
></el-input>
</el-form-item>
<el-form-item label="章节名:" prop="name">
<el-input
:disabled="isPermission"
v-model="form.name"
size="small"
></el-input>
</el-form-item>
</div>
<div class="flex-item">
<el-form-item label="是否激活:" prop="isActive">
<el-select
:disabled="isPermission"
v-model="form.isActive"
size="small"
placeholder="请选择是否激活"
>
<el-option :value="true" label="是"></el-option>
<el-option :value="false" label="否"></el-option>
</el-select>
</el-form-item>
<el-form-item label="收费类型:" prop="listenPermission">
<el-select
:disabled="isPermission"
v-model="form.listenPermission"
size="small"
placeholder="请选择收费类型"
>
<el-option
v-if="form.mediaType !== 'drama'"
:value="0"
label="会员"
></el-option>
<el-option :value="1" label="金币"></el-option>
<el-option :value="2" label="免费"></el-option>
</el-select>
</el-form-item>
<el-form-item
:disabled="isPermission"
label="购买价格:"
prop="price"
v-if="form.listenPermission == 1"
>
<el-input v-model.number="form.price" size="small"></el-input>
</el-form-item>
</div>
<div class="flex-item">
<el-form-item
label="上传视频:"
v-if="
form.mediaType === 'video' ||
form.mediaType === 'video_adv' ||
form.mediaType === 'drama'
"
>
<input
:disabled="isPermission"
type="file"
@change="changeFile"
accept="video/*"
id="inputFile"
/>
</el-form-item>
<el-form-item
style="width: 1100px"
label="上传图集:"
prop="cover"
v-if="form.mediaType === 'image'"
>
<!--上传图集-->
<imgs-upload
:cover="form.cover"
:action="actionPics"
@successCallback="successCallbackPic"
:id="form.sourceID"
:wSize="180"
:hSize="320"
:width="'100px'"
:height="'100px'"
></imgs-upload>
</el-form-item>
<el-form-item
style="width: 1100px"
label="小说内容:"
prop="text"
v-if="form.mediaType === 'text'"
>
<!--小说-->
<el-input
:disabled="isPermission"
type="textarea"
:rows="10"
placeholder="请输入小说内容"
v-model="form.text"
></el-input>
</el-form-item>
</div>
<div class="flex-item" v-if="form.mediaType === 'text'">
<!--小说音频上传-->
<el-upload
accept="audio/mp3"
class="upload-demo"
:action="actionAudio"
:headers="header"
:on-success="handleTrialChange"
:file-list="fileList"
:limit="1"
>
<el-button size="small" type="success" class="uploadMp3"
>点击上传</el-button
>
</el-upload>
</div>
<el-form-item class="bottomBtn">
<el-button
:disabled="isPermission"
type="primary"
size="small"
@click="submitDate"
>立即添加</el-button
>
<el-button size="small" @click="cancel">取消</el-button>
</el-form-item>
</el-form>
</div>
</el-dialog>
</template>
<script>
import { validatorIndex } from "@/utils/validator";
import baseConfig from "@/utils/serverUrlConfig";
import local from "@/utils/localStroage";
import API from "@/api";
export default {
props: ["isShow"],
data() {
return {
isPermission: true,
SeriesCover: [],
trackId: "",
show: false,
// 封面上传地址
action: baseConfig.baseServerUrl + "/vid/uploadStatic",
//图集上传
actionPics: baseConfig.baseServerUrl + "/vid/uploadStaticBatch",
// 视频播放地址
videoBaseUrl: baseConfig.playVideoUrl,
// 音频上传地址
actionAudio: baseConfig.baseServerUrl + "/vid/upload/audio",
header: {
Authorization: local.get("userInfo").token,
},
isFree: true,
flag: false,
player: "",
files: null,
// 表单数据
mediaTitle: "", //父级媒体标题
form: {
mediaId: "", //父级媒体id
mediaType: "video", //上传文件类型
episodeNumber: "", // 第几集
listenPermission: 0, // 收听权限 0:会员 1:金币购买 2:免费
price: 0, // 价格
cover: "", // 封面图
name: "", // 章节名
text: "", // 小说专用
audioUrl: "", // 小说专用
md5: "", // 媒体md5
sourceID: "", // 视频唯一id,对应文件服
videoUrl: "",
filename: "",
isActive: false, //是否激活
urlSet: [], // 图集
},
rules: {
title: [{ required: true, message: "请输入标题", trigger: "blur" }],
mediaType: [
{ required: true, message: "请选择上传类型", trigger: "blur" },
],
mediaId: [
{ required: true, message: "请输入父级媒体ID", trigger: "blur" },
],
name: [{ required: true, message: "请输入章节名", trigger: "blur" }],
price: [{ required: true, message: "请输入购买价格", trigger: "blur" }],
text: [{ required: true, message: "请输入小说内容", trigger: "blur" }],
episodeNumber: [
{ required: true, message: "请输入集数", trigger: "blur" },
{ validator: validatorIndex, trigger: "blur" },
],
},
};
},
created() {
// console.log('创建')
},
methods: {
changeFile(event) {
let files = event.target.files[0];
this.files = files;
},
cancel() {
this.cleanForm();
this.$emit("close");
},
findFatherInfo() {
if (this.form.mediaId === undefined || this.form.mediaId === "") {
this.$message.error("请输入父级媒体id");
return false;
}
API.getMediaInfo({ id: this.form.mediaId })
.then((res) => {
if (res.code === 200 && res.data) {
console.log(res.data);
if (res.data.isExist === true) {
this.isPermission = false;
this.form.mediaType = res.data.mediaType;
this.mediaTitle = res.data.mediaTitle;
if (res.data.mediaType === "drama") {
this.form.listenPermission = 2;
this.form.price = 0;
}
} else {
this.$message.error("父级媒体不存在");
return false;
}
}
})
.catch(() => {
this.isPermission = true;
});
},
// 处理音频
handleTrialChange(res) {
if (res.data.code === 200) {
this.form.audioUrl = res.data.data.fileName;
} else {
this.fileList = [];
}
},
// 提交验证
submitDate() {
if (
(this.form.mediaType === "video" || this.form.mediaType === "drama") &&
!this.files
) {
this.$message.error("请选择视频");
return false;
}
if (this.form.mediaType === "image" && this.form.urlSet.length <= 0) {
this.$message.error("请选择漫画图集");
return false;
}
this.$refs.form.validate((valid) => {
if (valid) {
if (this.form.mediaType === "drama" && this.form.listenPermission === 0) {
this.form.listenPermission = 2;
}
if (this.form.listenPermission !== 1) {
this.form.price = 0;
}
let data = {
files: this.files || null,
form: this.form,
time: new Date().getTime(),
};
this.$emit("addTaskSuccess", data);
this.$emit("close");
this.cleanForm();
} else {
return false;
}
});
},
cleanForm() {
this.files = null;
let dom = document.querySelector("#inputFile");
if (dom) {
dom.value = "";
}
this.isPermission = true;
this.mediaTitle = "";
this.form = {
mediaId: this.form.mediaId, //父级媒体id
mediaType: "video", //上传文件类型
episodeNumber: 1, // 第几集
listenPermission: 0, // 收听权限 0:会员 1:金币购买 2:免费
price: 0, // 价格
name: "", // 章节名
text: "", // 小说专用
md5: "", // 媒体md5
sourceID: "", // 视频唯一id,对应文件服
videoUrl: "",
filename: "",
isActive: false, //是否激活
urlSet: [], // 图集
};
},
// 封面图上传成功回调
successCallbackHeng(res) {
this.form.horizontalCover = res.data.coverImg;
},
successCallbackShu(res) {
this.form.verticalCover = res.data.coverImg;
},
// 封面图上传成功回调
successCallbackPic(seriesCover) {
this.form.urlSet = seriesCover;
},
// 封面图上传成功回调
successCallback(res) {
this.form.cover = res.data.coverImg;
},
},
};
</script>
<style lang="scss" scoped>
.add-video-warp-box {
/deep/ .el-select--small {
width: 250px !important;
}
.bottomBtn {
display: flex;
align-items: center;
justify-content: center;
margin-top: 14px;
}
.flex-item {
width: 90%;
display: flex;
.el-form-item {
flex: 1;
}
}
.el-upload .el-upload--picture-card {
width: 100px;
height: 100px;
}
.el-textarea {
width: 500px;
}
.el-input {
width: 250px;
}
.uploader-drop {
background: none;
border-radius: 5px;
border: none;
padding: 0;
}
.uploader-file {
border-bottom: 1px dashed #ccc;
}
.uploader-btn {
background: #e63765;
color: white;
padding: 5px 10px;
font-size: 13px;
border: none;
border-radius: 3px;
&:hover {
background: #eb5f84;
}
}
.title {
width: 100%;
height: 30px;
display: inline-block;
font-size: 14px;
line-height: 30px;
overflow: hidden;
color: #666;
margin-bottom: 15px;
cursor: pointer;
i {
&:hover {
color: #e63765;
}
}
span {
margin-left: 10px;
}
}
.upload-video {
width: 300px !important;
.circle-bar {
width: 100%;
height: 100%;
text-align: center;
.el-progress-circle {
margin: 0 auto;
margin-top: 8px;
}
}
.video-box {
width: 100%;
height: 100%;
}
}
.avatar-uploader {
width: 150px;
height: 150px;
}
.el-upload {
width: 100%;
height: 100%;
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
&:hover {
border: 1px dashed #e63765;
}
img {
width: 100%;
height: 100%;
}
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 150px;
height: 150px;
line-height: 150px;
text-align: center;
&:hover {
color: 1px dashed #e63765 !important;
}
}
}
.show {
left: 20px;
transition: left 0.2s linear;
}
.upload-demo {
display: flex;
margin: 0 0 0 50px;
}
</style>
@@ -0,0 +1,461 @@
<template>
<div class="movie_warp">
<el-button
type="primary"
size="small"
@click="isShow = true"
class="taskBtn"
>新增任务</el-button
>
<div class="movide_box">
<div class="left">
<h3>上传子媒体列表</h3>
<!--视频分片上传-->
<uploader
:options="options"
:fileStatusText="fileStatusText"
class="uploader-example"
ref="uploader"
@file-added="fileAdd"
@file-success="onFileSuccess"
>
<uploader-unsupport></uploader-unsupport>
<uploader-list class="upload_list" style="height: 600px">
<div class="file-panel" slot-scope="props">
<ul class="file-list">
<li v-for="file in props.fileList" :key="file.id">
<uploader-file
:class="'file_' + file.id"
ref="files"
:file="file"
:list="true"
>
</uploader-file>
</li>
</ul>
</div>
</uploader-list>
</uploader>
</div>
<div class="right">
<h3>任务上传列表</h3>
<div class="right_content">
<el-table :data="taskList" style="width: 100%" stripe>
<el-table-column prop="type" label="类型" width="90" align="center">
<template slot-scope="scope">
<el-tag
v-if="scope.row.form && scope.row.form.mediaType === 'video'"
>
视频
</el-tag>
<el-tag
v-if="
scope.row.form && scope.row.form.mediaType === 'video_adv'
"
>
视频广告
</el-tag>
<el-tag
v-if="scope.row.form && scope.row.form.mediaType === 'image'"
>
漫画
</el-tag>
<el-tag
v-if="scope.row.form && scope.row.form.mediaType === 'text'"
>
小说
</el-tag>
<el-tag
v-if="scope.row.form && scope.row.form.mediaType === 'drama'"
>
短剧
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="name"
label="章节名"
min-width="180"
align="center"
>
<template slot-scope="scope">
{{ (scope.row.form && scope.row.form.name) || "未知" }}
</template>
</el-table-column>
<el-table-column
prop="size"
label="文件大小"
width="90"
align="center"
>
<template slot-scope="scope">
{{
(scope.row.files && getSize(scope.row.files.size)) ||
(scope.row.form.size && getSize(scope.row.form.size)) ||
""
}}
</template>
</el-table-column>
<el-table-column
prop="status"
label="状态"
width="120"
align="center"
>
<template slot-scope="scope">
<el-tag type="info" v-if="scope.row.status == 'wait'"
>等待上传</el-tag
>
<el-tag
type="warning"
v-else-if="scope.row.status == 'video_uploaded'"
>视频上传中</el-tag
>
<el-tag
type="warning"
v-else-if="scope.row.status == 'video_success'"
>视频上传完成</el-tag
>
<el-tag
type="warning"
v-else-if="scope.row.status == 'img_success'"
>图集上传完成</el-tag
>
<el-tag
type="warning"
v-else-if="scope.row.status == 'text_success'"
>小说上传完成</el-tag
>
<el-tag type="success" v-else-if="scope.row.status == 'success'"
>上传成功</el-tag
>
<el-tag type="error" v-else-if="scope.row.status == 'error'"
>上传失败</el-tag
>
<el-tag
type="error"
v-else-if="scope.row.status == 'error_post'"
>提交失败</el-tag
>
</template>
</el-table-column>
<el-table-column
fixed="right"
label="操作"
align="center"
width="100"
>
<template slot-scope="scope">
<el-button
@click="deleteTask(scope.$index)"
type="text"
size="small"
>删除</el-button
>
<el-button
type="text"
v-if="scope.row.status == 'error_post'"
@click="rest(scope.row, scope.$index)"
size="small"
>重试</el-button
>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
<addVideo
v-show="isShow"
:isShow="isShow"
@close="isShow = false"
@addTaskSuccess="addTaskSuccess"
/>
</div>
</template>
<script>
import baseConfig from "@/utils/serverUrlConfig";
import local from "@/utils/localStroage.js";
import addVideo from "./addMedia.vue";
import API from "@/api";
import SparkMD5 from "spark-md5";
export default {
components: {
addVideo,
},
data() {
return {
isShow: false,
trackId: "",
isUploading: false, //是否正在上传
fileStatusText: {
success: "上传成功",
error: "上传失败",
uploading: "正在上传",
paused: "暂停上传",
waiting: "等待上传",
},
attrs: {
accept: [
".mp4",
".qlv",
".qsv",
".ogg",
".flv",
".avi",
".wmv",
".rmvb",
], //接受文件类型
},
taskList: [],
};
},
computed: {
options() {
return {
target: baseConfig.baseServerUrl + "/vid/uploadDotStream", //上传地址
testChunks: false, //是否开启服务器分片校验
method: "application/json",
headers: {
// 额外请求头
Authorization: local.get("userInfo")
? local.get("userInfo").token
: "",
},
fileParameterName: "upload", //文件名参数,默认为file
query: this.uploadQuery, //额外参数
singleFile: false, //单文件上传
chunkSize: 2048000, //分片大小,默认1M
simultaneousUploads: 6,
maxChunkRetries: 9, //失败重试次数
// 把英文时间转成中文
parseTimeRemaining: function(timeRemaining, parsedTimeRemaining) {
return parsedTimeRemaining
.replace(/\syears?/, "年")
.replace(/\days?/, "天")
.replace(/\shours?/, "小时")
.replace(/\sminutes?/, "分钟")
.replace(/\sseconds?/, "秒");
},
};
},
},
methods: {
deleteTask(index) {
this.taskList.splice(index, 1);
},
async rest(row, index) {
let from = row.form;
let res = await API.uploadMedia(from);
if (res.code === 200) {
this.taskList[index].status = "success";
return;
}
this.taskList[index].status = "error_post";
},
// 获取文件大小
getSize(byte) {
let MB = (byte / 1024 / 1024).toFixed(2);
return MB + "M" || "未知";
},
addTaskSuccess(info) {
let repeatFile = this.taskList.filter((file) => {
return info.files && info.files.name == file.filename;
});
if (repeatFile.length) {
this.$message({
type: "error",
message: "视频名重复,添加失败",
});
return;
}
info.status = "wait";
if (info.files) {
info.filename = info.files.name;
this.$refs.uploader.uploader.addFile(info.files);
this.taskList.push(info);
} else if (info.form.text) {
//小说上传完成
info.status = "text_success";
this.taskList.push(info);
this.uploadPost();
} else {
//图集上传完成
info.status = "img_success";
this.taskList.push(info);
this.uploadPost();
}
this.$message.success("任务添加成功");
},
//视频上传选中时
fileAdd(file) {
this.computeMD5(file);
},
//定义视频上传唯一标识符id
generateTaskId() {
const time = new Date().getTime();
this.trackId = time;
},
computeMD5(file) {
let fileReader = new FileReader();
let blobSlice =
File.prototype.slice ||
File.prototype.mozSlice ||
File.prototype.webkitSlice;
let currentChunk = 0;
const chunkSize = 10 * 1024 * 1000;
let chunks = Math.ceil(file.size / chunkSize);
let spark = new SparkMD5.ArrayBuffer();
//校验MD5 文件先暂停
file.pause();
loadNext();
// 实时展示MD5的计算进度
this.$nextTick(() => {
let dom = document.querySelector(
`.file_${file.id} .uploader-file-status span`
);
fileReader.onload = (e) => {
spark.append(e.target.result);
if (currentChunk < chunks) {
currentChunk++;
loadNext();
// 实时展示MD5的计算进度
dom.innerHTML =
"校验MD5 " + ((currentChunk / chunks) * 100).toFixed(0) + "%";
} else {
let md5 = spark.end();
this.computeMD5Success(md5, file);
}
};
});
fileReader.onerror = function() {
this.$message.error(`文件${file.name}读取出错,请检查该文件`);
file.cancel();
};
function loadNext() {
let start = currentChunk * chunkSize;
let end =
start + chunkSize >= file.size ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
}
},
computeMD5Success(md5, file) {
file.fileMd5 = md5;
file.resume();
},
// eslint-disable-next-line no-unused-vars
onFileSuccess(rootFile, file, response, chunk) {
this.$refs.uploader.uploader.removeFile(file);
let res = JSON.parse(response);
let fileName = file.name;
let fIndex = this.taskList.findIndex(
(el) => el.files && el.files.name == fileName
);
if (res.code === 200) {
if (fIndex > -1) {
this.taskList[fIndex].status = "video_success";
this.taskList[fIndex].form.filename = res.data.filename;
this.taskList[fIndex].form.size = res.data.size;
this.taskList[fIndex].form.md5 = res.data.md5;
this.taskList[fIndex].form.mimeType = res.data.mimeType;
this.taskList[fIndex].form.sourceID = res.data.sourceID;
this.taskList[fIndex].form.videoUrl = res.data.sourceURL;
this.taskList[fIndex].form.resolution = res.data.resolution;
this.taskList[fIndex].form.playTime = res.data.playTime;
this.taskList[fIndex].files = null;
}
this.uploadPost();
} else {
if (fIndex > -1) {
this.taskList[fIndex].status = "error";
this.$message.error(`文件${fileName}上传失败`);
}
}
},
//上传帖子
async uploadPost() {
if (this.isUploading) return;
const result = this.taskList.filter(
(item) =>
item.status == "img_success" ||
item.status == "video_success" ||
item.status == "text_success"
);
if (result.length == 0) {
this.$message.success("当前任务执行完毕,等待视频上传完成");
return;
}
let taskItem = result[0];
let form = taskItem.form;
// console.log(form)
try {
this.isUploading = true;
let res = await API.subUploadMedia(form);
if (res.code === 200) {
taskItem.status = "success";
return;
}
taskItem.status = "error_post";
} catch (err) {
taskItem.status = "error_post";
} finally {
this.isUploading = false;
this.uploadPost();
}
},
//视频上传额外参数
uploadQuery(file, chunk) {
return {
pos: chunk.offset,
id: file.fileMd5,
};
},
},
};
</script>
<style lang="scss" scoped>
.movie_warp {
h3 {
padding: 10px 0;
}
.movide_box {
display: flex;
height: 600px;
.left {
flex: 1;
min-width: 500px;
.upload_list {
border: 1px dashed #ccc;
background-color: #f5f5f5;
overflow-y: scroll;
}
}
.right {
margin-left: 20px;
flex: 1;
min-width: 500px;
.right_content {
border: 1px dashed #ccc;
height: 100%;
overflow-y: scroll;
.right_item {
height: 30px;
background: #6aebb4;
}
}
}
}
}
</style>