Răsfoiți Sursa

Merge branch 'ETA1.0.8'

cxmo 1 an în urmă
părinte
comite
ccc2cd5394

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

@@ -0,0 +1,162 @@
+import http from "@/api/http.js"
+
+/* 培训管理模块 */
+
+
+//标签管理
+export const TagInterface = {
+    /**
+     * 获取标签列表
+     * @param {Object} params 
+     * @param {Number} params.PageSize 
+     * @param {Number} params.CurrentIndex
+     * @param {String} params.Keyword
+     * @returns 
+     */
+    getTagList:(params)=>{
+        return http.get('/eta_training_video/tag/page_list',params)
+    },
+    /**
+     * 新增标签
+     * @param {Object} params 
+     * @param {String} params.TagName
+     * @returns 
+     */
+    addTag:(params)=>{
+        return http.post('/eta_training_video/tag/add',params)
+    },
+    /**
+     * 编辑标签
+     * @param {Object} params 
+     * @param {Number} params.TagId
+     * @param {String} params.TagName
+     * @returns 
+     */
+    editTag:(params)=>{
+        return http.post('/eta_training_video/tag/edit',params)
+    },
+    /**
+     * 删除标签
+     * @param {Object} params 
+     * @param {Number} params.TagId
+     * @returns 
+     */
+    deleteTag:(params)=>{
+        return http.post('/eta_training_video/tag/remove',params)
+    }
+}
+
+//分类管理
+export const ClassifyInterface = {
+    /**
+     * 获取分类列表
+     * @param {Object} params 
+     * @param {Number} params.Keyword 
+     * @returns 
+     */
+     getClassifyList:(params)=>{
+        return http.get('/eta_training_video/classify/tree',params)
+    },
+    /**
+     * 新增分类
+     * @param {Object} params 
+     * @param {Number} params.ParentId
+     * @param {String} params.ClassifyName
+     * @returns 
+     */
+    addClassify:(params)=>{
+        return http.post('/eta_training_video/classify/add',params)
+    },
+    /**
+     * 编辑分类
+     * @param {Object} params 
+     * @param {Number} params.ParentId
+     * @param {Number} params.ClassifyId
+     * @param {String} params.ClassifyName
+     * @returns 
+     */
+    editClassify:(params)=>{
+        return http.post('/eta_training_video/classify/edit',params)
+    },
+    /**
+     * 删除分类
+     * @param {Object} params 
+     * @param {Number} params.ClassifyId
+     * @returns 
+     */
+    deleteClassify:(params)=>{
+        return http.post('/eta_training_video/classify/remove',params)
+    }
+}
+
+//视频管理
+export const VideoInterface = {
+    /**
+     * 获取视频列表
+     * @param {Object} params 
+     * @param {Number} params.PageSize
+     * @param {Number} params.CurrentIndex 
+     * @param {String} params.Keyword
+     * @param {String} params.StartTime
+     * @param {String} params.EndTime
+     * @param {Number} params.ClassifyId
+     * @param {String} params.TagIds 标签IDs, 英文逗号拼接
+     * @param {Number} params.PublishState 发布状态:1-未发布;2-已发布
+     * @returns 
+     */
+    getVideoList:(params)=>{
+        return http.get('/eta_training_video/page_list',params)
+    },
+    /**
+     * 新增视频
+     * @param {Object} params 
+     * @param {String} params.Title
+     * @param {String} params.Introduce
+     * @param {String} params.CoverImg
+     * @param {String} params.VideoUrl
+     * @param {Number} params.ClassifyId
+     * @param {Array} params.TagIds
+     * @returns 
+     */
+    addVideo:(params)=>{
+        return http.post('/eta_training_video/add',params)
+    },
+    /**
+     * 编辑视频
+     * @param {Object} params 
+     * @param {Number} params.VideoId
+     * 其他同上
+     * @returns 
+     */
+    editVideo:(params)=>{
+        return http.post('/eta_training_video/edit',params)
+    },
+    /**
+     * 发布/取消发布视频
+     * @param {Object} params
+     * @param {Number} params.VideoId
+     * @param {Number} params.PublishState 发布状态:0-取消发布;1-发布
+     * @returns 
+     */
+    publishVideo:(params)=>{
+        return http.post('/eta_training_video/publish',params)
+    },
+    /**
+     * 删除视频
+     * @param {Object} params
+     * @param {Number} params.VideoId
+     * @returns 
+     */
+    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)
+    }
+}

