浏览代码

培训课程页面

cxmo 1 年之前
父节点
当前提交
f2207d8a2c

+ 1 - 0
package.json

@@ -16,6 +16,7 @@
     "lodash": "^4.17.21",
     "normalize.css": "^8.0.1",
     "vue": "^3.3.4",
+    "vue-qr": "^4.0.9",
     "vue-router": "^4.0.12"
   },
   "devDependencies": {

+ 38 - 0
src/api/trainingVideoApi.js

@@ -0,0 +1,38 @@
+import {get,post} from './index'
+
+/**
+ * 获取分类列表
+ */
+export function apiClassifyList(params){
+    return get('/training_video/classify/tree',params)
+}
+/**
+ * 获取标签列表
+ */
+export function apiTagList(params){
+    return get('/training_video/tag/list',params)
+}
+/**
+ * 获取视频列表
+ * @param {Object} params 
+ * @param {Number} params.page_size
+ * @param {Number} params.current_index
+ * @param {String} params.keyword
+ * @param {Boolean} params.is_new 是否最新:true-是;false-否
+ * @param {Boolean} params.is_hot 是否最热:true-是;false-否
+ * @param {Number} params.classify_id
+ * @param {String} params.tag_ids 标签IDs, 英文逗号拼接
+ */
+export function apiVideoList(params){
+    return get('/training_video/video/page_list',params)
+}
+/**
+ * 获取视频详情
+ * @param {Object} params 
+ * @param {String} params.video_code
+ * @param {String} params.business_code
+ * @returns 
+ */
+export function apiVideoDetail(params){
+    return get('/training_video/video/detail',params)
+}

+ 16 - 0
src/assets/css/style.scss

@@ -4,4 +4,20 @@ li {
   margin: 0;
   padding: 0;
   box-sizing: border-box;
+}
+
+.word-ellipsis{
+    text-overflow: ellipsis;
+    overflow: hidden;
+    white-space: nowrap;
+}
+
+.simple-center{
+    display: flex;
+    align-items: center;
+}
+
+.simple-between{
+    display:flex;
+    justify-content:space-between;
 }

文件差异内容过多而无法显示
+ 1 - 0
src/assets/img/icon/wx_icon.svg


+ 3 - 0
src/layouts/Index.vue

@@ -40,6 +40,9 @@
         .title{
             font-size: 18px;
         }
+        @media screen and (max-width:650px){
+            display: none;
+        }
     }
 }
 </style>

+ 16 - 0
src/router/index.js

@@ -22,6 +22,22 @@ const routes=[
                     title: "ETA投研系统更新日志",
                 },
             },
