浏览代码

视频上传预览

cxmo 1 年之前
父节点
当前提交
9a0e6f7b30

+ 9 - 0
src/api/modules/trainingApi.js

@@ -150,4 +150,13 @@ export const VideoInterface = {
     deleteVideo:(params)=>{
         return http.post('/eta_training_video/remove',params)
     },
+    /**
+     * 获取视频详情
+     * @param {Object} params 
+     * @param {Number} params.VideoId
+     * @returns 
+     */
+    getVideoDetail:(params)=>{
+        return http.get('/eta_training_video/detail',params)
+    }
 }

+ 159 - 0
src/views/training_manage/components/addTags.vue

@@ -0,0 +1,159 @@
+<template>
+    <el-dialog
+        title="选择标签"
+        :visible.sync="isModifyDialogShow"
+        :close-on-click-modal="false"
+        :modal-append-to-body="false"
+        @close="$emit('close')"
+        width="589px"
+        v-dialogDrag
+        center
+        class="add-Tags-wrap"
+    >
+        <div class="dialog-container">
+            <el-input  placeholder="请输入标签名称"  prefix-icon="el-icon-search"
+                v-model.trim="searchText" clearable @input="getTagList(searchText)"></el-input>
+            <el-button type="text" @click="getTagList(searchText)">搜索</el-button>
+            <div class="tag-list-box">
+                <el-tag v-for="item in tagList"
+                    :class="['tag-item',{ choosed: choosedTags.findIndex(i=>i.TagId===item.TagId)!==-1 }]"
+                    :key="item.TagId"
+                    type="info"
+                    effect="plain"
+                    @click="chooseTag(item)"
+                >
+                    {{ item.TagName }}
+                </el-tag>
+            </div>
+            <div class="add-tag-box">
+                <el-input  placeholder="请输入标签名称" v-model.trim="addText"></el-input>
+                <el-button type="text" @click="addTag">添加</el-button>
+                <el-button type="text">标签管理</el-button>
+                <p style="color:#999999;font-size: 12px;">注:名称不得超过5个字</p>
+            </div>
+        </div>
+        <div class="foot-container">
+            <el-button @click="$emit('close')">取 消</el-button>
+            <el-button type="primary" @click="modifyTags">确认</el-button>
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+import {TagInterface} from '@/api/modules/trainingApi'
+export default {
+    props:{
+        isModifyDialogShow:{
+            type:Boolean,
+            default:false
+        },
+        Tags:{
+            type:Array,
+            default:[]
+        },
+        getTagList:{
+            type:Function,
+            default:null
+        },
+        tagList:{
+            type:Array,
+            default:[]
+        }
+    },
+    data() {
+        return {
+            searchText:'',
+            addText:'',
+            choosedTags:[],
+        };
+    },
+    watch:{
+        isModifyDialogShow(newVal){
+            if(newVal){
+                this.searchText=''
+                this.addText=''
+                this.choosedTags = _.cloneDeep(this.Tags)
+                this.getTagList()
+            }
+        }
+    },
+    methods: {
+        //选择标签
+        chooseTag(tag){
+            const {TagId} = tag
+            const index = this.choosedTags.findIndex(i=>i.TagId===TagId)
+            if(index!==-1){
+                this.choosedTags.splice(index,1)
+            }else{
+                if(this.choosedTags.length===3){
+                    this.$message.warning("最多选择3个标签")
+                    return
+                }
+                this.choosedTags.push(tag)
+            }
+        },
+        //添加标签
+        addTag(){
+            if(!this.addText){
+                this.$message.warning("请输入标签名称")
+                return
+            }
+            if(this.addText>5){
+                this.$message.warning("标签名称过长,请重新编辑")
+                return
+            }
+            TagInterface.addTag({
+                TagName:this.addText
+            }).then(res=>{
+                if(res.Ret!==200) return 
+                this.getTagList()
+                this.addText = ''
+            })
+        },
+        modifyTags(){
+            this.$emit('modify',this.choosedTags)
+        }
+    },
+};
+</script>
+
+<style scoped lang="scss">
+.add-Tags-wrap{
+    .el-dialog{
+        .dialog-container{
+            .tag-list-box{
+                margin-top:20px;
+                padding:10px;
+                box-sizing: border-box;
+                border: 1px dashed #DCDFE6;
+                border-radius: 4px;
+                max-height: 200px;
+                overflow-y: auto;
+                display: flex;
+                flex-wrap: wrap;
+                gap:10px;
+                .tag-item{
+                    cursor: pointer;
+                    box-sizing: border-box;
+                    text-align: center;
+                    min-width:78px;
+                    /* border:1px solid black; */
+                    border-radius: 2px;
+                    &:hover,&.choosed{
+                        background-color: #EAF3FE;
+                        border-color: #409EFF;
+                        color: #409EFF;
+                    }
+                }
+            }
+            .add-tag-box{
+                margin-top:20px;
+            }
+        }
+        .foot-container{
+            text-align: center;
+            padding:20px 0;
+        }
+    }
+}
+</style>