BIN
src/assets/img/home/training_icon.png


+ 33 - 0
src/routes/modules/trainingRoutes.js

@@ -0,0 +1,33 @@
+//培训管理路由模块
+const home = r => require.ensure([], () => r(require('@/views/Home.vue')), 'Home'); //主页
+export default[
+    {
+        path:'/',
+        component:home,
+        name:'培训管理',
+        hidden:false,
+        icon_path:require('@/assets/img/home/training_icon.png'),
+        children:[
+            {
+                path: "trainingVideo",
+                name: "视频管理",
+                component: () => import('@/views/training_manage/videoManage.vue')
+            },
+            {
+                path: "trainingLabel",
+                name: "标签管理",
+                component: () => import('@/views/training_manage/labelManage.vue')
+            },
+            {
+                path: "trainingClassify",
+                name: "分类管理",
+                component: () => import('@/views/training_manage/classifyManage.vue')
+            },
+            {
+                path:'modifyVideo',
+                name:'编辑视频',
+                component:()=> import('@/views/training_manage/modifyVideoPage.vue')
+            }
+        ]
+    }
+]

+ 173 - 0
src/views/training_manage/classifyManage.vue

@@ -0,0 +1,173 @@
+<template>
+    <div class="classify-manage-wrap traing-manage">
+        <div class="top-wrap">
+            <el-button type="primary" @click="handleModifyClassify({})">添加分类</el-button>
+            <el-input v-model="searchText" clearable prefix-icon="el-icon-search" placeholder="请输入分类名称"
+                @input="getTableData" style="width:240px;"></el-input>
+        </div>
+        <div class="table-wrap">
+            <el-table :data="tableData" v-loading="tableLoading" row-key="ClassifyId"
+                :tree-props="{children:'Children',hasChildren:'hasChildren'}">
+                <el-table-column prop="ClassifyName" label="一级分类" align="center">
+                    <template slot-scope="{row}">
+                        <span>{{row.ParentId?'':row.ClassifyName}}</span>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="ClassifyName" label="二级分类" align="center">
+                    <template slot-scope="{row}">
+                        <span>{{row.ParentId?row.ClassifyName:''}}</span>
+                    </template>
+                </el-table-column>
+                <el-table-column label="操作" align="center" width="400px">
+                    <template slot-scope="{row}">
+                        <el-button type="text" @click="handleModifyClassify(row)">编辑</el-button>
+                        <el-button type="text" @click="deleteClassify(row)">删除</el-button>
+                    </template>
+                </el-table-column>
+            </el-table>
+        </div>
+        <!-- 添加分类弹窗 -->
+        <el-dialog :title="currentClassify.ClassifyId?'编辑分类':'添加分类'" :visible.sync="isModifyDialogShow"
+            :close-on-click-modal="false" :modal-append-to-body="false" @close="isModifyDialogShow=false" width="589px"
+            v-dialogDrag center>
+            <div class="dialog-container">
+                <el-form :model="currentClassify" :rules="rules" ref="form" label-position="top">
+                    <el-form-item label="上级分类" prop="ParentId">
+                        <el-select v-model="currentClassify.ParentId" placeholder="请选择上级分类" style="width:100%;">
+                            <el-option v-for="item in currentClassify.ClassifyId
+                                ?(currentClassify.ParentId<=0?defaultList:optionList)
+                                :[...defaultList,...optionList]" 
+                            :key="item.ClassifyId"
+                            :label="item.ClassifyName"
+                            :value="item.ClassifyId"/>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item label="分类名称" prop="ClassifyName">
+                        <el-input v-model="currentClassify.ClassifyName" placeholder="请输入分类名称" style="width:100%;"></el-input>
+                    </el-form-item>
+                </el-form>
+            </div>
+            <div class="foot-container">
+                <el-button @click="isModifyDialogShow=false">取 消</el-button>
+                <el-button type="primary" @click="modifyClassify">确认</el-button>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import {ClassifyInterface} from '@/api/modules/trainingApi'
+export default {
+    data() {
+        return {
+            searchText: '',
+            tableData: [],
+            tableLoading: false,
+            currentClassify: {},
+            defaultList:[{ClassifyId:-1,ClassifyName:'无'}],
+            isModifyDialogShow: false,
+            rules:{
+                ParentId:[{required:true,message:'请选择上级分类'}],
+                ClassifyName:[{required:true,message:'请输入分类名称'}]
+            }
+        };
+    },
+    watch:{
+        isModifyDialogShow(newVal){
+            if(newVal){
+                this.$nextTick(()=>{
+                    this.$refs.form&&this.$refs.form.clearValidate();
+                })
+            }
+        }
+    },
+    computed:{
+        optionList(){
+            const list = this.tableData.map(i=>{
+                return {
+                    ClassifyId:i.ClassifyId,
+                    ClassifyName:i.ClassifyName
+                }
+            })
+            return list
+        },
+    },
+    methods: {
+        handleModifyClassify(data) {
+            this.currentClassify = _.cloneDeep(data)
+            if(data.ParentId===0){
+                this.currentClassify.ParentId = -1
+            }
+            this.isModifyDialogShow = true
+        },
+        async modifyClassify() {
+            //params对ParentId为-1的数据做处理,转为0
+            await this.$refs.form.validate()
+            let res = null
+            if(this.currentClassify.ClassifyId){
+                res = await ClassifyInterface.editClassify({
+                    ClassifyId:this.currentClassify.ClassifyId,
+                    ParentId:this.currentClassify.ParentId===-1?0:this.currentClassify.ParentId,
+                    ClassifyName:this.currentClassify.ClassifyName
+                })
+            }else{
+                res = await ClassifyInterface.addClassify({
+                    ParentId:this.currentClassify.ParentId===-1?0:this.currentClassify.ParentId,
+                    ClassifyName:this.currentClassify.ClassifyName
+                })
+            }
+            if(res.Ret!==200) return 
+            this.$message.success(`${this.currentClassify.ClassifyId?'编辑':'添加'}成功`)
+            this.getTableData()
+            this.isModifyDialogShow = false
+        },
+        deleteClassify(data) {
+            if(data.Children&&data.Children.length){
+                this.$confirm(
+                    '该分类下已关联内容,不可删除!',
+                    '提示',
+                    {
+                        confirmButtonText: '知道了',
+                        showCancelButton:false,
+                        type: 'error',
+                    }
+                ).then(()=>{})
+            }else{
+                this.$confirm(
+                    '删除后不可恢复,是否确认删除?',
+                    '提示',
+                    {
+                        confirmButtonText: '确定',
+                        cancelButtonText: '取消',
+                        type: 'warning',
+                    }
+                ).then(()=>{
+                    ClassifyInterface.deleteClassify({
+                        ClassifyId:data.ClassifyId
+                    }).then(res=>{
+                        if(res.Ret!==200) return 
+                        this.$message.success('删除成功')
+                        this.getTableData()
+                    })
+                    
+                })
+            }
+        },
+        getTableData(){
+            ClassifyInterface.getClassifyList({
+                Keyword:this.searchText
+            }).then(res=>{
+                if(res.Ret!==200) return
+                this.tableData = res.Data&&res.Data.List||[]
+            })
+        },
+    },
+    mounted(){
+        this.getTableData()
+    }
+};
+</script>
+
+<style scoped lang="scss">
+@import "./css/manage.scss";
+</style>

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