+            {
+                path:"/video/list",
+                name:"VideoList",
+                component:() => import("@/views/trainingVideo/List.vue"),
+                meta:{
+                    title:"培训课程"
+                }
+            },
+            {
+                path:"/video/detail",
+                name:"VideoDetail",
+                component:() => import("@/views/trainingVideo/Detail.vue"),
+                meta:{
+                    title:""
+                }
+            },
         ]
     },
     {

+ 192 - 0
src/views/trainingVideo/Detail.vue

@@ -0,0 +1,192 @@
+<script setup>
+import { onMounted , onUnmounted , ref,reactive, onDeactivated} from 'vue';
+import {apiVideoDetail} from '@/api/trainingVideoApi';
+import { useRoute} from "vue-router";
+import vueQr from 'vue-qr/src/packages/vue-qr.vue';
+import { ElMessage } from 'element-plus';
+
+const route = useRoute()
+let videoData = ref({})
+let qrCodeUrl = ref('')
+let offsetWidth = ref(0)
+
+function getVideoDetail(){
+    const {video_code,bus_code}=route.query
+    apiVideoDetail({
+        video_code,
+        bus_code
+    }).then(res=>{
+        if(res.code!==200) return
+        videoData.value = res.data
+        getQrCodeUrl()
+    })
+}
+function getQrCodeUrl(){
+    qrCodeUrl.value = window.location.href
+}
+async function copyLink(){
+    if(navigator.clipboard&&window.isSecureContext){
+         try{
+            await navigator.clipboard.writeText(qrCodeUrl.value)
+            ElMessage({message:'复制链接成功',type:'success'})
+        }catch(err){
+            console.log(err);
+            ElMessage({message:'复制链接失败',type:'error'})
+        }
+    }else{
+        const input = document.createElement('input')
+        input.setAttribute('readonly','readonly')
+        input.value = qrCodeUrl.value
+        document.body.appendChild(input)
+        input.select();
+        document.execCommand('copy');
+        document.body.removeChild(input);
+        ElMessage({message:'复制链接成功',type:'success'})
+    }
+}
+
+function changeOffsetWidth(){
+    offsetWidth.value = document.body.offsetWidth
+}
+
+onMounted(()=>{
+    getVideoDetail()
+    changeOffsetWidth()
+    window.addEventListener('resize',changeOffsetWidth)
+})
+onUnmounted(()=>{
+    window.removeEventListener('resize',changeOffsetWidth)
+})
+
+</script>
+
+<template>
+    <div class="video-detail-wrap">
+        <div class="video-main-box" v-show="route.query.video_code&&offsetWidth>650">
+            <h3 class="video-title">{{videoData.Title}}</h3>
+            <div class="video-box">
+                <video :src="videoData.VideoUrl" controls></video>
+            </div>
+            <div class="video-info-box">
+                <div class="simple-between">
+                    <div class="tag-box">
+                        <span class="tag-item" v-for="tag in videoData.Tags" :key="tag.TagId">{{tag.TagName}}</span>
+                    </div>
+                    <el-popover
+                        placement="bottom-start"
+                        :width="200"
+                        trigger="click"
+                    >
+                        <template #reference>
+                            <div class="share-btn simple-center"><el-icon style="margin-right: 5px;"><Share /></el-icon>分享</div>
+                        </template>
+                        <div class="pop-box">
+                            <div class="copy-link simple-center" style="cursor: pointer;" @click="copyLink">
+                                <el-icon style="margin-right: 5px;"><Link /></el-icon>复制链接
+                            </div>
+                            <div class="code-scan">
+                                <p><img src="~@/assets/img/icon/wx_icon.svg"> 微信扫一扫</p>
+                                <div class="qr-code" style="text-align:center;">
+                                    <vueQr :text="qrCodeUrl" :size="120" :margin="0" v-if="qrCodeUrl"></vueQr>
+                                </div>
+                            </div>
+                        </div>
+                    </el-popover>
+                </div>
+                <div class="simple-between" style="margin-top: 20px;">
+                    <span class="simple-center"><el-icon style="margin-right:5px;"><VideoPlay /></el-icon>{{videoData.ViewTotal}}</span>
+                    <span>{{videoData.PublishTime}}</span>
+                </div>
+                <p class="introduce">{{videoData.Introduce}}</p>
+            </div>
+        </div>
+        <div class="video-mobile-box" v-show="route.query.video_code&&offsetWidth<=650">
+            <div class="video-box">
+                <video :src="videoData.VideoUrl" controls></video>
+            </div>
+            <h4 class="video-title">{{videoData.Title}}</h4>
+            <div class="tag-box">
+                <span class="tag-item" v-for="tag in videoData.Tags" :key="tag.TagId">{{tag.TagName}}</span>
+            </div>
+            <div class="simple-between" style="margin-top: 20px;">
+                <span class="simple-center"><el-icon style="margin-right:5px;"><VideoPlay /></el-icon>{{videoData.ViewTotal}}</span>
+                <span>{{videoData.PublishTime}}</span>
+            </div>
+            <p class="introduce">{{videoData.Introduce}}</p>
+        </div>
+        <div v-if="!route.query.video_code">
+            暂无视频信息
+        </div>
+
+    </div>
+</template>
+
+<style scoped lang="scss">
+.video-detail-wrap{
+    background-color:#F2F6FA;
+    height: calc(100vh - 60px);
+    box-sizing: border-box;
+    padding:10px;
+    @media screen and (max-width:650px){
+        padding: 0;
+        height: 100vh;
+        /* margin-top: -60px; */
+    }
+    .video-title{
+        margin:0 0 20px 0;
+    }
+    .video-box{
+        text-align: center;
+        background-color: #D9D9D9;
+        video{
+            /* width:100%; */
+            height:500px;
+            outline: none;
+        }
+    }
+    .introduce{
+        color: #999999;
+    }
+    .tag-item,.share-btn{
+        box-sizing: border-box;
+        padding:6px;
+        background-color: #ECF2FE;
+        color:#0052D9;
+        border-radius: 4px;
+        margin-right: 10px;
+    }
+    .video-main-box{
+        max-width: 1520px;
+        background-color: #fff;
+        margin: 0 auto;
+        box-sizing: border-box;
+        padding:40px 130px;
+        height:calc(100vh - 60px - 20px);
+        overflow-y: auto;
+        .video-info-box{
+            margin:20px 0;
+            .share-btn{
+                margin-right: 0;
+                cursor: pointer;
+            }
+        }
+    }
+    .video-mobile-box{
+        width:100%;
+        height: 100%;
+        background-color: #fff;
+        box-sizing: border-box;
+        padding: 10px;
+        .video-box{
+            background-color: transparent;
+            video{
+                width:100%;
+                height: auto;
+            }
+        }
+        .video-title{
+            margin-top: 20px;
+        }
+    }
+}
+</style>

+ 247 - 0
src/views/trainingVideo/List.vue

@@ -0,0 +1,247 @@
+<script setup>
+import { onMounted , ref , watch} from 'vue';
+import { useRoute , useRouter} from "vue-router";
+import {apiClassifyList,apiTagList,apiVideoList} from '@/api/trainingVideoApi';
+import { Search } from '@element-plus/icons-vue'
+
+const route = useRoute()
+const router = useRouter()
+
+let classifyList = ref([]) //分类列表
+let choosedClassify = ref({ClassifyId:0}) //所选分类
+let isClassifyListExpand = ref(false) //分类列表是否展开
+let tagList = ref([]) //标签列表
+let choosedTags = ref([]) //所选标签
+let isTagListExpand = ref(false) //标签列表是否展开
+let videoList = ref([]) //视频列表
+let total = ref(0) //视频总数
+let currentIndex = ref(1) //列表页
+let keyword = ref('') //搜索词
+let selectValue = ref('New') //select筛选项
+
+
+
+let beforeMounted = ref(false)
+
+function getClassifyList(){
+    apiClassifyList().then(res=>{
+        if(res.code!==200) return 
+        classifyList.value = res.data&&res.data.List||[]
+    })
+}
+
+function changeClassify(classify){
+    choosedClassify.value = classify
+    handleCurrentChange(1)
+}
+
+function changeTags(tag){
+    const {TagId} = tag
+    const index = choosedTags.value.findIndex(i=>i.TagId===TagId)
+    if(index!==-1){
+        choosedTags.value.splice(index,1)
+    }else{
+        choosedTags.value.push(tag)
+    }
+}
+watch(
+    ()=>choosedTags.value.length,
+    ()=>{
+        handleCurrentChange(1)
+    }
+)
+
+function getTagList(){
+    apiTagList().then(res=>{
+        if(res.code!==200) return 
+        tagList.value = res.data||[]
+    })
+}
+
+function getVideoList(){
+    apiVideoList({
+        page_size:15,
+        current_index:currentIndex.value,
+        keyword:keyword.value,
+        is_new:selectValue.value==='New',
+        is_hot:selectValue.value==='Hot',
+        classify_id:choosedClassify.value.ClassifyId||0,
+        tag_ids:choosedTags.value.map(i=>i.TagId).join(',')
+    }).then(res=>{
+        if(res.code!==200) return
+        if(res.data){
+            videoList.value = res.data.list||[]
+            total.value = res.data.page.total||0
+        }
+    })
+}
+function handleCurrentChange(page){
+    currentIndex.value = page
+    getVideoList()
+}
+
+function gotoVideoDetail(video){
+    const { VideoCode } = video
+    const {bus_code} = route.query
+    router.push({
+        path:'/video/detail',
+        query:{
+            video_code:VideoCode,
+            bus_code:bus_code||''
+        }
+    })
+}
+
+onMounted(()=>{
+    getClassifyList()
+    getTagList()
+    getVideoList()
+    beforeMounted.value = true
+
+})
+
+
+</script>
+
+<template>
+    <div class="video-list-wrap">
+        <div class="classify-box select">
+            <span style="align-self:center;">分类:</span>
+            <div class="list">
+                <span class="list-item" :class="{'active':choosedClassify.ClassifyId===0}"
+                    @click="changeClassify({ClassifyId:0})">全部</span>
+                <span class="list-item" :class="{'active':choosedClassify.ClassifyId===item.ClassifyId}"
+                    v-for="item in classifyList" :key="item.ClassifyId" @click="changeClassify(item)">{{item.ClassifyName}}</span>
+            </div>
+        </div>
+        <div class="tag-box select">
+            <span style="align-self:center;">标签:</span>
+            <div class="list">
+                <span class="list-item" :class="{'active':choosedTags.length===0}"
+                    @click="choosedTags = []">全部</span>
+                <span class="list-item" :class="{'active':choosedTags.findIndex(i=>i.TagId===item.TagId)!==-1}"
+                    v-for="item in tagList" :key="item.TagId" @click="changeTags(item)">{{item.TagName}}</span>
+            </div>
+        </div>
+        <div class="select-box">
+            <el-select v-model="selectValue" placeholder="Select" size="large" @change="handleCurrentChange(1)">
+                <el-option label="最新" value="New"/>
+                <el-option label="最热" value="Hot"/>
+            </el-select>
+        </div>
+        <div class="list-box">
+            <div class="list-item" v-for="item in videoList" :key="item.VideoId" @click="gotoVideoDetail(item)">
+                <div class="list-image" :style="{background:`no-repeat top/contain url('${item.CoverImg}')`}"></div>
+                <h4 class="word-ellipsis">{{item.Title}}</h4>
+                <div class="tag-box">
+                    <span class="tag-item" v-for="tag in item.Tags" :key="tag.TagId">{{tag.TagName}}</span>
+                </div>
+                <p class="word-ellipsis" style="flex:1;color:#999999;text-overflow: ellipsis;">{{item.Introduce||'暂无简介'}}</p>
+                <div class="other">
+                    <span>{{item.PublishTime}}</span>
+                    <el-icon style="margin-left: auto;margin-right:5px;"><VideoPlay /></el-icon>{{item.ViewTotal}}
+                </div>
+            </div>
+            <div class="empty-hint" v-if="videoList.length===0" style="width:100%;text-align: center;color: #999999;">
+                <img src="~@/assets/img/nodata.png" alt="" style="width: 200px;"/>
+                <p>{{keyword?`没有找到与${keyword}相关的课程`:'暂无课程'}}</p>
+            </div>
+        </div>
+        <div class="page-box">
+            <el-pagination
+                v-model:current-page="currentIndex"
+                layout="total,->,prev,pager,next,jumper" 
+                :page-size="15" 
+                :total="total"
+                @current-change="handleCurrentChange"
+               >
+            </el-pagination>
+        </div>
+        <!-- teleport search -->
+        <Teleport to=".layout-header-other" v-if="beforeMounted">
+            <div class="search-box">
+                <el-input placeholder="请输入视频名称" size="large" clearable style="width:280px;"
+                :prefix-icon="Search" v-model.trim="keyword" @input="handleCurrentChange(1)"></el-input>
+            </div>
+        </Teleport>
+    </div>
+</template>
+
+<style scoped lang="scss">
+.video-list-wrap{
+    padding:30px 120px 30px 120px;
+    box-sizing: border-box;
+    background-color:#F2F6FA;
+    height: calc(100vh - 60px);
+    display: flex;
+    flex-direction: column;
+    .select{
+        display: flex;
+        margin-bottom:20px;
+        .list{
+            display: flex;
+            margin-left: 15px;
+            gap: 10px;
+            .list-item{
+                box-sizing: border-box;
+                padding: 8px;
+                background-color: #fff;
+                border-radius: 4px;
+                cursor: pointer;
+                &.active,&:hover{
+                    color:#fff;
+                    background-color:#0052D9;
+                }
+            }
+        }
+    }
+    .list-box{
+        margin-top: 20px;
+        flex: 1;
+        overflow-y: auto;
+        width:100%;
+        display: flex;
+        gap: 20px;
+        .list-item{
+            width:19%;
+            height: 345px;
+            box-sizing: border-box;
+            padding:10px;
+            background-color: #fff;
+            border-radius:8px;
+            display: flex;
+            flex-direction: column;
+            cursor: pointer;
+            .list-image{
+                width:100%;
+                height:0;
+                padding-bottom:67%;
+            }
+            h4{
+                margin:10px 0;
+            }
+            .tag-box{
+                .tag-item{
+                    background-color: #ECF2FE;
+                    color:#0052D9;
+                    padding:4px;
+                    text-align: center;
+                    margin-right:10px;
+                    border-radius: 4px;
+                }
+            }
+            .other{
+                display: flex;
+                justify-content: space-between;
+                align-items: center;
+            }
+
+        }
+    }
+    .page-box{
+        background-color: #fff;
+        box-sizing: border-box;
+        padding:12px;
+    }
+}
+</style>

部分文件因为文件数量过多而无法显示