+ 9 - 2
src/views/training_manage/mixins/videoMixins.js

@@ -8,11 +8,17 @@ export default{
     },
     methods: {
         //获取分类列表
-        getClassifyList(){
+        getClassifyList(type=''){
             ClassifyInterface.getClassifyList({}).then(res=>{
                 if(res.Ret!==200) return 
                 this.classifyList = res.Data&&res.Data.List||[]
                 this.filterNodes(this.classifyList)
+                this.classifyList = this.classifyList.map(item=>{
+                    if(!item.Children){
+                        item.disabled = true
+                    }
+                    return item
+                })
             })
         },
         filterNodes(arr) {
@@ -24,8 +30,9 @@ export default{
             })
         },
         //获取标签列表
-        getTagList(){
+        getTagList(keyword=''){
             TagInterface.getTagList({
+                Keyword:keyword,
                 PageSize:1000,
                 CurrentIndex:1
             }).then(res=>{

+ 301 - 80
src/views/training_manage/modifyVideoPage.vue

@@ -1,44 +1,56 @@
 <template>
     <!-- 新增编辑视频 -->
     <div class="modify-video-page-wrap">
-        <el-form :model="form">
+        <el-form :model="form" :rules="rules" ref="form">
             <el-form-item label="所属分类" prop="ClassifyId">
-                <el-select v-model="form.ClassifyId"></el-select>
+                <el-cascader placeholder="选择所属分类" 
+                    v-model="form.ClassifyId"
+                    :options="classifyList"
+                    clearable
+                    :show-all-levels="false"
+                    :props="{emitPath:false,
+                            label:'ClassifyName',
+                            value:'ClassifyId',
+                            children:'Children'}">
+                </el-cascader>
             </el-form-item>
             <el-form-item label="视频名称" prop="Title">
                 <el-input v-model="form.Title"></el-input>
             </el-form-item>
             <el-form-item label="视频简介" prop="Introduce">
-                <el-input v-model="form.ClassifyId" type="textarea" maxlength="50" show-word-limit :rows="5"></el-input>
+                <el-input v-model="form.Introduce" 
+                placeholder="请输入视频简介"
+                type="textarea" maxlength="50" show-word-limit :rows="5"></el-input>
             </el-form-item>
             <el-form-item label="上传封面" prop="CoverImg">
                 <el-upload action="" accept="image/*" 
-                    :http-request="handleUploadImg" :show-file-list="false">
-                    <el-button type="primary">点击上传</el-button>
+                    :http-request="handleUploadImg" :show-file-list="false" :disabled="isImageUploading">
+                    <el-button type="primary" :loading="isImageUploading">点击上传</el-button>
                     <span style="color:#999999;margin-left: 5px;" @click.stop>建议尺寸比例3:2,支持png、jpg、gif、jpeg格式</span>
                 </el-upload>
                 <div class="img-box">
                     <img :src="form.CoverImg" v-if="form.CoverImg">
-                    <span v-else>请上传封面图</span>
+                    <span v-else style="color:#999999;line-height: 100px;">请上传封面图</span>
                 </div>
             </el-form-item>
             <el-form-item label="上传视频" prop="VideoUrl">
                 <el-upload action="" accept=".mp4" 
-                    :http-request="handleUploadVideo" :show-file-list="false">
-                    <el-button type="primary">点击上传</el-button>
+                    :http-request="handleUploadVideo" :show-file-list="false" :disabled="isVideoUploading">
+                    <el-button type="primary" :loading="isVideoUploading">点击上传</el-button>
                     <span style="color:#999999;margin-left: 5px;" @click.stop>仅支持mp4格式</span>
                 </el-upload>
+                
                 <div class="img-box">
-                    <span v-if="form.VideoUrl">时长</span>
-                    <img :src="form.CoverImg" v-if="form.VideoUrl">
-                    
-                    <span v-else>请上传视频</span>
+                    <el-progress type="circle" :percentage="percentage" width="40" v-if="isVideoUploading"></el-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>
             </el-form-item>
             <el-form-item label="视频标签" prop="TagIds">
-                <el-button type="text">选择标签</el-button>
-                <div class="tag-list">
-                    <span class="tag-item" v-for="tag in TagIds">
+                <el-button type="text" @click="isModifyDialogShow = true">选择标签</el-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="el-icon-close"></i></span>
                     </span>
@@ -46,102 +58,276 @@
             </el-form-item>
         </el-form>
         <div class="btn-box">
-            <el-button type="primary" plain>取消</el-button>
-            <el-button type="primary">保存</el-button>
-            <el-button type="primary">发布</el-button>
+            <el-button type="primary" plain @click="changeRoute">取消</el-button>
+            <el-button type="primary" @click="modifyVideo" :loading="isImageUploading||isVideoUploading">保存</el-button>
+            <el-button type="primary" @click="publishVideo" :loading="isImageUploading||isVideoUploading">保存并发布</el-button>
         </div>
         <!-- 选择标签弹窗 -->
+        <AddTags 
+            :isModifyDialogShow="isModifyDialogShow"
+            :Tags="form.TagIds"
+            :tagList="tagList"
+            :getTagList="getTagList"
+            @modify="modifyTags"
+            @close="isModifyDialogShow=false"
+        />
+        <!-- 预览视频弹窗 -->
+        <el-dialog
+            :visible.sync="previewPop"
+            :modal-append-to-body='false'
+            v-dialogDrag
+            width="60vw"
+            :title="previewPopTitle"
+            @close="endingPreview"
+        >
+         <video style="width: 100%;height: 100%;max-height: 70vh;outline: none;" 
+         controls :src="previewVideoUrl" autoplay ref="previewVideo">
+            您的浏览器暂不支持,请更换浏览器
+         </video>   
+        </el-dialog>
     </div>
 </template>
 
 <script>
+import MD5 from 'js-md5'
+import {getOSSSign} from '@/api/api.js'
 import {bannerupload} from '@/api/api.js'
 import {VideoInterface} from '@/api/modules/trainingApi'
+import AddTags from './components/addTags.vue'
 import mixin from './mixins/videoMixins'
+let ALOSSINS=null //阿里云上传实例
+let ALOSSAbortCheckpoint=null //阿里云上传实例中断点
 export default {
-    mixins:[mixin],
-    props:{
-        VideoList:{
-            type:Array,
-            default:[]
-        }
-    },
+    mixins: [mixin],
     data() {
         return {
-            form:{},
-
-            isImageUploading:false,
-            isVideoUploading:false
+            form: {
+                Title: '',
+                Introduce: '',
+                ClassifyId: '',
+                TagIds: [],
+                CoverImg: '',
+                VideoUrl: ''
+            },
+            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()
+                    }
+                }}]
+            },
+            tagIdKey: 0,
+            isModifyDialogShow: false,
 
+            isImageUploading: false,
 
+            isVideoUploading: false,
+            percentage:0,
+            timeDuration:'',
+            previewVideoUrl:'',
+            previewPop:false,
+            previewPopTitle:''
+            
         };
     },
     methods: {
-        handleUploadImg(file){
-            //检查图片是否合法
-            console.log('hhh',file)
-
-            isImageUploading = true
+        //检查图片是否合法
+        handleUploadImg(file) {
+            this.isImageUploading = true;
+            //图片格式限制
+            const { type } = file.file;
+            if (!['image/png', 'image/jpeg'].includes(type)) {
+                this.$message.warning('仅支持png、jpg格式的图片');
+                this.isImageUploading = false;
+                return;
+            }
+            this.uploadImg(file);
         },
-        uploadImg(file){
+        //上传图片
+        uploadImg(file) {
             let form = new FormData();
-            form.append('file',file.file);
-            bannerupload(form).then(res=>{
-                this.isImageUploading = false
-                if(res.Ret!==200) return 
-                this.form.CoverImg = res.Data.ResourceUrl
+            form.append('file', file.file);
+            bannerupload(form).then(res => {
+                this.isImageUploading = false;
+                if (res.Ret !== 200)
+                    return;
+                this.form.CoverImg = res.Data.ResourceUrl;
+            });
+        },
+        //检查视频是否合法,并获取视频时长
+        async handleUploadVideo(file) {
+            if(file.file.type!='video/mp4'){
+                this.$message.warning('上传失败,上传视频格式不正确');
+                return
+            }
+            const duration=await this.handleGetDuration(file.file);
+            this.timeDuration = `${String(parseInt(duration/60)).padStart(2,'0')}:${String(parseInt(duration%60)).padStart(2,'0')}`;
+            this.uploadVideo(file.file);
+            this.isVideoUploading = true;
+        },
+        //获取视频时长的promise
+        async handleGetDuration(file){
+            return new Promise((resolve,reject)=>{
+                const fileUrl=URL.createObjectURL(file);
+                const audioEl=new Audio(fileUrl);
+                audioEl.addEventListener('loadedmetadata',(e)=>{
+                    const t=e.composedPath()[0].duration;
+                    resolve(t);
+                })
             })
         },
-        handleUploadVideo(file){
-            //检查视频是否合法,并获取视频时长
-            this.isVideoUploading = true
+        //上传视频
+        async uploadVideo(file) {
+            const res = await getOSSSign();
+            if(res.Ret===200){
+                this.handleUploadToOSS(file,res.Data);
+            }
+        },
+        //上传到阿里云
+        async handleUploadToOSS(file,{AccessKeyId,AccessKeySecret,SecurityToken}){
+            ALOSSINS=new OSS({
+                // yourRegion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。
+                region: "oss-cn-shanghai",
+                // 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
+                accessKeyId: AccessKeyId,
+                accessKeySecret: AccessKeySecret,
+                // 从STS服务获取的安全令牌(SecurityToken)。
+                stsToken: SecurityToken,
+                // 填写Bucket名称,例如examplebucket。
+                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;
+                    this.percentage=parseInt(p*100);
+                },
+                // 设置并发上传的分片数量。
+                parallel: 10,
+                // 设置分片大小。默认值为1 MB,最小值为100 KB。
+                partSize: 1024 * 1024 * 10, // 10MB
+            };
+            try {
+                const res=await ALOSSINS.multipartUpload(temName,file,{...options});
+                console.log('上传结果',res);
+                if(res.res.status===200){
+                    this.form.VideoUrl='https://hzstatic.hzinsights.com/'+res.name;
+                    this.percentage=0;
+                    ALOSSAbortCheckpoint=null;
+                    this.isVideoUploading = false;
+                }
+            } catch (error) {
+                console.log('上传到阿里云失败',error);
+                if(error.name!=="cancel"){//不是取消上传的则给错误提示
+                    this.$message.warning('上传失败,请刷新重试');
+                }
+                this.percentage=0;
+                ALOSSAbortCheckpoint=null;
+                this.isVideoUploading = false;
+            }
         },
-        uploadVideo(file,OSSInfo){
-            this.isVideoUploading = false
+        //删除所选标签
+        removeTag(tag) {
+            const index = this.form.TagIds.findIndex(i => i.TagId === tag.TagId);
+            index !== -1 && (this.form.TagIds.splice(index, 1));
+            this.tagIdKey++;
         },
-        removeTag(tag){
-            const index = this.form.TagIds.findIndex(i=>i.TagId===tag.TagId)
-            index!==-1&&(this.form.TagIds.splice(index,1))
+        //改变所选标签
+        modifyTags(tags) {
+            this.form.TagIds = _.cloneDeep(tags);
+            this.isModifyDialogShow = false;
         },
         //获取视频信息
-        getVideoDetail(){
-            const {VideoId} = this.$route.query
-            const videoInfo = _.cloneDeep(this.VideoList.find(i=>i.VideoId==VideoId)||null)
-            if(videoInfo){
-                const {Classify} = videoInfo
-                const classifyArr = this.getDataClassify(Classify)
-                videoInfo.ClassifyId = classifyArr[classifyArr.length-1]
-                delete videoInfo.Classify
-            }
-            this.form = videoInfo||{}
+        getVideoDetail() {
+            const { VideoId } = this.$route.query;
+            if (!VideoId)
+                return;
+            VideoInterface.getVideoDetail({
+                VideoId: Number(VideoId)
+            }).then(res => {
+                if (res.Ret !== 200)
+                    return;
+                this.form = res.Data || {};
+                if (this.form.Classify) {
+                    const { Classify } = this.form;
+                    const classifyArr = this.getDataClassify(Classify);
+                    this.form.ClassifyId = classifyArr[classifyArr.length - 1];
+                    delete this.form.Classify;
+                }
+                if (this.form.Tags) {
+                    this.form.TagIds = this.form.Tags;
+                    delete this.form.Tags;
+                }
+            });
         },
         //获取视频分类路径
-        getDataClassify(classify,classifyArr=[]){
-            classifyArr.push(classify.ClassifyId)
-            if(classify.Children&&classify.Children.length){
-                return this.getDataClassify(classify.Children[0],classifyArr)
+        getDataClassify(classify, classifyArr = []) {
+            classifyArr.push(classify.ClassifyId);
+            if (classify.Children && classify.Children.length) {
+                return this.getDataClassify(classify.Children[0], classifyArr);
             }
-            return classifyArr
+            return classifyArr;
         },
         //添加/编辑视频
-        async modifyVideo(type){
-            let res  = null
-            let params = {...this.form,...{TagIds:this.form.TagIds.map(t=>t.TagId)}}
-            if(type==='add'){
-                res = await VideoInterface.addVideo(params)
-            }else{
-                res = await VideoInterface.editVideo(params)
+        async modifyVideo(type='modify') {
+            await this.$refs.form.validate()
+            let res = null;
+            let params = { ...this.form, ...{ TagIds: this.form.TagIds.map(t => t.TagId) } };
+            if (!this.form.VideoId) {
+                res = await VideoInterface.addVideo(params);
             }
-            if(res.Ret!==200) return 
-            this.$message.success(`${type==='add'?'添加':'编辑'}成功`)
-            this.$router.push('/trainingVideo')
-        }
+            else {
+                res = await VideoInterface.editVideo(params);
+            }
+            if (res.Ret !== 200)
+                return;
+            type!=='publish'&&this.$message.success(`${type === 'add' ? '添加' : '编辑'}成功`);
+            type!=='publish'&&this.changeRoute();
+        },
+        //发布视频
+        async publishVideo(){
+            await this.modifyVideo('publish');
+            this.$message.success("保存成功")
+            this.changeRoute();
+        },
+        changeRoute(){
+            if(ALOSSAbortCheckpoint){
+                console.log('终止上传');
+                ALOSSINS.abortMultipartUpload(ALOSSAbortCheckpoint.name,ALOSSAbortCheckpoint.uploadId)
+            }
+            this.$router.push('/trainingVideo');
+        },
+        // 预览视频
+        handlePreviewVideo(){
+            if(this.isVideoUploading||!this.form.VideoUrl) return
+            this.$refs.previewVideo && this.$refs.previewVideo.play()
+            this.previewPopTitle = this.form.Title||'暂无标题'
+            this.previewVideoUrl = this.form.VideoUrl
+            this.previewPop = true
+        },
+        // 结束预览弹窗关闭回调 -- 暂停视频
+        endingPreview(){
+            this.$refs.previewVideo && this.$refs.previewVideo.pause()
+        },
     },
-    mounted(){
-        this.getClassifyList()
-        this.getTagList()
-        this.getVideoDetail()
-    }
+    mounted() {
+        this.getClassifyList('leaf');
+        //this.getTagList();
+        this.getVideoDetail();
+    },
+    components: { AddTags }
 };
 </script>
 <style lang="scss">
@@ -169,15 +355,50 @@ export default {
             width:500px;
         }
         .img-box{
-            background-color: #D9D9D9;
+            /* 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;
+            }
+            .el-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{