@@ -0,0 +1,162 @@
+<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" @click="toTagPage">标签管理</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)
+        },
+        toTagPage(){
+            window.open('/trainingLabel')
+        }
+    },
+};
+</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>

+ 42 - 0
src/views/training_manage/config/tableColumn.js

@@ -0,0 +1,42 @@
+export const labelTableColumn = [
+    {
+        label:'标签名称',
+        key:'TagName',
+        minWidth:'200px',
+    },{
+        label:'视频数',
+        key:'VideoTotal',
+    },{
+        label:'创建时间',
+        key:'CreateTime',
+        minWidth:'200px',
+    }
+]
+
+
+export const videoTableColumn = [
+    {
+        label:'视频封面',
+        key:'CoverImg',
+        Width:'300px',
+    },{
+        label:'视频名称',
+        key:'Title',
+        minWidth:'200px'
+    },{
+        label:'分类',
+        key:'Classify',
+        Width:'200px',
+    },{
+        label:'标签',
+        key:'Tags',
+        minWidth:'200px',
+    },{
+        label:'状态',
+        key:'PublishState',
+    },{
+        label:'创建时间',
+        key:'CreateTime',
+        Width:'200px',
+    }
+]

+ 30 - 0
src/views/training_manage/css/manage.scss

@@ -0,0 +1,30 @@
+.traing-manage{
+    .top-wrap,.table-wrap{
+        padding:20px;
+        background-color:#fff;
+        border-radius: 4px;
+    }
+    .top-wrap{
+        display: flex;
+        justify-content: space-between;
+    }
+    .table-wrap{
+        margin-top: 20px;
+    }
+    .el-dialog{
+        .dialog-container{
+          padding-bottom: 20px;
+          .input-item{
+            margin-bottom:10px;
+          }
+          .form-hint{
+            color:#999999;
+            font-size: 12px;
+          }
+        }
+        .foot-container{
+            text-align: center;
+            padding-bottom: 20px;
+        }
+      }
+}

