Prechádzať zdrojové kódy

Merge branch 'master' of http://52.130.252.100:10000/TEAMMODEL/TMDContest

CrazyIter_Bin 1 rok pred
rodič
commit
a57430d2ab

+ 1 - 1
Contest/contest.client/src/api/http.js

@@ -216,7 +216,7 @@ function checkToken() {
 function refreshToken() {
     refreshing = true
     let areaRoute = window.location.pathname.split('/')
-    axios.post('/api/activity/login-portal', {
+    axios.post('/activity/login-portal', {
         "route": areaRoute[1],
         "token": localStorage.getItem('auth_token')
     }).then(res => {

+ 4 - 1
Contest/contest.client/src/main.js

@@ -15,7 +15,10 @@ import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
 
 let app = createApp(App)
 
-axios.defaults.baseURL = 'https://test.teammodel.cn'
+// 根据域名判断调用测试站接口
+let hostName = window.location.hostname
+
+axios.defaults.baseURL = hostName ? 'https://test.teammodel.cn' : ''
 
 app.config.globalProperties.$axios = axios
 app.config.globalProperties.$api = api

+ 15 - 4
Contest/contest.client/src/pinia/common.js

@@ -7,19 +7,26 @@ import { defineStore } from 'pinia'
 export const useStore = defineStore('main', {
     state: () => {
         return {
+            srvAdr: '', // 伺服器位置 China, Global
+            srvAdrType: '', // 正式站 product 測試站 test
             userInfo: {},
             schoolList: [],
             website: {},
         }
     },
     getters: {
-        numAdd: (state) => {
-
-        }
+        getSrvAdr: (state) => {
+            return state.srvAdr
+        },
     },
     actions: {
+        setSrvAdr(value) {
+            this.srvAdr = value
+        },
+        setSrvAdrType(value) {
+            this.srvAdrType = value
+        },
         setSchoolList(value) {
-            console.log('746545111');
             this.schoolList = value
         },
         setUserInfo(value) {
@@ -28,5 +35,9 @@ export const useStore = defineStore('main', {
         setWebsite(value) {
             this.website = value
         },
+        checkSrvAdr(context) {
+            let hostname = window.location.hostname
+            let domainUrl = hostname
+        }
     }
 })

+ 49 - 9
Contest/contest.client/src/view/Home.vue

@@ -1,17 +1,14 @@
 <template>
     <div class="common-layout">
         <el-container style="height: 100%;">
-            <el-header style="position: relative;">
-                <el-menu
-                    :default-active="activeIndex"
-                    class="el-menu-demo"
-                    mode="horizontal"
-                >
+            <el-header>
+                <el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal">
                     <el-menu-item v-for="(item, index) in menuList.list" :key="index" :index="item.menuName" v-show="item.isShow">
                         <router-link :to="{name: item.router}">{{ item.name }}</router-link>
                     </el-menu-item>
                 </el-menu>
-                <div class="head-photo">
+                <span class="website-name">{{ store.website.name }}</span>
+                <div class="head-photo" v-show="userInfo?.exp">
                     <el-dropdown>
                         <el-avatar :size="35" :src="userInfo?.picture" />
                         <template #dropdown>
@@ -42,6 +39,9 @@
                         </template>
                     </el-dropdown>
                 </div>
+                <div class="head-photo" v-show="!userInfo?.exp">
+                    <p @click="toTeammodel()" style="height: 35px; line-height: 35px;">登录</p>
+                </div>
             </el-header>
             <el-main>
                 <router-view></router-view>
@@ -157,7 +157,7 @@ function getWebsite() {
         }
         proxy.$api.getWebsite(params).then(res => {
             if(res.code === 200) {
-                res.website.blobUrl = res.blobUrl
+                // res.website.blobUrl = res.blobUrl
                 store.setWebsite(res.website)
                 otherWebsite.value = res.websites
             } else if(res.code === 2) {
@@ -262,6 +262,7 @@ watch(ticket, (newValue, oldValue) => {
         position: absolute;
         top: 11px;
         right: 40px;
+        margin-right: 40px;
         cursor: pointer;
         .user_avatar{
             width: 36px;
@@ -270,8 +271,47 @@ watch(ticket, (newValue, oldValue) => {
             background-color: #72727f;
         }
     }
+    .website-name {
+        position: absolute;
+        left: 50%;
+        top: 50%;
+        transform: translate(-50%, -50%);
+    }
     .el-header {
-        padding: 0;
+        position: relative;
+        // padding: 0;
+        // border-bottom: solid 1px var(--el-menu-border-color);
+        background-color: #15559a;
+        color: #fff;
+        
+        a {
+            text-decoration: none; /* 移除链接默认的下划线 */
+        }
+        
+        .el-menu {
+            background-color: #15559a;
+            min-width: 350px;
+        }
+        .el-menu--horizontal.el-menu {
+            border-bottom: none;
+        }
+        .el-menu--horizontal>.el-menu-item {
+            color: #F1F1E6;
+        }
+
+        .el-menu--horizontal>.el-menu-item.is-active {
+            border-bottom-color: #fff;
+            // border-bottom: none;
+            color: #fff !important;
+            // font-size: 16px;
+            font-weight: bold;
+        }
+        .el-menu--horizontal .el-menu-item:not(.is-disabled):focus, .el-menu--horizontal .el-menu-item:not(.is-disabled):hover {
+            background-color: #15559a;
+            // font-size: 16px;
+            // font-weight: bold;
+            border-bottom: 2px solid #fff;
+        }
     }
     .el-avatar {
         outline: none;

+ 51 - 5
Contest/contest.client/src/view/activitylist/ActivityInfo.less

@@ -1,5 +1,7 @@
 .act-info {
     height: 100%;
+    background: linear-gradient(0deg, #c3d9e9, transparent);
+    overflow: auto;
 
     .list-header {
         height: 45px;
@@ -53,11 +55,20 @@
         padding: 20px 0;
         font-size: 16px;
 
-        img {
-            width: 70%;
-            height: 400px;
-            margin-left: 15%;
-            border-radius: 5px;
+        
+
+        .img-box {
+            // height: 500px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+
+            img {
+                width: auto;
+                max-width: 100%;
+                max-height: 500px;
+                border-radius: 5px;
+            }
         }
 
         // .sk-box{
@@ -69,11 +80,46 @@
         }
 
         .sk-info {
+            margin-bottom: 20px;
 
             // padding: 0 10px;
             &>p {
                 margin: 10px;
             }
+
+            .activity-info {
+                background-color: #fff;
+                border-radius: 5px;
+                // padding: 20px;
+                margin-top: 20px;
+
+                .title-time {
+                    display: flex;
+                    justify-content: space-between;
+                    font-size: 20px;
+                    padding: 20px;
+                    border-bottom: 1px dashed #ccc;
+                }
+
+                .detail-info {
+                    padding: 20px;
+                    color: #565656;
+
+                    &>p {
+                        margin-bottom: 15px;
+                    }
+                    
+                    .attach-name {
+                        cursor: pointer;
+                        margin-bottom: 10px;
+
+                        &:hover {
+                            color: #0d7001;
+                            font-weight: bold;
+                        }
+                    }
+                }
+            }
         }
 
         // }

+ 128 - 32
Contest/contest.client/src/view/activitylist/ActivityInfo.vue

@@ -12,40 +12,72 @@
                 {{ actInfo.name }}
             </p>
             <div class="join-btn" @click="openDraw">
-                报名
+                {{ registered === 1 ? '已报名' : '报名' }}
             </div>
         </div>
-        <div class="info-box">
-            <div style="margin-bottom: 20px;" class="sk-info">
-                <img :src="actInfo.posterSas" alt="">
-                <p style="text-align: center; font-size: 20px;font-weight: bold; margin: 20px 0;">
-                    <span>{{ $tools.formatTime(actInfo.stime, 'yyyy-MM-dd') }}</span> - <span>{{ $tools.formatTime(actInfo.etime, 'yyyy-MM-dd') }}</span>
-                </p>
-                <p>主题:{{ actInfo.subject || '-' }}</p>
-                <p>简介:{{ actInfo.description || '-' }}</p>
-                <p>地点:{{ actInfo.address || '-' }}</p>
-                <p>主办:
-                    <span v-for="(item, index) in actInfo.zb" :key="index">{{ item || '-' }}</span>
-                </p>
-                <p>承办:
-                    <span v-for="(item, index) in actInfo.zb" :key="index">{{ item || '-' }}</span>
-                </p>
-                <p>免责声明:{{ actInfo.mzsm || '-' }}</p>
-                <div style="display: flex; margin: 10px;">
-                    <p style="min-width: 50px;">附件:</p>
-                    <div v-if="actInfo.attachment.length">
-                        <p v-for="item in actInfo.attachment" :key="item.blob" @click="onPreview(item)" class="attach-name">{{ item.name }}</p>
+        <div style="height: 94%">
+            <el-scrollbar>
+                <div class="info-box">
+                    <div class="sk-info">
+                        <div class="img-box">
+                            <img :src="actInfo.posterSas" alt="">
+                        </div>
+                        <div class="activity-info">
+                            <h1 class="title-time">
+                                <span>活动信息</span>
+                                <span style="font-size: 16px; color: #205bd0;">
+                                    <span>{{ $tools.formatTime(actInfo.stime, 'yyyy-MM-dd') }}</span> - <span>{{ $tools.formatTime(actInfo.etime, 'yyyy-MM-dd') }}</span>
+                                </span>
+                            </h1>
+                            <div class="detail-info">
+                                <p>主题:{{ actInfo.subject || '-' }}</p>
+                                <p>简介:{{ actInfo.description || '-' }}</p>
+                                <p>地点:{{ actInfo.address || '-' }}</p>
+                                <p>主办:
+                                    <span v-for="(item, index) in actInfo.zb" :key="index">{{ item || '-' }}</span>
+                                </p>
+                                <p>承办:
+                                    <span v-for="(item, index) in actInfo.zb" :key="index">{{ item || '-' }}</span>
+                                </p>
+                                <p>免责声明:{{ actInfo.mzsm || '-' }}</p>
+                                <div style="display: flex;">
+                                    <p style="min-width: 50px;">附件:</p>
+                                    <div v-if="actInfo.attachment.length">
+                                        <p v-for="item in actInfo.attachment" :key="item.blob" @click="onPreview(item)" class="attach-name">{{ item.name }}</p>
+                                    </div>
+                                    <span v-else>-</span>
+                                </div>
+                            </div>
+                        </div>
+                        <template v-if="actInfo.modules">
+                            <div v-if="actInfo.modules.includes('Contest')" class="activity-info">
+                                <h1 class="title-time">赛课活动</h1>
+                                <div class="detail-info">
+                                    <el-steps :active="contestStep" align-center>
+                                        <el-step v-for="(item, index) in contest.modules" :key="index" :title="skModuleList[index]">
+                                            <template v-slot:icon>
+                                                <el-icon v-show="item === 'sign'" size="20"><Edit /></el-icon>
+                                                <el-icon v-show="item === 'upload'" size="20"><Folder /></el-icon>
+                                                <el-icon v-show="item === 'review'" size="20"><Finished /></el-icon>
+                                                <el-icon v-show="item === 'score'" size="20"><CircleCheck /></el-icon>
+                                            </template>
+                                            <template v-slot:description>
+                                                <span>
+                                                    {{ $tools.formatTime(contest[item].stime, 'yyyy-MM-dd') }}
+                                                    -
+                                                    {{ $tools.formatTime(contest[item].etime, 'yyyy-MM-dd') }}
+                                                </span>
+                                            </template>
+                                        </el-step>
+                                    </el-steps>
+                                </div>
+                            </div>
+                        </template>
                     </div>
-                    <span v-else>-</span>
                 </div>
-                <p v-if="actInfo.modules">
-                    <el-tag effect="dark" v-if="actInfo.modules.includes('Contest')">
-                        赛课模块
-                    </el-tag>
-                </p>
-            </div>
-            <!-- <el-button type="success" @click="joinBtn">报名</el-button> -->
+            </el-scrollbar>
         </div>
+            <!-- <el-button type="success" @click="joinBtn">报名</el-button> -->
         <el-drawer v-model="joinDra">
             <template #header>
                 <h4>活动报名</h4>
@@ -151,11 +183,25 @@
                 </div>
             </template>
         </el-drawer>
+        <div v-if="previewStatus" class="image-viewer">
+			<div style="width:fit-content;position:relative;margin:auto;" v-if="previewFile.type != 'image'">
+                <el-icon class="close-icon" @click="previewStatus = false"><CloseBold /></el-icon>
+				<video v-if="previewFile.type == 'video'" id="previewVideo" :src="previewFile.urlShow" width="870"
+					controls="controls" style="max-height: 800px;">
+					您的浏览器不支持 video 标签。
+				</video>
+				<audio v-else-if="previewFile.type === 'audio'" controls>
+					<source :src="previewFile.urlShow">
+					您的浏览器不支持 audio 元素。
+				</audio>
+			</div>
+            <el-image-viewer v-else hide-on-click-modal @close="previewStatus = false" :url-list="[previewFile.urlShow]" />
+		</div>
     </div>
 </template>
 
 <script setup>
-import { ArrowLeft, WarningFilled, Search, CircleCheck, CircleClose, CaretBottom, CaretRight, Message } from '@element-plus/icons-vue'
+import { ArrowLeft, WarningFilled, Search, CircleCheck, CircleClose, CaretBottom, CaretRight, Message, Edit, Finished, Folder, CloseBold } from '@element-plus/icons-vue'
 import { ElMessageBox, ElMessage, ElLoading } from 'element-plus'
 import { useStore } from "@/pinia/common"
 import { reactive, ref, onMounted, watch, toRaw, getCurrentInstance } from 'vue'
@@ -170,7 +216,9 @@ let skModule = ref(true)
 let yxModule = ref(true)
 let jyModule = ref(true)
 let codeSearch = ref(true) //搜索标识码
+let previewStatus = ref(false)
 let registered = ref(0) //是否可以报名 0:可以 1:已经报名 2:所在学校未被邀请 3:不在报名时间
+let contestStep = ref(0)
 let schoolIndex = ref('')
 let contest = ref({})
 let research = ref({})
@@ -179,6 +227,7 @@ const infoId = ref(route.params.info)
 const actInfo = ref({attachment: []})
 let teamMembers = ref([])
 let rules = ref({})
+let previewFile = ref({})
 let enrollForm = ref(null)
 
 let loading = reactive({
@@ -201,6 +250,7 @@ const enrollData = reactive({
     type: null, //1团队。0个人
     enrollInfos: [],
 })
+const skModuleList = ref(['报名', '上传', '评审', '公示'])
 
 const validateInfo = (rule, value, callback) => {
     let info = enrollData.enrollInfos.find(item => item.code === rule.field)
@@ -232,7 +282,7 @@ function getListInfo() {
         if(res.code === 200) {
             res.activity.posterSas = !res?.activity.poster ? require('@/assets/img/no-poster-cn.png') : res?.activity.url + res?.activity.poster + '?' + res?.activity.sas
             res?.activity.attachment.forEach(item => {
-                item.urlSas = res?.activity.url + item.url + '?' + res?.activity.sas
+                item.urlShow = res?.activity.url + item.url + '?' + res?.activity.sas
             })
             actInfo.value = res.activity
             // 公开 && 未指定学校 可以参加
@@ -251,9 +301,14 @@ function getListInfo() {
             }
 
             let nowTime = Date.parse(new Date())
+            res.contest.modules.forEach((item, index) => {
+                res.contest[item].actState = nowTime < res.contest[item].stime ? 'notStart' : (nowTime > res.contest[item].etime ? 'finish' : 'going')
+                if(res.contest[item].actState === 'going') {
+                    contestStep = index === 1 ? (res.contest.sign.actState === 'going' ? 0 : 1) : index
+                }
+            })
             contest.value = res.contest
             enrollData.type = res.contest.sign.type
-            console.log(nowTime > res.contest.sign.stime && nowTime < res.contest.sign.etime);
             registered.value = nowTime > res.contest.sign.stime && nowTime < res.contest.sign.etime ? 0 : 3
             enrollData.enrollInfos = contest.value.sign.fields.map(item => {
                 return {
@@ -283,6 +338,10 @@ function getEnroll() {
         activityId: infoId.value
     }
     proxy.$api.teaContest(params).then(res => {
+        if(res?.enroll) {
+            registered.value = 1
+            return
+        }
         if(registered.value != 3 && actInfo.value?.scope != 'school') {
             registered.value = schoolList.length ? (res.code === 200 ? 1 : 0) : 2
         }
@@ -439,6 +498,9 @@ function joinBtn() {
     })
 }
 function openDraw() {
+    if(registered.value === 1) {
+        return
+    }
     if(registered.value) {
         ElMessage({
             type: 'warning',
@@ -451,6 +513,24 @@ function openDraw() {
         joinDra.value = true
     }
 }
+
+/* 预览 */
+async function onPreview(item) {
+    let url = item.urlShow
+    console.log('gr5e4gt4h', url);
+    if (proxy.$tools.getSuffix(item.name) === 'pdf') {
+        window.open('https://www.teammodel.cn/web/viewer.html?file=' + encodeURIComponent(url));
+    } else if(item.type === 'doc') {
+        window.open('https://view.officeapps.live.com/op/view.aspx?src=' + escape(url));
+    // } else if(item.type === 'image') {
+        // $hevueImgPreview(url)
+    } else if(item.type === 'link') {
+        window.open(/^(http:|https:)/i.test(url) ? url : "http://" + url)
+    } else {
+        previewFile.value = item
+        previewStatus.value = true
+    }
+}
 </script>
 
 <style scoped lang="less">
@@ -476,4 +556,20 @@ function openDraw() {
         }
     }
 }
+.detail-info {
+    .el-step__head.is-finish,
+    .el-step__title.is-finish,
+    .el-step__description.is-finish {
+        color: #85bc73;
+        border-color: #9cba92;
+    }
+
+    .el-step__head.is-process,
+    .el-step__title.is-process,
+    .el-step__description.is-process {
+        color: #409eff;
+        border-color: #409eff;
+
+    }
+}
 </style>

+ 14 - 10
Contest/contest.client/src/view/homepage/HomePage.vue

@@ -4,10 +4,10 @@
             <el-carousel-item v-for="(item, index) in carousel" :key="index">
                 <div class="carousel-box" @click="openBanner(item)">
                     <img :src="item.img" alt="" />
-                    <div class="title-box">
+                    <!-- <div class="title-box">
                         <p>{{ item.title }}</p>
                         <p>{{ item.subtitle }}</p>
-                    </div>
+                    </div> -->
                 </div>
             </el-carousel-item>
         </el-carousel>
@@ -90,9 +90,9 @@ onMounted(() => {
     let banners = store.website.banners
     banners.forEach(item => {
         if(item.source === 'upload') {
-            item.img = 'https://teammodeltest.blob.core.chinacloudapi.cn' + '/' + store.website.id + item.blob + '?' + store.website.sas
+            item.img = store.website.url + item.blob + '?' + store.website.sas
         } else if(item.source === 'activity') {
-            item.img = item.blob + '?' + store.website.sas
+            item.img = store.website.url + item.blob + '?' + store.website.sas
         }
         carousel.value.push(item)
     })
@@ -131,6 +131,7 @@ function getActList() {
                 item.posterShow = !item.poster ? require('@/assets/img/no-poster-cn.png') : item.url + item.poster + '?' + item.sas
                 lessonList.value.push(item)
             })
+            lessonList.value = lessonList.value.sort((a, b) => b.createTime - a.createTime)
             lessonListShow.value = lessonList.value
         }
     }).finally(() => {
@@ -166,7 +167,7 @@ watch(actType, ((newValue, oldValue) => {
 </script>
 <style lang="less" scoped>
 .home-page {
-    padding: 0 15%;
+    padding: 0 13%;
     // padding-top: 20px;
     height: 100%;
     background: linear-gradient(0deg, #c3d9e9, transparent);
@@ -176,6 +177,8 @@ watch(actType, ((newValue, oldValue) => {
         width: 100%;
         height: 100%;
         position: relative;
+        display: flex;
+        align-items: center;
 
         .title-box {
             position: absolute;
@@ -221,18 +224,19 @@ watch(actType, ((newValue, oldValue) => {
 
                 .img-box {
                     max-width: 95%;
-                    display: block;
                     /* border-top-left-radius: 5px;
                     border-top-right-radius: 5px; */
-                    border-radius: 5px;
                     margin: 10px;
-                    height: 170px;
-                    text-align: center;
+                    height: 142px;
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
 
                     img {
                         width: auto;
                         max-width: 100%;
                         max-height: 100%;
+                        border-radius: 5px;
                     }
                 }
 
@@ -288,7 +292,7 @@ watch(actType, ((newValue, oldValue) => {
                 .lesson-type {
                     position: absolute;
                     top: 10px;
-                    right: 9px;
+                    right: 10px;
                     height: 20px;
                     line-height: 20px;
                     font-size: 14px;

+ 42 - 9
Contest/contest.client/src/view/myactivity/MyActivity.less

@@ -7,6 +7,8 @@
         width: 20%;
         height: 100%;
         border-right: 1px solid #ccc;
+        // background-color: #15559a;
+        // color: #fff;
 
         .list-icon {
             width: auto;
@@ -34,6 +36,7 @@
 
         .list-info {
             padding: 15px 20px;
+            // margin: 0 5px;
 
             &>p {
                 margin-bottom: 10px;
@@ -89,18 +92,24 @@
 
         .list-info:hover,
         .list-info-active {
-            background-color: #E1EFF6;
+            background-color: #EBECED;
             cursor: pointer;
+            border-radius: 5px 0 0 5px;
         }
     }
 
     .act-info {
         width: 80%;
-        height: 100%;
+        height: auto;
+        padding: 20px;
+        background-color: #e8f3fd;
+        // background-color: #f4f4f4;
 
         .info-box {
-            padding: 20px;
-            height: 90%;
+            padding: 20px 30px;
+            height: 95%;
+            background-color: #fff;
+            border-radius: 5px;
 
             .demo-tabs {
                 height: 100%;
@@ -211,18 +220,24 @@
         .not-sign {
             // height: 100%;
             text-align: center;
-            margin-top: 10%;
+            // margin-top: 10%;
 
-            &>p {
+            &>p,
+            &>span {
                 font-size: 20px;
-                margin-top: 20px;
+                margin: 20px 0;
                 color: #3d3d3d;
                 font-weight: bold;
 
-                &:nth-of-type(2):hover {
+                /* &:nth-of-type(2):hover {
                     cursor: pointer;
                     color: #19be6b;
-                }
+                } */
+            }
+
+            &>span:hover {
+                cursor: pointer;
+                color: #19be6b;
             }
         }
     }
@@ -261,4 +276,22 @@
         margin: auto;
         font-size: 2rem;
     }
+}
+
+.el-popper {
+    .filter-box {
+        // margin: 10px;
+        &>p:first-child {
+            border-bottom: 1px solid #ccc;
+            padding: 5px;
+        }
+
+        &>div {
+            padding: 5px;
+
+            &>p {
+                margin-bottom: 5px;
+            }
+        }
+    }
 }

+ 166 - 24
Contest/contest.client/src/view/myactivity/MyActivity.vue

@@ -10,11 +10,45 @@
                         <el-button :icon="Search" @click="searchName()" />
                     </template>
                 </el-input> -->
+                <el-popover :width="200">
+                    <template #reference>
+                        <el-icon><Filter /></el-icon>
+                    </template>
+                    <template #default>
+                        <div class="filter-box">
+                            <p>筛选</p>
+                            <div>
+                                <p>类型:</p>
+                                <el-select v-model="scopeVal" @change="scopeChange" clearable placeholder="请选择" style="width: 160px">
+                                    <el-option label="公开" value="public" />
+                                    <el-option label="区级" value="area" />
+                                    <el-option label="校级" value="school" />
+                                </el-select>
+                            </div>
+                            <div>
+                                <p>状态:</p>
+                                <el-select v-model="publishVal" @change="publishChange" clearable placeholder="请选择" style="width: 160px">
+                                    <el-option label="未报名" value="notRegister" />
+                                    <el-option label="进行中" value="going" />
+                                    <el-option label="已结束" value="finish" />
+                                </el-select>
+                            </div>
+                            <div>
+                                <p>参与模块:</p>
+                                <el-checkbox-group v-model="modalList" @change="modalChange">
+                                    <el-checkbox label="Contest">赛课活动</el-checkbox>
+                                    <el-checkbox label="Training">教培活动</el-checkbox>
+                                    <el-checkbox label="Research">教研活动</el-checkbox>
+                                </el-checkbox-group>
+                            </div>
+                        </div>
+                    </template>
+                </el-popover>
             </div>
             <div style="height: 90%;">
-                <template v-if="actList.length">
+                <template v-if="actListShow.length">
                     <el-scrollbar>
-                        <div v-for="(item, index) in actList" :key="index" @click="getListInfo(item, index)"
+                        <div v-for="(item, index) in actListShow" :key="index" @click="getListInfo(item, index)"
                             :class="['list-info', actIndex === index ? 'list-info-active' : '']">
                             <p class="info-name">{{ item.name }}</p>
                             <div class="info-type">
@@ -37,8 +71,8 @@
                 <el-empty v-else description="暂无活动" />
             </div>
         </div>
-        <div class="act-info" v-if="actList.length">
-            <div class="info-box" v-if="actList[actIndex]?.isJoin">
+        <div class="act-info" v-if="actListShow.length">
+            <div class="info-box" v-if="actListShow[actIndex]?.isJoin">
                 <el-tabs v-model="actTab" class="demo-tabs">
                     <el-tab-pane label="成员信息" name="user">
                         <div v-if="!contest && !research && !training">
@@ -51,6 +85,7 @@
                                     <el-icon v-show="!skModule"><CaretRight /></el-icon>
                                     赛课模块
                                 </p>
+                                <el-alert title="上传作品时间已过" v-show="contest.upload.actState === 'finish'" type="warning" :closable="false" show-icon style="margin-top: 20px;" />
                                 <div v-show="skModule">
                                     <div class="cipher-box" v-show="contest.sign.type">
                                         <p>队伍名称:{{ cipher.teamName }}</p>
@@ -86,6 +121,9 @@
                                                 <el-button size="small" type="danger" @click="deleteMem(scope.$index, scope.row)" v-if="isLeader && !scope.row.myself && !scope.row.identity">
                                                     删除
                                                 </el-button>
+                                                <el-button size="small" @click="exitAct(scope.$index, scope.row)" v-if="scope.row.myself">
+                                                    退出
+                                                </el-button>
                                             </template>
                                         </el-table-column>
                                     </el-table>
@@ -107,8 +145,8 @@
                                             </div>
                                         </div>
                                     </template>
-                                    <!-- <template v-if="contest?.upload && !contest?.upload.captainUpload || contest?.upload.captainUpload && isLeader"> -->
-                                        <el-upload class="upload-demo" ref="refUpload" :file-list="fileList" :accept="accept" drag multiple :on-change="handleChange" :auto-upload="false" action="">
+                                    <template v-if="contest?.upload && contest.upload.actState === 'going' && !contest?.upload.captainUpload || contest?.upload.captainUpload && isLeader">
+                                        <el-upload class="upload-demo" ref="refUpload" :file-list="fileList" :accept="accept" drag multiple :on-remove="handleRemove" :on-change="handleChange" :auto-upload="false" action="">
                                             <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
                                             <div class="el-upload__text">
                                                 上传作品
@@ -123,18 +161,22 @@
                                                             {{ item }}
                                                         </el-tag>
                                                     </template>
+                                                    <template v-else-if="contest?.upload.type === 'lesson'">
+                                                        <el-tag type="success" disable-transitions>
+                                                            必须上传视频
+                                                        </el-tag>
+                                                    </template>
                                                 </div>
                                             </template>
                                         </el-upload>
                                         <el-button type="success" @click="uploadFile()">
                                             上传
                                         </el-button>
-                                    <!-- </template> -->
-                                    <!-- <template v-else-if="contest?.upload && contest?.upload.captainUpload && !isLeader">
-                                        <div>
-                                            本次活动由组长上传作品
-                                        </div>
-                                    </template> -->
+                                    </template>
+                                    <div v-else-if="contest?.upload && contest?.upload.captainUpload && !isLeader" style="text-align: center; margin-top: 10px;">
+                                        <el-icon color="#ba6b26"><Warning /></el-icon>
+                                        本次活动由组长上传作品
+                                    </div>
                                 </div>
                             </el-scrollbar>
                         </template>
@@ -170,13 +212,13 @@
                     </el-tab-pane>
                 </el-tabs>
             </div>
-            <div class="not-sign" v-else>
-                <el-icon size="120" color="#f4a83e"><WarnTriangleFilled /></el-icon>
+            <div class="info-box not-sign" v-else>
+                <el-icon size="120" color="#f4a83e" style="margin-top: 10%;"><WarnTriangleFilled /></el-icon>
                 <p>还未报名该活动</p>
-                <p @click="toSign()">
+                <span @click="toSign()">
                     马上去报名
                     <el-icon><DArrowRight /></el-icon>
-                </p>
+                </span>
             </div>
         </div>
         <el-empty v-else description="暂无活动" :image-size="300" />
@@ -198,7 +240,7 @@
 </template>
 
 <script setup>
-import { Search, Delete, UserFilled, Clock, UploadFilled, CaretBottom, CaretRight, WarnTriangleFilled, DArrowRight, CloseBold } from '@element-plus/icons-vue'
+import { Search, Delete, UserFilled, Clock, UploadFilled, CaretBottom, CaretRight, WarnTriangleFilled, DArrowRight, CloseBold, Warning, Filter } from '@element-plus/icons-vue'
 import { ElMessageBox, ElMessage, FormRules, ElLoading } from 'element-plus'
 import { getCurrentInstance, nextTick, onMounted, reactive, ref, toRaw, watch } from 'vue'
 import { useStore } from "@/pinia/common"
@@ -210,23 +252,29 @@ let store = useStore()
 let router = useRouter()
 
 let actIndex = ref(0)
+let activeStep = ref(0)
 let actTab = ref('user')
 let searchName = ref('')
 let accept = ref('')
 let nowYear = ref('')
+let scopeVal = ref('')
+let publishVal = ref('')
 let searShow = ref(false)
 let skModule = ref(true)
 let isLeader = ref(false)
 let previewStatus = ref(false)
 let actList = ref([])
+let actListShow = ref([])
 let memberData = ref([])
 let fileList = ref([])
 let uploadList = ref([])
+let modalList = ref([])
 let actInfo = ref({attachment: []})
 let contest = ref({})
 let research = ref({}) //教研
 let training = ref({}) //研修
 let previewFile = ref({})
+
 const refUpload = ref(HTMLElement)
 const cipher = ref({})
 
@@ -257,8 +305,9 @@ function getActList() {
                 item.isJoin = item.contestSign
                 return item
             })
-            if(actList.value.length) {
-                getListInfo(actList.value[0], 0)
+            actListShow.value = actList.value
+            if(actListShow.value.length) {
+                getListInfo(actListShow.value[0], 0)
             }
         }
     }).finally(() => {
@@ -284,6 +333,10 @@ function getListInfo(info, index) {
             })
             actInfo.value = res.activity
             if(res.contest?.upload.type === 'file') accept.value = res.contest?.upload.fileType.map(item => '.' + item).join()
+            let nowDate = new Date().getTime()
+            res.contest.modules.forEach((item, index) => {
+                res.contest[item].actState = nowDate < res.contest[item].stime ? 'notStart' : (nowDate > res.contest[item].etime ? 'finish' : 'going')
+            })
             contest.value = res.contest
             research.value = res.research
             training.value = res.training
@@ -299,7 +352,7 @@ function getListInfo(info, index) {
 function getEnroll() {
     let params = {
         grant_type: 'get-enroll',
-        activityId: actList.value[actIndex.value].id
+        activityId: actListShow.value[actIndex.value].id
     }
     proxy.$api.teaContest(params).then(async res => {
         if(res.code === 200) {
@@ -331,6 +384,7 @@ function getEnroll() {
                     res.enroll.contest.enrollInfos.forEach(filed => {
                         res.enroll[filed.code] = filed.val
                     })
+                    res.enroll.myself = true
                     memberData.value  = [res.enroll]
                     // loading.close()
                 }
@@ -344,7 +398,7 @@ function getEnroll() {
 function getTeamInfo() {
     let params = {
         grant_type: 'search-team-by-cipher',
-        activityId: actList.value[actIndex.value].id,
+        activityId: actListShow.value[actIndex.value].id,
         cipher: cipher.value.cipher
     }
     proxy.$api.teaContest(params).then(res => {
@@ -375,7 +429,7 @@ function changeLeader(index, data) {
     }) */
     let params = {
         grant_type: 'change-team-leader',
-        activityId: actList.value[actIndex.value].id,
+        activityId: actListShow.value[actIndex.value].id,
         targetLeader: data.tmdid
     }
     proxy.$api.teaContest(params).then(res => {
@@ -394,7 +448,36 @@ function changeLeader(index, data) {
     })
 }
 
-
+function exitAct(index, info) {
+    if(info.identity) {
+        ElMessage({
+            type: 'warning',
+            message: '请移交队长后再退出参赛'
+        })
+    } else {
+        ElMessageBox.confirm(`确认退出活动?`).then(() => {
+            let params = {
+                grant_type: 'cancel-enroll',
+                activityId: actInfo.value.id
+            }
+            proxy.$api.teaContest(params).then(res => {
+                if(res.code === 204) {
+                    ElMessage({
+                        type: 'success',
+                        message: '已退出活动'
+                    })
+                    actListShow.value[actIndex.value].isJoin = false
+                } else {
+                    ElMessage({
+                        type: 'warning',
+                        message: '退出失败'
+                    })
+                }
+            })
+            
+        })
+    }
+}
 
 function deleteMem(index, info) {
     console.log(index, info);
@@ -406,6 +489,10 @@ function deleteMem(index, info) {
     })
 }
 
+function handleRemove(file, files) {
+    fileList.value = files
+}
+
 function handleChange(file, files) {
     let info = {
         name: file.name,
@@ -512,6 +599,13 @@ async function uploadFile() {
         })
         return
     }
+    if(contest.value.upload.type === 'lesson' && !fileList.value.find(item => item.raw.type === 'video/mp4')) {
+        ElMessage({
+            type: 'warning',
+            message: '必须上传视频!'
+        })
+        return
+    }
     /* loading = ElLoading.service({
         lock: true,
         text: '加载中',
@@ -542,6 +636,8 @@ async function uploadFile() {
     } */
     if(contest.value.upload.type === 'file') {
         params.uploadData.files = blobList
+    } else if(contest.value.upload.type === 'lesson') {
+        params.uploadData.lessons = blobList
     } else {
         params.uploadData.sokrates = []
     }
@@ -568,7 +664,7 @@ function toSign() {
     router.push({
         name: 'activityInfo',
         params: {
-            info: actList.value[actIndex.value].id
+            info: actListShow.value[actIndex.value].id
         }
     })
 }
@@ -595,6 +691,52 @@ async function onDownload(item) {
     proxy.$tools.doDownloadByUrl(item.urlShow, item.name)
 }
 
+function scopeChange(value) {
+    if(value) {
+        actListShow.value = actList.value.filter(item => {
+            return item.scope === value && (publishVal.value ? (publishVal.value === 'notRegister' ? !item.isJoin : (item.isJoin && (publishVal.value === 'going' ? item.publish === 1 : item.publish === 2))) : true)
+        })
+    } else {
+        onFilterClear()
+    }
+}
+
+function publishChange(value) {
+    if(value) {
+        actListShow.value = actList.value.filter(item => {
+            return (value === 'notRegister' ? !item.isJoin : (item.isJoin && (value === 'going' ? item.publish === 1 : item.publish === 2))) && (scopeVal.value ? item.scope === scopeVal.value : true) && (modalList.value.length ? item.modules.every(modules => modalList.value.includes(modules)) : true)
+        })
+    } else {
+        onFilterClear()
+    }
+}
+
+function modalChange(value) {
+    if(value.length) {
+        actListShow.value = actList.value.filter(item => {
+            return item.modules.every(modules => value.includes(modules)) && (publishVal.value ? (publishVal.value === 'notRegister' ? !item.isJoin : (item.isJoin && (publishVal.value === 'going' ? item.publish === 1 : item.publish === 2))) : true) && (scopeVal.value ? item.scope === scopeVal.value : true)
+        })
+    } else {
+        onFilterClear()
+    }
+}
+/* 取消过滤 */
+function onFilterClear() {
+    if(scopeVal.value){
+        scopeChange(scopeVal.value)
+        return
+    }
+    if(publishVal.value){
+        publishChange(publishVal.value)
+        return
+    }
+    if(modalList.value.length) {
+        modalChange(modalList.value)
+        return
+    }
+    actListShow.value = actList.value
+}
+
 watch(nowYear, (newValue, oldValue) => {
     getActList()
 })

+ 31 - 3
Contest/contest.client/src/view/myactivity/MyReview.less

@@ -91,16 +91,29 @@
 
         .list-info:hover,
         .list-info-active {
-            background-color: #E1EFF6;
+            background-color: #EBECED;
             cursor: pointer;
+            border-radius: 5px 0 0 5px;
         }
     }
 
     .act-info {
         width: 80%;
+        height: auto;
+        padding: 20px;
+        background-color: #e8f3fd;
+        // background-color: #f4f4f4;
 
         .info-box {
             padding: 20px;
+            padding: 20px 30px;
+            height: 95%;
+            background-color: #fff;
+            border-radius: 5px;
+
+            .demo-tabs {
+                height: 100%;
+            }
 
             .upload-demo {
                 margin: 20px 0;
@@ -162,19 +175,32 @@
                     .review-header-works {
                         display: flex;
                         height: 300px;
+                        background-color: #f4f4f4;
+                        border-radius: 5px;
+                        // padding: 10px 15px;
 
                         .works-list {
                             width: 20%;
+                            height: 90%;
+                            // border-right: 1px solid #ccc;
+                            margin-left: 10px;
+                            margin-top: 10px;
 
                             .files-list {
-                                padding: 10px 10px;
+                                padding: 13px 15px;
                                 border-radius: 5px;
                                 color: #535353;
                                 cursor: pointer;
+
+                                &:hover {
+                                    background-color: #638ED4;
+                                    color: #fff;
+                                }
                             }
 
                             .active-file {
-                                background-color: #E1EFF6;
+                                background-color: #638ED4;
+                                color: #fff;
                             }
                         }
 
@@ -182,6 +208,8 @@
                             width: 80%;
                             text-align: center;
                             line-height: 300px;
+                            margin: 10px 0;
+
                             img {
                                 height: 100%;
                             }

+ 89 - 24
Contest/contest.client/src/view/myactivity/MyReview.vue

@@ -23,7 +23,7 @@
                                 </p> -->
                                 <span v-if="item.isFinish" class="info-end">已结束</span>
                                 <span v-else :class="['info-end', item.completeCount === item.taskCount ? 'info-review' : 'info-progress']">{{ item.completeCount === item.taskCount ? '已评审' : '评审中' }}</span>
-                                <span v-show="!item.isFinish" class="info-end">需评审 {{ item.taskCount - item.completeCount }} 个</span>
+                                <span v-show="!item.isFinish && item.completeCount != item.taskCount" class="info-end">需评审 {{ item.taskCount - item.completeCount }} 个</span>
                             </div>
                             <p class="info-time">
                                 <el-icon><Clock /></el-icon>
@@ -39,7 +39,7 @@
             <div class="info-box">
                 <el-tabs v-model="actTab" class="demo-tabs">
                     <el-tab-pane label="作品评审" name="user">
-                        <el-scrollbar height="750px">
+                        <el-scrollbar>
                             <div>
                                 <p @click="skModule = !skModule" class="module-box">
                                     <el-icon v-show="skModule"><CaretBottom /></el-icon>
@@ -65,12 +65,14 @@
                                             </div>
                                             <div class="review-header-works">
                                                 <div class="works-list">
-                                                    <div v-for="(item, index) in workInfo.files" :key="index" @click="changeFile(item, index)" :class="['files-list', {'active-file': fileIndex === index}]">
-                                                        <p>{{ item.name }}</p>
-                                                    </div>
+                                                    <el-scrollbar>
+                                                        <div v-for="(item, index) in workInfo.files" :key="index" @click="changeFile(item, index)" :class="['files-list', {'active-file': fileIndex === index}]">
+                                                            <p>{{ item.name }}</p>
+                                                        </div>
+                                                    </el-scrollbar>
                                                 </div>
                                                 <div class="works-info">
-                                                    <video v-if="fileInfo.type === 'video'" :id="'video'" :src="fileInfo.urlShow" controls="controls" style="border-radius: 5px;max-height: 700px;max-width: 100%;" />
+                                                    <video v-if="fileInfo.type === 'video'" :id="'video'" :src="fileInfo.urlShow" controls="controls" style="border-radius: 5px;max-height: 280px;max-width: 100%;" />
                                                     <img v-else-if="fileInfo.type === 'image'" :src="fileInfo.urlShow" style="border-radius: 5px;max-height: 700px !important;max-width: 100% !important;" />
                                                     <audio v-else-if="fileInfo.type === 'audio'" controls>
                                                         <source :src="fileInfo.urlShow" />
@@ -87,7 +89,7 @@
                                                 </div>
                                             </div>
                                         </div>
-                                        <div style="text-align: right; margin: 10px 0;" v-show="workList.length && workList.length != 1">
+                                        <div style="text-align: right; margin: 10px 20px;" v-show="workList.length && workList.length != 1">
                                             <el-button type="info" @click="changeWork()" v-show="workIndex">上一位</el-button>
                                             <el-button type="info" @click="changeWork(true)" v-show="workIndex != (workList.length - 1)">下一位</el-button>
                                             <el-button type="info" @click="scoring = true" :icon="Notebook" circle />
@@ -150,14 +152,19 @@
         </div>
         <el-empty v-else description="暂无活动" :image-size="300" />
         <el-dialog v-model="scoring" title="请选择打分作品" width="40%">
-            <div v-for="item in workList" :key="item.id">
+            <!-- <div v-for="item in workList" :key="item.id">
                 姓名:{{ item.name }} 学段:{{ item.period }} 学科:{{ item.subject }}
-            </div>
+            </div> -->
             <div>
                 <el-table :data="workList" row-key="id" highlight-current-row default-expand-all @current-change="handleCurrentChange">
                     <el-table-column prop="name" label="姓名" align="center" width="170" />
                     <el-table-column prop="period" label="学段" align="center" width="100" />
                     <el-table-column prop="subject" label="学科" align="center" width="100" />
+                    <el-table-column label="成绩" align="center" width="100">
+                        <template #default="scope">
+                            <p>{{ scope.row.score === -1 ? '-' : scope.row.score }}</p>
+                        </template>
+                    </el-table-column>
                     <!-- <el-table-column label="作品" align="center">
                         <template #default="scope">
                             <p>
@@ -197,7 +204,7 @@
 </template>
 
 <script setup>
-import { Search, Delete, Clock, CaretBottom, CaretRight, Avatar, Management, Notebook, Picture, Film, Paperclip, FolderRemove } from '@element-plus/icons-vue'
+import { Search, Delete, Clock, CaretBottom, CaretRight, Avatar, Management, Notebook, Picture, Film, Paperclip, FolderRemove, CloseBold } from '@element-plus/icons-vue'
 import { ElMessageBox, ElMessage, FormRules } from 'element-plus'
 import { getCurrentInstance, onMounted, reactive, ref, watch, watchEffect } from 'vue'
 
@@ -217,6 +224,8 @@ let workList = ref([])
 let workIndex = ref(0)
 // 展示作品信息
 let workInfo = ref({})
+// 作品信息
+let workInfoShow = ref({})
 let fileIndex = ref(0)
 let fileInfo = ref({})
 let previewFile = ref({})
@@ -231,8 +240,6 @@ let skModule = ref(true)
 // 作品列表弹出框
 let scoring = ref(false)
 
-// 作品信息
-let workInfoShow = reactive({})
 // workInfo = workList[0]
 
 getTaskList()
@@ -284,6 +291,17 @@ function getRuleScore(arr, workInfo, isRestore) {
     return arr
 }
 
+function checkNotScore(arr) {
+    let notScore = arr.find(item => {
+        let needBreak = undefined
+        if(item.children.length) {
+            needBreak = checkNotScore(item.children)
+        }
+        return needBreak || item.score === null
+    })
+    return notScore
+}
+
 function getUploadInfo(info) {
     if(!info?.files) {
         let params = {
@@ -293,9 +311,14 @@ function getUploadInfo(info) {
         }
         proxy.$api.getTaskList(params).then(res => {
             if(res?.upload) {
-                info.files = res.upload.files.map(item => {
+                info.files = []
+                res.upload.files.map(item => {
                     item.urlShow = actInfo.value.url + item.url + '?' + actInfo.value.sas
-                    return item
+                    if(item.type === 'video') {
+                        info.files.unshift(item)
+                    } else {
+                        info.files.push(item)
+                    }
                 })
                 workInfo.value = info
                 // changeFile(workInfo.value.files[0], 0)
@@ -312,19 +335,31 @@ function getUploadInfo(info) {
 
 function changeWork(type) {
     ElMessageBox.confirm(`当前分数不会保存,是否切换?`).then(() => {
+        workIndex.value = type ? workIndex.value + 1 : workIndex.value - 1
         if(ruleInfo.value.scoreDetail) {
             ruleInfo.value.trees = getRuleScore(ruleInfo.value.trees, workList.value[workIndex.value].detailScore, true)
         } else {
             reviewScore.value = null
         }
-        workIndex.value = type ? workIndex.value + 1 : workIndex.value - 1
         getUploadInfo(workList.value[workIndex.value])
     })
 }
 
 function saveScore() {
-    let totalScore = 0
-
+    let notScore = undefined
+    if(ruleInfo.value.scoreDetail) {
+        // 排查细项打分时,是否有遗漏(可以打0分)
+        notScore = checkNotScore(ruleInfo.value.trees)
+    } else {
+        notScore = reviewScore.value
+    }
+    if(notScore) {
+        ElMessage({
+            type: 'warning',
+            message: ruleInfo.value.scoreDetail ? '有细项未打分' : '未打分'
+        })
+        return
+    }
     let params = {
         grant_type: 'decide-score',
         activityId: taskList.value[taskIndex.value].activityId,
@@ -342,10 +377,30 @@ function saveScore() {
     } else {
         params.scoreData.score += reviewScore.value
     }
+    // 可以打0分
+    /* if(!params.scoreData.score) {
+        ElMessage({
+            type: 'warning',
+            message: '分数不能为0'
+        })
+        return
+    } */
     console.log(params);
     proxy.$api.getTaskList(params).then(res => {
         if(res.code === 200) {
-
+            ElMessage({
+                type: 'success',
+                message: '保存成功'
+            })
+            if(!workList.value[workIndex.value].detailScore.length) {
+                taskList.value[taskIndex.value].completeCount = res.completeCount
+            }
+            workList.value = res.contestTasks
+        } else {
+            ElMessage({
+                type: 'warning',
+                message: '保存失败'
+            })
         }
     })
 }
@@ -385,15 +440,21 @@ function changeFile(info, index) {
     }
 }
 
-
 // 选择将要打分作品
-let handleCurrentChange = (key, keyIndex) => {
-    workInfoShow = key
+function handleCurrentChange(key, keyIndex) {
+    workInfoShow.value = key
 }
+
 // 确认开始打分
-let startWork = () => {
-    // workInfo = workInfoShow
-    Object.assign(workInfo.value, workInfoShow)
+function startWork() {
+    workIndex.value = workList.value.findIndex(item => item.uploadId === workInfoShow.value.uploadId)
+    if(ruleInfo.value.scoreDetail) {
+        ruleInfo.value.trees = getRuleScore(ruleInfo.value.trees, workList.value[workIndex.value].detailScore, true)
+    } else {
+        reviewScore.value = null
+    }
+    workInfo.value = workInfoShow.value
+    getUploadInfo(workInfo.value)
     scoring.value = false
 }
 </script>
@@ -406,5 +467,9 @@ let startWork = () => {
     .el-upload-list__item-info {
         height: 20px;
     }
+    .el-tabs__content,
+    .el-tab-pane {
+        height: 97%;
+    }
 }
 </style>