|
@@ -0,0 +1,489 @@
|
|
|
+<template>
|
|
|
+ <!-- 新增编辑视频 -->
|
|
|
+ <div class="modify-video-page-wrap">
|
|
|
+ <t-form :data="form" :rules="rules" ref="formRules">
|
|
|
+ <t-form-item label="所属分类" name="ClassifyId">
|
|
|
+ <t-cascader placeholder="选择所属分类"
|
|
|
+ v-model="form.ClassifyId"
|
|
|
+ :options="classifyList"
|
|
|
+ style="width: 300px;"
|
|
|
+ clearable
|
|
|
+ :keys="{
|
|
|
+ emitPath:false,
|
|
|
+ label:'ClassifyName',
|
|
|
+ value:'ClassifyId',
|
|
|
+ children:'Children'}">
|
|
|
+ </t-cascader>
|
|
|
+ </t-form-item>
|
|
|
+ <t-form-item label="视频名称" name="Title">
|
|
|
+ <t-input style="width: 300px;" v-model="form.Title" placeholder="请输入视频名称"></t-input>
|
|
|
+ </t-form-item>
|
|
|
+ <t-form-item label="视频简介" name="Introduce">
|
|
|
+ <t-textarea v-model="form.Introduce"
|
|
|
+ placeholder="请输入视频简介"
|
|
|
+ maxlength="50" show-word-limit :autosize="{ minRows: 5, maxRows: 5 }"></t-textarea >
|
|
|
+ </t-form-item>
|
|
|
+ <t-form-item label="上传封面" name="CoverImg">
|
|
|
+ <div style="display: block;">
|
|
|
+ <t-upload accept="image/*"
|
|
|
+ :http-request="handleUploadImg" :show-file-list="false" :disabled="isImageUploading">
|
|
|
+ <t-button theme="primary" :loading="isImageUploading">点击上传</t-button>
|
|
|
+ <span style="color:#999999;margin-left: 5px;" @click.stop>建议尺寸比例3:2,支持png、jpg、gif、jpeg格式</span>
|
|
|
+ </t-upload>
|
|
|
+ <div class="img-box">
|
|
|
+ <img :src="form.CoverImg" v-if="form.CoverImg">
|
|
|
+ <span v-else style="color:#999999;line-height: 100px;">请上传封面图</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </t-form-item>
|
|
|
+ <t-form-item label="上传视频" name="VideoUrl">
|
|
|
+ <div style="display: block;">
|
|
|
+ <t-upload accept=".mp4"
|
|
|
+ :request-method="handleUploadVideo" :show-file-list="false" :disabled="isVideoUploading">
|
|
|
+ <t-button theme="primary" :loading="isVideoUploading">点击上传</t-button>
|
|
|
+ <span style="color:#999999;margin-left: 5px;" @click.stop>仅支持mp4格式</span>
|
|
|
+ </t-upload>
|
|
|
+
|
|
|
+ <div class="img-box">
|
|
|
+ <t-progress type="circle" :percentage="percentage" width="40" v-if="isVideoUploading"></t-progress>
|
|
|
+ <span v-if="form.VideoUrl&&!form.VideoId" class="duration">{{timeDuration}}</span>
|
|
|
+ <img :src="form.CoverImg" v-if="form.VideoUrl" @click="handlePreviewVideo">
|
|
|
+ <span v-else style="color:#999999;line-height: 100px;">请上传视频</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ </t-form-item>
|
|
|
+ <t-form-item label="视频标签" name="TagIds">
|
|
|
+ <t-button variant="text" theme="primary" @click="isModifyDialogShow = true">选择标签</t-button>
|
|
|
+ <div class="tag-list" :key="tagIdKey">
|
|
|
+ <span class="tag-item" v-for="tag in form.TagIds" :key="tag.TagId">
|
|
|
+ <span>{{tag.TagName}}</span>
|
|
|
+ <span @click.stop="removeTag(tag)"><i class="t-icon-close"></i></span>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </t-form-item>
|
|
|
+ </t-form>
|
|
|
+ <div class="btn-box">
|
|
|
+ <t-button variant="outline" plain @click="changeRoute">取消</t-button>
|
|
|
+ <t-button theme="primary" @click="modifyVideo" :loading="isImageUploading||isVideoUploading">保存</t-button>
|
|
|
+ <t-button theme="primary" @click="publishVideo" :loading="isImageUploading||isVideoUploading">发布</t-button>
|
|
|
+ </div>
|
|
|
+ <!-- 选择标签弹窗 -->
|
|
|
+ <AddTags
|
|
|
+ :isModifyDialogShow="isModifyDialogShow"
|
|
|
+ :Tags="form.TagIds"
|
|
|
+ :tagList="tagList"
|
|
|
+ :getTagList="getTagList"
|
|
|
+ @modify="modifyTags"
|
|
|
+ @close="isModifyDialogShow=false"
|
|
|
+ />
|
|
|
+ <!-- 预览视频弹窗 -->
|
|
|
+ <t-dialog
|
|
|
+ :visible.sync="previewPop"
|
|
|
+ :modal-append-to-body='false'
|
|
|
+ width="60vw"
|
|
|
+ :title="previewPopTitle"
|
|
|
+ @close="endingPreview"
|
|
|
+ >
|
|
|
+ <video style="width: 100%;height: 100%;max-height: 70vh;outline: none;"
|
|
|
+ controls :src="previewVideoUrl" autoplay ref="previewVideo">
|
|
|
+ 您的浏览器暂不支持,请更换浏览器
|
|
|
+ </video>
|
|
|
+ </t-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, onMounted } from 'vue';
|
|
|
+import MD5 from 'js-md5';
|
|
|
+import { VideoInterface, TagInterface, ClassifyInterface } from '@/api/modules/trainingApi';
|
|
|
+import AddTags from './components/addTags.vue';
|
|
|
+import _ from 'lodash';
|
|
|
+import { useRoute, useRouter } from 'vue-router'
|
|
|
+
|
|
|
+const router=useRouter()
|
|
|
+const route=useRoute()
|
|
|
+const form = ref({
|
|
|
+ Title: '',
|
|
|
+ Introduce: '',
|
|
|
+ ClassifyId: '',
|
|
|
+ TagIds: [],
|
|
|
+ CoverImg: '',
|
|
|
+ VideoUrl: ''
|
|
|
+});
|
|
|
+
|
|
|
+const rules = {
|
|
|
+ ClassifyId: [{ required: true, message: '请选择所属分类' }],
|
|
|
+ Title: [{ required: true, message: '请输入视频名称' }],
|
|
|
+ CoverImg: [{ required: true, message: '请选择视频封面' }],
|
|
|
+ VideoUrl: [{ required: true, message: '请上传视频' }],
|
|
|
+ TagIds: [{
|
|
|
+ required: true,
|
|
|
+ validator: (rule, value, callback) => {
|
|
|
+ if (!value.length) {
|
|
|
+ return callback(new Error("请至少选择一个标签"));
|
|
|
+ } else {
|
|
|
+ return callback();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }]
|
|
|
+}
|
|
|
+
|
|
|
+const tagIdKey = ref(0);
|
|
|
+const isModifyDialogShow = ref(false);
|
|
|
+
|
|
|
+const isImageUploading = ref(false);
|
|
|
+const isVideoUploading = ref(false);
|
|
|
+const percentage = ref(0);
|
|
|
+const timeDuration = ref('');
|
|
|
+const previewVideoUrl = ref('');
|
|
|
+const previewPop = ref(false);
|
|
|
+const previewPopTitle = ref('');
|
|
|
+const tagList = ref([]);
|
|
|
+const classifyList = ref([]);
|
|
|
+const previewVideo = ref(null);
|
|
|
+const formRules = ref(null);
|
|
|
+
|
|
|
+// 定义全局变量
|
|
|
+let ALOSSINS = null;
|
|
|
+let ALOSSAbortCheckpoint = null;
|
|
|
+
|
|
|
+
|
|
|
+// 获取分类列表
|
|
|
+const getClassifyList = async (type = '') => {
|
|
|
+ try {
|
|
|
+ const res = await ClassifyInterface.getClassifyList({});
|
|
|
+ if (res.Ret !== 200) return;
|
|
|
+ classifyList.value = (res.Data && res.Data.List) || [];
|
|
|
+ filterNodes(classifyList.value);
|
|
|
+ classifyList.value = classifyList.value.map(item => {
|
|
|
+ if (!item.Children) {
|
|
|
+ item.disabled = true;
|
|
|
+ }
|
|
|
+ return item;
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取分类列表失败:', error);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 过滤节点
|
|
|
+const filterNodes = (arr) => {
|
|
|
+ if (arr.length) {
|
|
|
+ arr.forEach(item => {
|
|
|
+ if (item.Children && item.Children.length) {
|
|
|
+ filterNodes(item.Children);
|
|
|
+ }
|
|
|
+ if (item.Children && !item.Children.length) {
|
|
|
+ delete item.Children;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 获取标签列表
|
|
|
+const getTagList = async (keyword = '') => {
|
|
|
+ try {
|
|
|
+ const res = await TagInterface.getTagList({
|
|
|
+ Keyword: keyword,
|
|
|
+ PageSize: 1000,
|
|
|
+ CurrentIndex: 1
|
|
|
+ });
|
|
|
+ if (res.Ret !== 200) return;
|
|
|
+ tagList.value = (res.Data && res.Data.List) || [];
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取标签列表失败:', error);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 检查图片是否合法
|
|
|
+const handleUploadImg = (file) => {
|
|
|
+ isImageUploading.value = true;
|
|
|
+ const { type } = file.file;
|
|
|
+ if (!['image/png', 'image/jpeg'].includes(type)) {
|
|
|
+ MessagePlugin.warning('仅支持png、jpg格式的图片');
|
|
|
+ isImageUploading.value = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ uploadImg(file);
|
|
|
+};
|
|
|
+
|
|
|
+// 上传图片
|
|
|
+const uploadImg = (file) => {
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append('file', file.file);
|
|
|
+ VideoInterface.bannerupload(formData).then(res => {
|
|
|
+ isImageUploading.value = false;
|
|
|
+ if (res.Ret !== 200) return;
|
|
|
+ form.value.CoverImg = res.Data.ResourceUrl;
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 检查视频是否合法,并获取视频时长
|
|
|
+const handleUploadVideo = async (file) => {
|
|
|
+ if (file.type !== 'video/mp4') {
|
|
|
+ MessagePlugin.warning('上传失败,上传视频格式不正确');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const duration = await handleGetDuration(file);
|
|
|
+ timeDuration.value = `${String(parseInt(duration / 60)).padStart(2, '0')}:${String(parseInt(duration % 60)).padStart(2, '0')}`;
|
|
|
+ uploadVideo(file);
|
|
|
+ isVideoUploading.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+// 获取视频时长的Promise
|
|
|
+const handleGetDuration = (file) => {
|
|
|
+ console.log(file);
|
|
|
+
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ const fileUrl = URL.createObjectURL(file.raw);
|
|
|
+ const audioEl = new Audio(fileUrl);
|
|
|
+ audioEl.addEventListener('loadedmetadata', (e) => {
|
|
|
+ const t = e.composedPath()[0].duration;
|
|
|
+ resolve(t);
|
|
|
+ });
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 上传视频
|
|
|
+const uploadVideo = async (file) => {
|
|
|
+ const res = await VideoInterface.getOSSSign();
|
|
|
+ if (res.Ret === 200) {
|
|
|
+ handleUploadToOSS(file.raw, res.Data);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 上传到阿里云
|
|
|
+const handleUploadToOSS = async (file, { AccessKeyId, AccessKeySecret, SecurityToken }) => {
|
|
|
+ ALOSSINS = new OSS({
|
|
|
+ region: 'oss-cn-shanghai',
|
|
|
+ accessKeyId: AccessKeyId,
|
|
|
+ accessKeySecret: AccessKeySecret,
|
|
|
+ stsToken: SecurityToken,
|
|
|
+ bucket: 'hzchart',
|
|
|
+ endpoint: 'hzstatic.hzinsights.com',
|
|
|
+ cname: true,
|
|
|
+ timeout: 600000000000
|
|
|
+ });
|
|
|
+
|
|
|
+ const t = new Date().getTime().toString();
|
|
|
+ const temName = `static/yb/video/${MD5(t)}.${file.type.split('/')[1]}`;
|
|
|
+ const options = {
|
|
|
+ progress: (p, cpt, res) => {
|
|
|
+ ALOSSAbortCheckpoint = cpt;
|
|
|
+ percentage.value = parseInt(p * 100);
|
|
|
+ },
|
|
|
+ parallel: 10,
|
|
|
+ partSize: 1024 * 1024 * 10
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ console.log(temName, file, { ...options });
|
|
|
+
|
|
|
+ const res = await ALOSSINS.multipartUpload(temName, file, { ...options });
|
|
|
+ console.log('上传结果', res);
|
|
|
+ if (res.res.status === 200) {
|
|
|
+ form.value.VideoUrl = `https://hzstatic.hzinsights.com/${res.name}`;
|
|
|
+ percentage.value = 0;
|
|
|
+ ALOSSAbortCheckpoint = null;
|
|
|
+ isVideoUploading.value = false;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ if (error.name !== 'cancel') {
|
|
|
+ MessagePlugin.warning('上传失败,请刷新重试');
|
|
|
+ }
|
|
|
+ percentage.value = 0;
|
|
|
+ ALOSSAbortCheckpoint = null;
|
|
|
+ isVideoUploading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 删除所选标签
|
|
|
+const removeTag = (tag) => {
|
|
|
+ const index = form.value.TagIds.findIndex(i => i.TagId === tag.TagId);
|
|
|
+ if (index !== -1) {
|
|
|
+ form.value.TagIds.splice(index, 1);
|
|
|
+ }
|
|
|
+ tagIdKey.value++;
|
|
|
+};
|
|
|
+
|
|
|
+// 改变所选标签
|
|
|
+const modifyTags = (tags) => {
|
|
|
+ form.value.TagIds = _.cloneDeep(tags);
|
|
|
+ isModifyDialogShow.value = false;
|
|
|
+};
|
|
|
+
|
|
|
+// 获取视频信息
|
|
|
+const getVideoDetail = () => {
|
|
|
+ const { VideoId } = route.query;
|
|
|
+ if (!Number(VideoId)) return;
|
|
|
+ VideoInterface.getVideoDetail({ VideoId: Number(VideoId) }).then(res => {
|
|
|
+ if (res.Ret !== 200) return;
|
|
|
+ form.value = res.Data || {};
|
|
|
+ if (form.value.Classify) {
|
|
|
+ const classifyArr = getDataClassify(form.value.Classify);
|
|
|
+ form.value.ClassifyId = classifyArr[classifyArr.length - 1];
|
|
|
+ delete form.value.Classify;
|
|
|
+ }
|
|
|
+ if (form.value.Tags) {
|
|
|
+ form.value.TagIds = form.value.Tags;
|
|
|
+ delete form.value.Tags;
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 获取视频分类路径
|
|
|
+const getDataClassify = (classify, classifyArr = []) => {
|
|
|
+ classifyArr.push(classify.ClassifyId);
|
|
|
+ if (classify.Children && classify.Children.length) {
|
|
|
+ return getDataClassify(classify.Children[0], classifyArr);
|
|
|
+ }
|
|
|
+ return classifyArr;
|
|
|
+};
|
|
|
+
|
|
|
+// 添加/编辑视频
|
|
|
+const modifyVideo = async (type = 'modify') => {
|
|
|
+ const valid = await formRules.value.validate()
|
|
|
+ if (valid !== true) return
|
|
|
+ let res = null;
|
|
|
+ const params = { ...form.value, TagIds: form.value.TagIds.map(t => t.TagId) };
|
|
|
+ if (!form.value.VideoId) {
|
|
|
+ res = await VideoInterface.addVideo(params);
|
|
|
+ } else {
|
|
|
+ res = await VideoInterface.editVideo(params);
|
|
|
+ }
|
|
|
+ if (res.Ret !== 200) return;
|
|
|
+ if (type !== 'publish') {
|
|
|
+ MessagePlugin.success(`${form.value.VideoId ? '编辑' : '添加'}成功`);
|
|
|
+ changeRoute();
|
|
|
+ }
|
|
|
+ if (!form.value.VideoId) {
|
|
|
+ form.value.VideoId = res.Data || '';
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 发布视频
|
|
|
+const publishVideo = async () => {
|
|
|
+ let res = {};
|
|
|
+ await modifyVideo('publish');
|
|
|
+ if (form.value.VideoId) {
|
|
|
+ res = await VideoInterface.publishVideo({ VideoId: Number(form.value.VideoId), PublishState: 1 });
|
|
|
+ if (res.Ret !== 200) return;
|
|
|
+ MessagePlugin.success('发布成功');
|
|
|
+ }
|
|
|
+ changeRoute();
|
|
|
+};
|
|
|
+
|
|
|
+// 改变路由
|
|
|
+const changeRoute = () => {
|
|
|
+ if (ALOSSAbortCheckpoint) {
|
|
|
+ console.log('终止上传');
|
|
|
+ ALOSSINS.abortMultipartUpload(ALOSSAbortCheckpoint.name, ALOSSAbortCheckpoint.uploadId);
|
|
|
+ }
|
|
|
+ router.push('/training/trainingVideo');
|
|
|
+};
|
|
|
+
|
|
|
+// 预览视频
|
|
|
+const handlePreviewVideo = () => {
|
|
|
+ if (isVideoUploading.value || !form.value.VideoUrl) return;
|
|
|
+ previewVideo.play();
|
|
|
+ previewPopTitle.value = form.value.Title || '暂无标题';
|
|
|
+ previewVideoUrl.value = form.value.VideoUrl;
|
|
|
+ previewPop.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+// 结束预览弹窗关闭回调 -- 暂停视频
|
|
|
+const endingPreview = () => {
|
|
|
+ previewVideo.pause();
|
|
|
+};
|
|
|
+
|
|
|
+// 生命周期钩子 - 组件挂载时执行
|
|
|
+onMounted(() => {
|
|
|
+ getClassifyList('leaf');
|
|
|
+ getTagList();
|
|
|
+ getVideoDetail();
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss">
|
|
|
+.modify-video-page-wrap{
|
|
|
+ .t-textarea__inner{
|
|
|
+ resize: none;
|
|
|
+ }
|
|
|
+ .t-form-item{
|
|
|
+ .t-form-item__content{
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items:flex-start;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|
|
|
+<style scoped lang="scss">
|
|
|
+.modify-video-page-wrap{
|
|
|
+ box-sizing: border-box;
|
|
|
+ padding:40px;
|
|
|
+ background-color: #fff;
|
|
|
+ border-radius: 4px;
|
|
|
+ .t-form{
|
|
|
+ .t-input,.t-select,.t-textarea{
|
|
|
+ width:500px;
|
|
|
+ }
|
|
|
+ .img-box{
|
|
|
+ /* background-color: #D9D9D9; */
|
|
|
+ border:1px dashed #d9d9d9;
|
|
|
+ width:150px;
|
|
|
+ height:100px;
|
|
|
+ text-align: center;
|
|
|
+ margin-top: 10px;
|
|
|
+ position: relative;
|
|
|
+ line-height: normal;
|
|
|
+ img{
|
|
|
+ width:100%;
|
|
|
+ height:100%;
|
|
|
+ }
|
|
|
+ .duration{
|
|
|
+ position:absolute;
|
|
|
+ right:0;
|
|
|
+ bottom:0;
|
|
|
+ background-color: black;
|
|
|
+ color:#fff;
|
|
|
+ padding:4px;
|
|
|
+ }
|
|
|
+ .t-progress{
|
|
|
+ position: absolute;
|
|
|
+ left:50%;
|
|
|
+ top:50%;
|
|
|
+ transform: translate(-50%,-50%);
|
|
|
+ background-color: white;
|
|
|
+ border-radius: 50%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .tag-list{
|
|
|
+ display: flex;
|
|
|
+ gap:10px;
|
|
|
+ line-height:0;
|
|
|
+ .tag-item{
|
|
|
+ cursor: pointer;
|
|
|
+ text-align: center;
|
|
|
+ box-sizing: border-box;
|
|
|
+ padding:12px;
|
|
|
+ min-width:78px;
|
|
|
+ color: #409EFF;
|
|
|
+ background-color: #EAF3FE;
|
|
|
+ border:1px solid #409EFF;
|
|
|
+ border-radius: 2px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .btn-box{
|
|
|
+ margin-top: 20px;
|
|
|
+ text-align: center;
|
|
|
+ .t-button{
|
|
|
+ margin-right: 50px;
|
|
|
+ width:120px;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|