+ 168 - 0
src/views/training_manage/labelManage.vue

@@ -0,0 +1,168 @@
+<template>
+    <!-- 培训管理-标签管理 -->
+    <div class="label-manage-wrap traing-manage">
+        <div class="top-wrap">
+            <el-button type="primary" @click="handleModifyLabel({})">新增标签</el-button>
+            <el-input v-model="searchText" clearable 
+                prefix-icon="el-icon-search" placeholder="请输入标签名称" @input="handleCurrentChange(1)" 
+                style="width:240px;"></el-input>
+        </div>
+        <div class="table-wrap">
+            <el-table :data="tableData" border v-loading="tableLoading">
+                <el-table-column v-for="column in tableColumn" :key="column.key" 
+                    :label="column.label" align="center"
+                    :prop="column.key"
+                    :min-width="column.minWidth">
+                </el-table-column>
+                <el-table-column label="操作" align="center">
+                    <template slot-scope="{row}">
+                        <el-button type="text" @click="handleModifyLabel(row)">编辑</el-button>
+                        <el-button type="text" @click="deleteLabel(row)" style="color:red;">删除</el-button>
+                    </template>
+                </el-table-column>
+            </el-table>
+            <el-pagination
+                layout="total,prev,pager,next,jumper" 
+                background
+                :current-page="currentPage"
+                @current-change="handleCurrentChange"
+                :page-size="pageSize" 
+                :total="total"
+                style="text-align:right;margin-top:30px;">
+            </el-pagination>
+        </div>
+        <!-- 添加标签弹窗 -->
+        <el-dialog
+            :title="currentLabel.TagId?'编辑标签':'添加标签'"
+            :visible.sync="isModifyDialogShow"
+            :close-on-click-modal="false"
+            :modal-append-to-body="false"
+            @close="isModifyDialogShow=false"
+            width="589px"
+            v-dialogDrag
+            center
+            >
+            <div class="dialog-container">
+                <div class="input-item">
+                <el-input  placeholder="请输入标签名称" v-model.trim="currentLabel.TagName" required ></el-input>
+                </div>
+                <p class="form-hint">注:名称不得超过5个字</p>
+            </div>
+            <div class="foot-container">
+                <el-button @click="isModifyDialogShow=false">取 消</el-button>
+                <el-button type="primary" @click="modifyLabel">确认</el-button>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import {labelTableColumn} from './config/tableColumn'
+import {TagInterface} from '@/api/modules/trainingApi'
+export default {
+    data() {
+        return {
+            /* table */
+            tableLoading:false,
+            tableData:[],
+            tableColumn:labelTableColumn,
+            searchText:'',
+            /* table-page */
+            currentPage:1,
+            pageSize:10,
+            total:0,
+            /* modify label */
+            currentLabel:{},
+            isModifyDialogShow:false,
+        };
+    },
+    methods: {
+        handleModifyLabel(data){
+            this.currentLabel = _.cloneDeep(data)
+            this.isModifyDialogShow = true
+        },
+        async modifyLabel(){
+            if(!this.currentLabel.TagName){
+                this.$message.warning("请输入标签名称")
+                return
+            }
+            if(this.currentLabel.TagName.length>5){
+                this.$message.warning("标签名称过长,请重新编辑")
+                return
+            }
+            let res = null
+            if(this.currentLabel.TagId){
+                //edit
+                res = await TagInterface.editTag({
+                    TagId:this.currentLabel.TagId,
+                    TagName:this.currentLabel.TagName
+                })
+            }else{
+                //add
+                res = await TagInterface.addTag({
+                    TagName:this.currentLabel.TagName
+                })
+            }
+            if(res.Ret!==200) return 
+            //添加/编辑成功
+            this.$message.success(`${this.currentLabel.TagId?'编辑':'添加'}成功`)
+            this.currentPage = 1
+            this.getTableData()
+            this.isModifyDialogShow = false
+        },
+        deleteLabel(label){
+            if(label.VideoTotal!==0){
+                this.$confirm('该标签已关联视频,删除失败','提示',{confirmButtonText:'知道了',showCancelButton:false,type:'error'})
+                return
+            }
+            this.$confirm(
+                '删除后不可恢复,是否确认删除该标签?',
+                '提示',
+                {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'warning',
+                }
+            ).then(()=>{
+                TagInterface.deleteTag({
+                    TagId:label.TagId
+                }).then(res=>{
+                    if(res.Ret!==200) return 
+                    this.$message.success('删除成功')
+                    this.currentPage=1
+                    this.getTableData()
+                })
+            }).catch(()=>{}).finally(()=>{})
+        },
+        getTableData(){
+            this.tableLoading = true
+            TagInterface.getTagList({
+                PageSize:this.pageSize,
+                CurrentIndex:this.currentPage,
+                Keyword:this.searchText
+            }).then(res=>{
+                this.tableLoading = false
+                if(res.Ret!==200) return
+                if(!res.Data){
+                    this.tableData = []
+                    this.total = 0
+                    return
+                } 
+                this.tableData = res.Data.List||[]
+                this.total = res.Data.Paging.Totals
+            })
+        },
+        handleCurrentChange(page){
+            this.currentPage = page
+            this.getTableData()
+        }
+    },
+    mounted(){
+        this.getTableData()
+    }
+};
+</script>
+
+<style scoped lang="scss">
+@import "./css/manage.scss";
+</style>

+ 44 - 0
src/views/training_manage/mixins/videoMixins.js

@@ -0,0 +1,44 @@
+import {TagInterface,ClassifyInterface} from '@/api/modules/trainingApi'
+export default{
+    data() {
+        return {
+            tagList:[],
+            classifyList:[],
+        }
+    },
+    methods: {
+        //获取分类列表
+        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) {
+            arr.length && arr.forEach(item => {
+                item.Children && item.Children.length && this.filterNodes(item.Children)
+                if(item.Children && !item.Children.length) {
+                    delete item.Children
+                }
+            })
+        },
+        //获取标签列表
+        getTagList(keyword=''){
+            TagInterface.getTagList({
+                Keyword:keyword,
+                PageSize:1000,
+                CurrentIndex:1
+            }).then(res=>{
+                if(res.Ret!==200) return 
+                this.tagList = res.Data&&res.Data.List||[]
+            })
+        }
+    }
+}

+ 419 - 0
src/views/training_manage/modifyVideoPage.vue

@@ -0,0 +1,419 @@
+<template>
+    <!-- 新增编辑视频 -->
+    <div class="modify-video-page-wrap">
+        <el-form :model="form" :rules="rules" ref="form">
+            <el-form-item label="所属分类" prop="ClassifyId">
+                <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" placeholder="请输入视频名称"></el-input>
+            </el-form-item>
+            <el-form-item label="视频简介" prop="Introduce">
+                <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" :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 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" :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">
+                    <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" @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>
+                </div>
+            </el-form-item>
+        </el-form>
+        <div class="btn-box">
+            <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],
+    data() {
+        return {
+            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) {
+            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) {
+            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;
+            });
+        },
+        //检查视频是否合法,并获取视频时长
+        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);
+                })
+            })
+        },
+        //上传视频
+        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;
+            }
+        },
+        //删除所选标签
+        removeTag(tag) {
+            const index = this.form.TagIds.findIndex(i => i.TagId === tag.TagId);
+            index !== -1 && (this.form.TagIds.splice(index, 1));
+            this.tagIdKey++;
+        },
+        //改变所选标签
+        modifyTags(tags) {
+            this.form.TagIds = _.cloneDeep(tags);
+            this.isModifyDialogShow = false;
+        },
+        //获取视频信息
+        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);
+            }
+            return classifyArr;
+        },
+        //添加/编辑视频
+        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);
+            }
+            else {
+                res = await VideoInterface.editVideo(params);
+            }
+            if (res.Ret !== 200)
+                return;
+            type!=='publish'&&this.$message.success(`${this.form.VideoId ? '编辑' : '添加'}成功`);
+            type!=='publish'&&this.changeRoute();
+            !this.form.VideoId&&(this.form.VideoId = res.Data?res.Data:'');
+        },
+        //发布视频
+        async publishVideo(){
+            let res = {}
+            await this.modifyVideo('publish');
+            if(this.form.VideoId){
+                res = await VideoInterface.publishVideo({VideoId:Number(this.form.VideoId),PublishState:1})
+                if(res.Ret!==200) return;
+                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('leaf');
+        //this.getTagList();
+        this.getVideoDetail();
+    },
+    components: { AddTags }
+};
+</script>
+<style lang="scss">
+.modify-video-page-wrap{
+    .el-textarea__inner{
+        resize: none;
+    }
+    .el-form-item{
+            .el-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;
+    .el-form{
+        .el-input,.el-select,.el-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;
+            }
+            .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{
+        text-align: center;
+        .el-button{
+            margin-right: 50px;
+            width:120px;
+            text-align: center;
+        }
+    }
+}
+</style>

+ 265 - 0
src/views/training_manage/videoManage.vue

@@ -0,0 +1,265 @@
+<template>
+    <div class="video-manage-wrap traing-manage">
+        <div class="top-wrap">
+            <div class="select-box">
+                <el-date-picker
+                    v-model="datePick"
+                    type="daterange"
+                    value-format="yyyy-MM-dd"
+                    range-separator="至"
+                    start-placeholder="开始日期"
+                    end-placeholder="结束日期"
+                    clearable
+                    @change="handleCurrentChange(1)">
+                </el-date-picker>
+                <el-cascader placeholder="选择分类" 
+                    v-model="ClassifyId"
+                    :options="classifyList"
+                    clearable
+                    :show-all-levels="false"
+                    :props="{emitPath:false,
+                            checkStrictly:true,
+                            label:'ClassifyName',
+                            value:'ClassifyId',
+                            children:'Children'}"
+                    @change="handleCurrentChange(1)">
+                </el-cascader>
+                <el-select placeholder="选择标签" 
+                    v-model="TagIds" multiple clearable
+                    @change="handleCurrentChange(1)">
+                    <el-option
+                        v-for="item in tagList"
+                        :key="item.TagId"
+                        :label="item.TagName"
+                        :value="item.TagId">
+                    </el-option>
+                </el-select>
+                <el-select placeholder="选择状态" 
+                    v-model="PublishState" clearable
+                    @change="handleCurrentChange(1)">
+                    <el-option label="未发布" :value="1"/>
+                    <el-option label="已发布" :value="2"/>
+                </el-select>
+            </div>
+            <el-input v-model="searchText" clearable 
+                prefix-icon="el-icon-search" placeholder="请输入视频名称" @input="handleCurrentChange(1)" 
+                style="width:240px;"></el-input>
+        </div>
+        <div class="table-wrap">
+            <el-button type="primary" @click="handleModifyVideo(0)">新增视频</el-button>
+            <el-table border :data="tableData">
+                <el-table-column v-for="column in tableColumn" :key="column.key" 
+                    :label="column.label" align="center"
+                    :prop="column.key"
+                    :width="column.Width"
+                    :min-width="column.minWidth">
+                    <template slot-scope="{row}">
+                        <!-- 视频封面 -->
+                        <div class="img-box" v-if="column.key==='CoverImg'">
+                            <img :src="row.CoverImg" @click="handlePreviewVideo(row)">
+                        </div>
+                        <!-- 分类 -->
+                        <span v-else-if="column.key==='Classify'">
+                            {{getDataClassify(row.Classify).join('/')}}
+                        </span>
+                        <!-- 标签 -->
+                        <div class="label-box" v-else-if="column.key==='Tags'">
+                            <span class="label-item" v-for="tag in row.Tags" :key="tag.TagId">{{tag.TagName}}</span>
+                        </div>
+                        <!-- 发布状态 -->
+                        <span v-else-if="column.key==='PublishState'">{{row.PublishState===0?'未发布':'已发布'}}</span>
+                        <span v-else>{{row[column.key]}}</span>
+                    </template>
+                </el-table-column>
+                <el-table-column label="操作" align="center" width="150">
+                    <template slot-scope="{row}">
+                        <el-button type="text" @click="handleModifyVideo(row.VideoId)" v-show="row.PublishState===0">编辑</el-button>
+                        <el-button type="text" @click="publishVideo(row)">{{row.PublishState===0?'发布':'取消发布'}}</el-button>
+                        <el-button type="text" @click="deleteVideo(row)" style="color:red;">删除</el-button>
+                    </template>
+                </el-table-column>
+            </el-table>
+            <el-pagination
+                layout="total,prev,pager,next,jumper" 
+                background
+                :current-page="currentPage"
+                @current-change="handleCurrentChange"
+                :page-size="pageSize" 
+                :total="total"
+                style="text-align:right;margin-top:30px;">
+            </el-pagination>
+        </div>
+        <!-- 预览视频弹窗 -->
+        <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 {videoTableColumn} from './config/tableColumn'
+import {VideoInterface} from '@/api/modules/trainingApi'
+import mixin from './mixins/videoMixins'
+export default {
+    mixins:[mixin],
+    data() {
+        return {
+            /* search options */
+            searchText:'',
+            datePick:[],
+            ClassifyId:'',
+            TagIds:[],
+            PublishState:'',
+
+            tableData: [],
+            tableColumn:videoTableColumn,
+            tableLoading: false,
+
+            currentPage: 1,
+            pageSize: 10,
+            total: 0,
+
+            previewPop:false,
+            previewVideoUrl:'',
+            previewPopTitle:''
+
+
+        };
+    },
+    methods: {
+        //获取视频分类路径
+        getDataClassify(classify,classifyArr=[]){
+            classifyArr.push(classify.ClassifyName)
+            if(classify.Children&&classify.Children.length){
+                return this.getDataClassify(classify.Children[0],classifyArr)
+            }
+            return classifyArr
+        },
+        //获取视频列表
+        getTableData(){
+            VideoInterface.getVideoList({
+                PageSize:this.pageSize,
+                CurrentIndex:this.currentPage,
+                Keyword:this.searchText,
+                StartTime:this.datePick[0],
+                EndTime:this.datePick[1],
+                ClassifyId:Number(this.ClassifyId),
+                TagIds:this.TagIds.join(','),
+                PublishState:Number(this.PublishState)
+            }).then(res=>{
+                if(res.Ret!==200) return 
+                if(!res.Data){
+                    this.tableData = []
+                    this.total = 0
+                    return
+                } 
+                this.tableData = res.Data.List||[]
+                this.total = res.Data.Paging.Totals
+            })
+        },
+        handleModifyVideo(VideoId){
+            this.$router.push({path:'/modifyVideo',query:{VideoId}})
+        },
+        deleteVideo(data){
+            const hint = data.PublishState===0?'删除后不可恢复,是否确认删除?':'该视频已发布,删除后取消发布并删除,是否确认删除?'
+            this.$confirm(
+                    hint,'提示',
+                    {
+                        confirmButtonText: '确定',
+                        cancelButtonText: '取消',
+                        type: 'warning',  
+                    }
+            ).then(()=>{
+                VideoInterface.deleteVideo({
+                    VideoId:data.VideoId,
+                }).then(res=>{
+                    if(res.Ret!==200) return 
+                    this.$message.success("删除成功")
+                    this.getTableData()
+                })
+            })
+        },
+        publishVideo(data){
+            const hint = data.PublishState===0?'是否确认发布':'是否取消发布'
+            this.$confirm(
+                    hint,'提示',
+                    {
+                        confirmButtonText: '确定',
+                        cancelButtonText: '取消',
+                        type: 'warning',  
+                    }
+            ).then(()=>{
+                VideoInterface.publishVideo({
+                    VideoId:data.VideoId,
+                    PublishState:data.PublishState===0?1:0
+                }).then(res=>{
+                    if(res.Ret!==200) return 
+                    this.$message.success(`${data.PublishState?'取消发布':'发布'}成功`)
+                    this.getTableData()
+                })
+            })
+        },
+        handleCurrentChange(page){
+            this.currentPage = page
+            this.getTableData()
+        },
+        handlePreviewVideo(data){
+            if(!data.VideoUrl) return
+            this.$refs.previewVideo && this.$refs.previewVideo.play()
+            this.previewPopTitle = data.Title||'暂无标题'
+            this.previewVideoUrl = data.VideoUrl
+            this.previewPop = true
+        },
+        // 结束预览弹窗关闭回调 -- 暂停视频
+        endingPreview(){
+            this.$refs.previewVideo && this.$refs.previewVideo.pause()
+        },
+    },
+    mounted(){
+        this.getClassifyList()
+        this.getTagList()
+        this.getTableData()
+    }
+};
+</script>
+
+<style scoped lang="scss">
+@import "./css/manage.scss";
+.video-manage-wrap{
+    .table-wrap{
+        .el-table{
+            margin-top:20px;
+            .img-box{
+                text-align: center;
+                img{
+                    width:150px;
+                    height:100px;
+                }
+            }
+            .label-box{
+                display: flex;
+                justify-content: center;
+                gap:8px;
+                .label-item{
+                    padding:6px 12px;
+                    border: 1px solid #409EFF;
+                    color: #409EFF;
+                    background-color: #EAF3FE;
+                    box-sizing: border-box;
+                    border-radius: 4px;
+                }
+            }
+        }
+    }
+}
+</style>