Browse Source

Merge branch 'develop' into ZJ/develop-2022523

zhouj1203@hotmail.com 3 years ago
parent
commit
da7b70d05f
100 changed files with 15683 additions and 5717 deletions
  1. 57 49
      TEAMModelBI/ClientApp/src/view/areaServe/areamanage.vue
  2. 472 459
      TEAMModelBI/ClientApp/src/view/common/aside.vue
  3. 6 20
      TEAMModelBI/ClientApp/src/view/created/created.vue
  4. 1 1
      TEAMModelBI/ClientApp/src/view/home.vue
  5. 362 288
      TEAMModelBI/ClientApp/src/view/index/index.vue
  6. 868 878
      TEAMModelBI/ClientApp/src/view/schoolServe/school.vue
  7. 241 207
      TEAMModelBI/ClientApp/src/view/systemConfig/operate.vue
  8. 1341 1318
      TEAMModelBI/ClientApp/src/view/teachermanage/traitmanage.vue
  9. 43 19
      TEAMModelBI/Controllers/BIHome/OnLineController.cs
  10. 97 3
      TEAMModelBI/Controllers/BITest/TestController.cs
  11. 32 0
      TEAMModelBI/DI/BIAzureCosmosFactoryExtensions.cs
  12. 36 0
      TEAMModelBI/DI/BIAzureRedisFactoryExtensions.cs
  13. 37 0
      TEAMModelBI/DI/BIAzureServiceBusFactoryExtensions.cs
  14. 32 0
      TEAMModelBI/DI/BIAzureStorageFactoryExtensions.cs
  15. 32 4
      TEAMModelBI/Startup.cs
  16. 10 0
      TEAMModelBI/Tool/Context/BIConst.cs
  17. 24 0
      TEAMModelBI/appsettings.Development.json
  18. 18 0
      TEAMModelBI/appsettings.json
  19. 3 3
      TEAMModelOS.FunctionV4/HttpTrigger/IESHttpTrigger.cs
  20. 10 0
      TEAMModelOS.SDK/Models/Cosmos/Common/Inner/CodeValue.cs
  21. 9 7
      TEAMModelOS.SDK/Models/Cosmos/Common/LessonStudentRecord.cs
  22. 67 17
      TEAMModelOS.SDK/Models/Service/LessonService.cs
  23. 6 0
      TEAMModelOS/ClientApp/public/index.html
  24. 59 3
      TEAMModelOS/ClientApp/public/lang/en-US.js
  25. 59 3
      TEAMModelOS/ClientApp/public/lang/zh-CN.js
  26. 59 3
      TEAMModelOS/ClientApp/public/lang/zh-TW.js
  27. 6 4
      TEAMModelOS/ClientApp/public/web/viewer.html
  28. 4 0
      TEAMModelOS/ClientApp/src/api/serviceDriveAuth.js
  29. 26 3
      TEAMModelOS/ClientApp/src/assets/iconfont/demo_index.html
  30. 7 3
      TEAMModelOS/ClientApp/src/assets/iconfont/iconfont.css
  31. 1 1
      TEAMModelOS/ClientApp/src/assets/iconfont/iconfont.js
  32. 7 0
      TEAMModelOS/ClientApp/src/assets/iconfont/iconfont.json
  33. BIN
      TEAMModelOS/ClientApp/src/assets/iconfont/iconfont.ttf
  34. BIN
      TEAMModelOS/ClientApp/src/assets/iconfont/iconfont.woff
  35. BIN
      TEAMModelOS/ClientApp/src/assets/iconfont/iconfont.woff2
  36. BIN
      TEAMModelOS/ClientApp/src/assets/image/qrcode_en.png
  37. BIN
      TEAMModelOS/ClientApp/src/assets/image/qrcode_tw.png
  38. 5 3
      TEAMModelOS/ClientApp/src/common/BaseLayout.vue
  39. 9 3
      TEAMModelOS/ClientApp/src/components/dashboard/art/RightTop.vue
  40. 515 530
      TEAMModelOS/ClientApp/src/components/questionnaire/BaseQnForm.vue
  41. 114 0
      TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/Buzr.vue
  42. 7 6
      TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/ClassRecord.less
  43. 160 105
      TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/ClassRecord.vue
  44. 249 0
      TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/Exam.vue
  45. 283 0
      TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/ExamQu.vue
  46. 359 0
      TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/ExamTable.vue
  47. 116 0
      TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/Pick.vue
  48. 2 1
      TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/Receive.vue
  49. 256 0
      TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/ReceiveBack.vue
  50. 204 0
      TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/ScoreBarChart.vue
  51. 1 0
      TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/StuReceive.vue
  52. 230 0
      TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/myWorks.vue
  53. 529 0
      TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/newClassRecord.less
  54. 789 0
      TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/newClassRecord.vue
  55. 184 0
      TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/newDataCount.vue
  56. 729 747
      TEAMModelOS/ClientApp/src/components/vote/BaseVoteForm.vue
  57. 2 2
      TEAMModelOS/ClientApp/src/css/common-style.less
  58. 14 4
      TEAMModelOS/ClientApp/src/css/site.css
  59. 23 3
      TEAMModelOS/ClientApp/src/router/routes.js
  60. 4 4
      TEAMModelOS/ClientApp/src/static/Global.js
  61. 0 3
      TEAMModelOS/ClientApp/src/utils/blobTool.js
  62. 40 1
      TEAMModelOS/ClientApp/src/utils/public.js
  63. 19 3
      TEAMModelOS/ClientApp/src/view/Home.vue
  64. 172 11
      TEAMModelOS/ClientApp/src/view/auth/Product.vue
  65. 2 2
      TEAMModelOS/ClientApp/src/view/auth/Serial.vue
  66. 1 0
      TEAMModelOS/ClientApp/src/view/classmgt/ClassNotice.less
  67. 3 2
      TEAMModelOS/ClientApp/src/view/classmgt/ClassNotice.vue
  68. 1 0
      TEAMModelOS/ClientApp/src/view/classmgt/CreateNotice.vue
  69. 417 0
      TEAMModelOS/ClientApp/src/view/classrecord/ClassRecordNew.less
  70. 612 0
      TEAMModelOS/ClientApp/src/view/classrecord/ClassRecordNew.vue
  71. 1 0
      TEAMModelOS/ClientApp/src/view/classrecord/eventchart/Exam.vue
  72. 1 0
      TEAMModelOS/ClientApp/src/view/dashboard/Art.vue
  73. 5 1
      TEAMModelOS/ClientApp/src/view/dashboard/Research.vue
  74. 1 1
      TEAMModelOS/ClientApp/src/view/homepage/HomePage.less
  75. 12 7
      TEAMModelOS/ClientApp/src/view/homepage/HomePage.vue
  76. 975 971
      TEAMModelOS/ClientApp/src/view/homework/ManageHomeWork.vue
  77. 10 9
      TEAMModelOS/ClientApp/src/view/learnactivity/CreatePrivEva.vue
  78. 8 0
      TEAMModelOS/ClientApp/src/view/learnactivity/ExamMgt.less
  79. 66 4
      TEAMModelOS/ClientApp/src/view/learnactivity/ExamMgt.vue
  80. 1 1
      TEAMModelOS/ClientApp/src/view/learnactivity/ManualPaper.vue
  81. 166 0
      TEAMModelOS/ClientApp/src/view/mycourse/MyCourse.less
  82. 978 0
      TEAMModelOS/ClientApp/src/view/mycourse/MyCourse.vue
  83. 192 0
      TEAMModelOS/ClientApp/src/view/mycourse/exam/CreatePrivExam.less
  84. 711 0
      TEAMModelOS/ClientApp/src/view/mycourse/exam/CreatePrivExam.vue
  85. 69 0
      TEAMModelOS/ClientApp/src/view/mycourse/exam/Exam.less
  86. 390 0
      TEAMModelOS/ClientApp/src/view/mycourse/exam/Exam.vue
  87. 163 0
      TEAMModelOS/ClientApp/src/view/mycourse/exam/Grade.vue
  88. 95 0
      TEAMModelOS/ClientApp/src/view/mycourse/homework/Homework.less
  89. 153 0
      TEAMModelOS/ClientApp/src/view/mycourse/homework/Homework.vue
  90. 31 0
      TEAMModelOS/ClientApp/src/view/mycourse/notice/Create.less
  91. 247 0
      TEAMModelOS/ClientApp/src/view/mycourse/notice/Create.vue
  92. 86 0
      TEAMModelOS/ClientApp/src/view/mycourse/notice/Notice.less
  93. 166 0
      TEAMModelOS/ClientApp/src/view/mycourse/notice/Notice.vue
  94. 65 0
      TEAMModelOS/ClientApp/src/view/mycourse/record/Record.less
  95. 256 0
      TEAMModelOS/ClientApp/src/view/mycourse/record/Record.vue
  96. 18 0
      TEAMModelOS/ClientApp/src/view/mycourse/student/Student.less
  97. 512 0
      TEAMModelOS/ClientApp/src/view/mycourse/student/Student.vue
  98. 68 0
      TEAMModelOS/ClientApp/src/view/mycourse/survey/Survey.less
  99. 154 0
      TEAMModelOS/ClientApp/src/view/mycourse/survey/Survey.vue
  100. 0 0
      TEAMModelOS/ClientApp/src/view/mycourse/vote/Vote.less

+ 57 - 49
TEAMModelBI/ClientApp/src/view/areaServe/areamanage.vue

@@ -2,19 +2,22 @@
   <div class="areamanabox">
     <div class="nowtitle">
       <div class="select-List">
+        <div class="site-box">
+          <el-select v-model="siteList.siteValue" placeholder="站点">
+            <el-option v-for="item in siteList.list" :key="item.id" :label="item.label" :value="item.value">
+            </el-option>
+          </el-select>
+        </div>
         <div class="province-box">
           <!-- <span>{{$t('areaManages.selector.provinceName')}}:</span> -->
-          <el-select v-model="provinceOptions.provinceValue" :placeholder="$t('areaManages.selector.provinceDefault')"
-            @change="areaSelctChange(provinceOptions.provinceValue, 'province')">
-            <el-option v-for="item in provinceOptions.optionInfo" :key="item.code" :label="item.name"
-              :value="item.name">
+          <el-select v-model="provinceOptions.provinceValue" :placeholder="$t('areaManages.selector.provinceDefault')" @change="areaSelctChange(provinceOptions.provinceValue, 'province')">
+            <el-option v-for="item in provinceOptions.optionInfo" :key="item.code" :label="item.name" :value="item.name">
             </el-option>
           </el-select>
         </div>
         <div class="city-box">
           <!-- <span>{{$t('areaManages.selector.cityName')}}:</span> -->
-          <el-select v-model="cityOptions.cityValue" :placeholder="$t('areaManages.selector.cityDefault')"
-            @change="areaSelctChange(cityOptions.cityValue, 'city')">
+          <el-select v-model="cityOptions.cityValue" :placeholder="$t('areaManages.selector.cityDefault')" @change="areaSelctChange(cityOptions.cityValue, 'city')">
             <el-option v-for="item in cityOptions.cityInfo" :key="item.code" :label="item.name" :value="item.name">
             </el-option>
           </el-select>
@@ -56,10 +59,8 @@
       </div>
     </div>
     <div class="traitfrom">
-      <el-table :data="optionData" style="width: 100%" :highlight-current-row="true" height="74vh" v-loading="loading"
-        :empty-text="$t(`commonMsg.nodataTable`)" element-loading-text="数据加载中...">
-        <el-table-column prop="index" :label="$t(`areaManages.areaTable.serialnum`)" type="index" sortable
-          align="center" />
+      <el-table :data="optionData" style="width: 100%" :highlight-current-row="true" height="74vh" v-loading="loading" :empty-text="$t(`commonMsg.nodataTable`)" element-loading-text="数据加载中...">
+        <el-table-column prop="index" :label="$t(`areaManages.areaTable.serialnum`)" type="index" sortable align="center" />
         <el-table-column prop="name" :label="$t(`areaManages.areaTable.name`)" align="center" />
         <el-table-column prop="schoolCount" label="区内学校数量" align="center" sortable :sort-method="areaSort" />
         <el-table-column prop="location" label="位置" align="center" />
@@ -79,13 +80,12 @@
         <el-tabs v-model="activeName">
           <el-tab-pane :label="$t(`areaManages.operational.areaAddSchool.title`)" name="add">
             <div class="haveSchool">
-              <div class="schoolLeft"  v-loading="loadingSchoolList" element-loading-text="数据加载中...">
+              <div class="schoolLeft" v-loading="loadingSchoolList" element-loading-text="数据加载中...">
                 <p>
                   {{ $t(`areaManages.operational.areaAddSchool.schooltitle`) }}
                 </p>
                 <ul>
-                  <li class="details-list-school" v-for="(item, index) in notjoinSchool" :key="item.id"
-                    :class="{ active: position === index }">
+                  <li class="details-list-school" v-for="(item, index) in notjoinSchool" :key="item.id" :class="{ active: position === index }">
                     <div class="list-school-logo">
                       <el-image :src="item.picture" fit="fill"> </el-image>
                     </div>
@@ -109,7 +109,7 @@
                       </div>
                     </div>
                   </li>
-                   <!-- <div class="noData" v-show="notjoinSchool.length ===0">
+                  <!-- <div class="noData" v-show="notjoinSchool.length ===0">
                    <div>{{ $t(`commonMsg.nodataTable`) }}</div>
                     </div> -->
                 </ul>
@@ -207,10 +207,8 @@
                 <el-table-column prop="index" :label="
                   $t(`areaManages.operational.abilitys.tables.serialnum`)
                 " width="180" type="index" align="center" />
-                <el-table-column prop="standardName" :label="$t(`areaManages.operational.abilitys.tables.name`)"
-                  width="180" align="center" />
-                <el-table-column prop="name" :label="$t(`areaManages.operational.abilitys.tables.source`)"
-                  align="center" />
+                <el-table-column prop="standardName" :label="$t(`areaManages.operational.abilitys.tables.name`)" width="180" align="center" />
+                <el-table-column prop="name" :label="$t(`areaManages.operational.abilitys.tables.source`)" align="center" />
                 <el-table-column prop="institution" :label="
                   $t(`areaManages.operational.abilitys.tables.affiliation`)
                 " align="center" />
@@ -318,8 +316,7 @@
                   $t(`commonMsg.closes`)
               }}</el-button>
               <!-- <el-button type="primary" @click="loadingForm.cutAbility=true" :loading="loadingForm.cutAbility">确认</el-button> -->
-              <el-button type="primary" @click="notarizeAbility(), (loadingForm.cutAbility = true)"
-                :loading="loadingForm.cutAbility">{{ $t(`commonMsg.confirm`) }}</el-button>
+              <el-button type="primary" @click="notarizeAbility(), (loadingForm.cutAbility = true)" :loading="loadingForm.cutAbility">{{ $t(`commonMsg.confirm`) }}</el-button>
             </span>
           </div>
         </template>
@@ -371,6 +368,14 @@ export default {
     let loadingForm = ref({
       cutAbility: false,
     })
+    //站点切换
+    let siteList = ref({
+      siteValue: 'china',
+      list: [
+        { id: 1, label: '中国站点', value: 'china' },
+        { id: 2, label: '国际站点', value: 'international' }
+      ]
+    })
     //地区列表
     let provinceOptions = ref({
       optionInfo: [],
@@ -383,9 +388,9 @@ export default {
     let originalDatas = ref([])
     //加入学校询问
     let addSchoolHint = ref(false)
-    let addmodels=ref('1')
-    let addSchoolitem=ref()
-    let loadingSchoolList=ref(true)
+    let addmodels = ref('1')
+    let addSchoolitem = ref()
+    let loadingSchoolList = ref(true)
     onMounted(() => {
       provinceOptions.value.optionInfo = optionsData
     })
@@ -407,7 +412,7 @@ export default {
     //table按钮
     async function operation (index, row, state) {
       console.log(row)
-      loadingSchoolList.value=true
+      loadingSchoolList.value = true
       currentlySelect.value = row
       let dialogData = JSON.parse(JSON.stringify(optionData.value))
       console.log(dialogData, '进来获取的几套方案')
@@ -436,7 +441,7 @@ export default {
     }
     //获取当前未加入区的所有学校
     function getNotjoin () {
-      loadingSchoolList.value=true
+      loadingSchoolList.value = true
       console.log(currentlySelect.value.id)
       proxy.$api
         .getNotjoinSchool({ areaId: currentlySelect.value.id })
@@ -444,26 +449,26 @@ export default {
           console.log(res, '获取未加入区域的学校')
           res.state === 200
             ? ((notjoinSchool.value = []),
-              notjoinSchool.value.push(...res.notAreaSchools),(loadingSchoolList.value=false))
+              notjoinSchool.value.push(...res.notAreaSchools), (loadingSchoolList.value = false))
             : ''
         })
     }
     //区域内添加学校
     function areaAddschool () {
       let user = JSON.parse(localStorage.getItem('userData'))
-      let modelsA=addmodels.value ==='1' ? false:addmodels.value ==='2' ? true:''
+      let modelsA = addmodels.value === '1' ? false : addmodels.value === '2' ? true : ''
       let data = {
         tmdId: user.tmdId,
         tmdName: user.tmdName,
         standard: currentlySelect.value.standard,
         areaId: currentlySelect.value.id,
         schoolCode: [addSchoolitem.value.schoolCode],
-        isDefault:modelsA,
+        isDefault: modelsA,
       }
       proxy.$api.areaAddSchool(data).then((res) => {
         console.log(res, '区域加入学校返回')
         res.state === 200
-          ? (ElMessage.success(proxy.$t(`commonMsg.addschoolSuccess`)),(addSchoolHint.value=false),
+          ? (ElMessage.success(proxy.$t(`commonMsg.addschoolSuccess`)), (addSchoolHint.value = false),
             getNotjoin(),
             getAreaschool())
           : ElMessage.error(proxy.$t(`commonMsg.addschoolError`))
@@ -556,13 +561,13 @@ export default {
     }
     //区域内移除学校
     function deleteRow (index, data) {
-      console.log(index, data,currentlySelect.value)
+      console.log(index, data, currentlySelect.value)
       let datas = data
       let user = JSON.parse(localStorage.getItem('userData'))
       let removehint = proxy.$t(`areaManages.operational.areaAddSchool.removeHint`)
       let removecautious = proxy.$t(`areaManages.operational.areaAddSchool.cautious`)
       let states
-      data.areaId === currentlySelect.value.id ? ((removehint='确认要将此学校移除学区并 清除 微能力点方案吗?'),(states=true)) :states=false
+      data.areaId === currentlySelect.value.id ? ((removehint = '确认要将此学校移除学区并 清除 微能力点方案吗?'), (states = true)) : states = false
       ElMessageBox({
         title: '注意:',
         message: h('p', null, [
@@ -577,9 +582,9 @@ export default {
             instance.confirmButtonLoading = true
             instance.confirmButtonText = 'Loading...'
             let data = {
-              areaId:currentlySelect.value.id,
+              areaId: currentlySelect.value.id,
               schoolId: datas.id,
-              isDefault:states,
+              isDefault: states,
             }
             proxy.$api.areaDeleSchool(data).then((res) => {
               console.log(res, '移除返回')
@@ -601,11 +606,11 @@ export default {
         })
       })
     }
-    function changeStyle (index,item) {
+    function changeStyle (index, item) {
       position.value = index
-      addSchoolitem.value=item
+      addSchoolitem.value = item
       console.log(addSchoolitem.value)
-      addSchoolHint.value=true
+      addSchoolHint.value = true
     }
     //地区选择
     function areaSelctChange (value, model) {
@@ -695,7 +700,8 @@ export default {
       addSchoolHint,
       addmodels,
       addSchoolitem,
-      loadingSchoolList
+      loadingSchoolList,
+      siteList
     }
   },
 }
@@ -960,12 +966,14 @@ export default {
 .province-box,
 .city-box,
 .dist-box,
-.close-box {
+.close-box,
+.site-box {
   display: inline-block;
 }
 
 .city-box,
-.dist-box {
+.dist-box,
+.province-box {
   margin-left: 1%;
 }
 
@@ -1000,13 +1008,13 @@ export default {
   overflow: hidden;
   margin-right: 5px;
 }
-.storageadd-type{
+.storageadd-type {
   text-align: center;
 }
-.school-hints-info{
-  font-size:16px;
+.school-hints-info {
+  font-size: 16px;
   text-align: center;
-  color:#636e72;
+  color: #636e72;
 }
 </style>
 <style>
@@ -1124,13 +1132,13 @@ export default {
 .schoolRight-table .el-image__inner {
   width: 75%;
 }
-.addschoolHints .el-dialog__title{
-  padding-left:15px;
+.addschoolHints .el-dialog__title {
+  padding-left: 15px;
 }
-.addschoolHints .el-dialog__footer{
-  text-align:center;
+.addschoolHints .el-dialog__footer {
+  text-align: center;
 }
-.areamanabox .el-tree{
-  background-color:#fff;
+.areamanabox .el-tree {
+  background-color: #fff;
 }
 </style>

File diff suppressed because it is too large
+ 472 - 459
TEAMModelBI/ClientApp/src/view/common/aside.vue


+ 6 - 20
TEAMModelBI/ClientApp/src/view/created/created.vue

@@ -737,27 +737,13 @@ export default {
                 // batchData.value = []; 
                  router.push({ path: '/home/schoolmanage' })
             }else if(res.state ===201){
-            for(let i in res.schools){
-                let ndata=res.schools[i]
-              let result= batchData.value.filter((item)=>{
-                return  ndata.name === item.name &&  ndata.type ===item.type
-              })
-              console.log(result)
-              if(result.length >0){
-                batchData.value[i].state =false
-              }else{
-                batchData.value[i].state =true
-              }
-                // for(let x in batchData.value){
-                //     batchData.value[x].state=true
-                //     console.log(ndata.name,batchData.value[x].name,ndata.type,batchData.value[x].type)
-                //     ndata.name === batchData.value[x].name &&  ndata.type ===batchData.value[x].type ? batchData.value[x].state=false:''
-                //     console.log(batchData.value[x].state)
-                // }
+              for(let i  in batchData.value){
+                 batchData.value[i].state=true
+                 res.schools.forEach((x)=>{
+                    x.name === batchData.value[i].name && x.type ===batchData.value[i].type ? batchData.value[i].state=false:''
+                 })
             }
-            //  batchData.value.forEach((x)=>{
-            //     x.state !==false ? x.state=true:''
-            //  })
+            
             ElMessage.warning('部分学校已存在,请不要重新创建!')
             // batchCreatedResult.value=batchData.value
             batchCreatedSchool.value=true

+ 1 - 1
TEAMModelBI/ClientApp/src/view/home.vue

@@ -89,7 +89,7 @@ li {
 .viewbox .el-sub-menu__icon-arrow {
     /* top: 53% !important; */
     font-size: 15px !important;
-    right: 7px;
+    right: 5%;
 }
 .viewbox .el-menu-vertical-demo {
     min-height: 5px;

+ 362 - 288
TEAMModelBI/ClientApp/src/view/index/index.vue

@@ -1,60 +1,63 @@
 <template>
   <!--首页(管理员以及研发页面)-->
-  <div class="statisticsbox-all" v-if="showPattern.includes('admin')">
-    <c-scrollbar ref="scrollbarRef" width="100%" height="100%" trigger="hover" direction="y">
-      <div class="headerinbox" v-if="screen.icon === '#icon-tuichuquanping-fill-copy'">
-        <div class="headerinbox-title">TEAM Model·BI 数据监控系统</div>
-        <div class="showTime" @click="detectionsize(screen.state)">
-          <svg class="skip" aria-hidden="true">
-            <use :xlink:href="screen.icon"></use>
-          </svg>
-        </div>
-      </div>
-      <div class="statisboxs">
-        <div class="top-resource">
-          <div class="basics-databox">
-            <div class="top-aspects" v-for="(item, index) in areaAspectsData" :key="index">
-              <div :class="['left-top-icon']">
-                <svg class="top-header-icon" aria-hidden="true">
-                  <use :xlink:href="item.icon"></use>
+  <div class="eachSite">
+    <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick" type="card">
+      <el-tab-pane label="中国" name="first">
+        <div class="statisticsbox-all" v-if="showPattern.includes('admin')">
+          <c-scrollbar ref="scrollbarRef" width="100%" height="100%" trigger="hover" direction="y">
+            <div class="headerinbox" v-if="screen.icon === '#icon-tuichuquanping-fill-copy'">
+              <div class="headerinbox-title">TEAM Model·BI 数据监控系统</div>
+              <div class="showTime" @click="detectionsize(screen.state)">
+                <svg class="skip" aria-hidden="true">
+                  <use :xlink:href="screen.icon"></use>
                 </svg>
               </div>
-              <div class="right-top-text">
-                <p class="right-top-num">
-                  {{ item.num }}
-                <div class="right-top-num-increase" v-show="item.classname === 'online'">
-                  <div class="right-top-num-increase-teach"><span class="right-top-num-increase-teach">教师:</span><span>{{ item.teach }}</span>
-                  </div>
-                  <div class="right-top-num-increase-student"><span class="right-top-num-increase-teach">学生:</span><span>{{ item.student
+            </div>
+            <div class="statisboxs">
+              <div class="top-resource">
+                <div class="basics-databox">
+                  <div class="top-aspects" v-for="(item, index) in areaAspectsData" :key="index">
+                    <div :class="['left-top-icon']">
+                      <svg class="top-header-icon" aria-hidden="true">
+                        <use :xlink:href="item.icon"></use>
+                      </svg>
+                    </div>
+                    <div class="right-top-text">
+                      <p class="right-top-num">
+                        {{ item.num }}
+                      <div class="right-top-num-increase" v-show="item.classname === 'online'">
+                        <div class="right-top-num-increase-teach"><span class="right-top-num-increase-teach">教师:</span><span>{{ item.teach }}</span>
+                        </div>
+                        <div class="right-top-num-increase-student"><span class="right-top-num-increase-teach">学生:</span><span>{{ item.student
                                             }}</span>
-                  </div>
-                </div>
-                <div class="right-top-num-increase" v-if="(item.classname === 'datas' || item.classname === 'teach' || item.classname === 'student') && item.state === '1'">
-                  <span class="right-top-num-flag">+</span>
-                  <span class="right-top-num-nums">{{ item.increase }}</span>
-                </div>
-                <div class="right-top-num-reduce" v-else-if="(item.classname === 'datas' || item.classname === 'teach' || item.classname === 'student') && item.state === '2'">
-                  <span class="right-top-num-flag">-</span>
-                  <span class="right-top-num-nums">4</span>
-                </div>
-                </p>
-                <p class="right-top-title">
-                <div class="title-left">{{ item.title }}</div>
-                <!-- <div class="title-right">
+                        </div>
+                      </div>
+                      <div class="right-top-num-increase" v-if="(item.classname === 'datas' || item.classname === 'teach' || item.classname === 'student') && item.state === '1'">
+                        <span class="right-top-num-flag">+</span>
+                        <span class="right-top-num-nums">{{ item.increase }}</span>
+                      </div>
+                      <div class="right-top-num-reduce" v-else-if="(item.classname === 'datas' || item.classname === 'teach' || item.classname === 'student') && item.state === '2'">
+                        <span class="right-top-num-flag">-</span>
+                        <span class="right-top-num-nums">4</span>
+                      </div>
+                      </p>
+                      <p class="right-top-title">
+                      <div class="title-left">{{ item.title }}</div>
+                      <!-- <div class="title-right">
                             <p><span>较昨日增长</span><span class="last-increase">4.4%</span></p>
                             <p><span>较上月增长</span><span class="month-increase">0.3%</span></p>
                         </div> -->
-                </p>
-              </div>
-            </div>
-          </div>
-          <div class="basicsbox">
-            <div class="leftbox">
-              <!-- <div :class="[items.type==='month' ? 'alonebox' :'totalalonebox']" v-for="(items,indexs) in activityData.total" :key="indexs">
+                      </p>
+                    </div>
+                  </div>
+                </div>
+                <div class="basicsbox">
+                  <div class="leftbox">
+                    <!-- <div :class="[items.type==='month' ? 'alonebox' :'totalalonebox']" v-for="(items,indexs) in activityData.total" :key="indexs">
                             <p :class="[items.type ==='month' ? 'alonebox-title':'total-alonebox-title']">{{items.title}}</p>
                             <p :class="[items.type ==='month' ? 'alonebox-content':'total-alonebox-content']">{{items.num}}</p>
                         </div> -->
-              <!-- <div class="commonbox-title">
+                    <!-- <div class="commonbox-title">
                                 <div class="commonbox-title-name">在线人数趋势</div>
                                 <div class="commonbox-title-icon">
                                     <svg class="commonbox-title-onlineIcon" aria-hidden="true">
@@ -62,28 +65,28 @@
                                     </svg>
                                 </div>
                             </div> -->
-              <!-- <div class="leftbox-online" v-loading="loading.basics" element-loading-background="rgba(0, 0, 0, 0.2)"> -->
-              <!-- <Online ></Online> -->
-              <!-- <CommonLine :lineData="totalArea.online"></CommonLine> -->
-              <!-- </div> -->
-            </div>
-            <div class="center">
-              <div class="commonbox-title">
-                <!-- <div class="commonbox-title-name">在线人员区分</div> -->
-                <div class="commonbox-title-icon">
-                  <svg class="commonbox-title-onlineIcon" aria-hidden="true">
-                    <use xlink:href="#icon-keliuqushi"></use>
-                  </svg>
-                </div>
-                <div class="commonbox-title-name">在线人数趋势</div>
-              </div>
-              <div class="leftbox-online" v-loading="loading.basics" element-loading-background="rgba(0, 0, 0, 0.2)">
-                <CommonBar :barData="totalArea.onlineType"></CommonBar>
-              </div>
-            </div>
-            <div class="rightbox" v-loading="loading.basics2" element-loading-background="rgba(0, 0, 0, 0.2)">
-              <!-- <p class="online-title">在线量统计</p> -->
-              <!-- <div class="rightbox-pie">
+                    <!-- <div class="leftbox-online" v-loading="loading.basics" element-loading-background="rgba(0, 0, 0, 0.2)"> -->
+                    <!-- <Online ></Online> -->
+                    <!-- <CommonLine :lineData="totalArea.online"></CommonLine> -->
+                    <!-- </div> -->
+                  </div>
+                  <div class="center">
+                    <div class="commonbox-title">
+                      <!-- <div class="commonbox-title-name">在线人员区分</div> -->
+                      <div class="commonbox-title-icon">
+                        <svg class="commonbox-title-onlineIcon" aria-hidden="true">
+                          <use xlink:href="#icon-keliuqushi"></use>
+                        </svg>
+                      </div>
+                      <div class="commonbox-title-name">在线人数趋势</div>
+                    </div>
+                    <div class="leftbox-online" v-loading="loading.basics" element-loading-background="rgba(0, 0, 0, 0.2)">
+                      <CommonBar :barData="totalArea.onlineType"></CommonBar>
+                    </div>
+                  </div>
+                  <div class="rightbox" v-loading="loading.basics2" element-loading-background="rgba(0, 0, 0, 0.2)">
+                    <!-- <p class="online-title">在线量统计</p> -->
+                    <!-- <div class="rightbox-pie">
                             <CommonPie :proportionData="totalArea.alonePie"></CommonPie>
                         </div>
                         <div class="rightbox-pie">
@@ -92,58 +95,58 @@
                         <div class="rightbox-pie versions">
                             <ConventionPie :pieData="totalArea.versions"></ConventionPie>
                         </div> -->
-              <div class="commonbox-title">
-                <div class="commonbox-title-icon beginclass">
-                  <svg class="commonbox-title-onlineIcon" aria-hidden="true">
-                    <use xlink:href="#icon-XX_061"></use>
-                  </svg>
-                </div>
-                <div class="commonbox-title-name">HiTeach开课</div>
-              </div>
-              <div class="rightbox-login">
-                <!-- <div class="rightbox-login-select">
+                    <div class="commonbox-title">
+                      <div class="commonbox-title-icon beginclass">
+                        <svg class="commonbox-title-onlineIcon" aria-hidden="true">
+                          <use xlink:href="#icon-XX_061"></use>
+                        </svg>
+                      </div>
+                      <div class="commonbox-title-name">HiTeach开课</div>
+                    </div>
+                    <div class="rightbox-login">
+                      <!-- <div class="rightbox-login-select">
                                 <span :class="[onlineModels==='all' ? 'checked':'','all']" @click="onlineModels='all'">全年</span>
                                 <span :class="[onlineModels==='month' ? 'checked':'','nowmonth']" @click="onlineModels='month'">本月</span>
                             </div>
                             <CommonBarLine v-if="onlineModels==='month'"></CommonBarLine>
                             <BarLine v-else-if="onlineModels === 'all'"></BarLine> -->
-                <CommonBar :barData="totalArea.classData"></CommonBar>
+                      <CommonBar :barData="totalArea.classData"></CommonBar>
+                    </div>
+                  </div>
+                </div>
               </div>
-            </div>
-          </div>
-        </div>
-        <div class="center-resource">
-          <!-- <div class="center-resource-left" v-loading="loading.lessonData" element-loading-background="rgba(0, 0, 0, 0.2)">
+              <div class="center-resource">
+                <!-- <div class="center-resource-left" v-loading="loading.lessonData" element-loading-background="rgba(0, 0, 0, 0.2)">
                     <ConventionPie :pieData="totalArea.class"></ConventionPie>
                 </div> -->
-          <div class="center-resource-right">
-            <p class="commonbox-title areaClass">
-            <div class="commonbox-title-icon dynamicbox">
-              <svg class="commonbox-title-onlineIcon" aria-hidden="true">
-                <use xlink:href="#icon-huoyue"></use>
-              </svg>
-            </div>
-            <div class="commonbox-title-name">开课及上传数据</div>
-            </p>
-            <!-- <CommonBar :barData="totalArea.classAndactivity"></CommonBar> -->
-            <div class="center-resource-line" v-loading="loading.active" element-loading-background="rgba(0, 0, 0, 0.2)">
-              <CommonLine :lineData="totalArea.dynamic"></CommonLine>
-            </div>
-            <div class="selectboxs">
-              <el-select v-model="optionsData.values" class="m-2" placeholder="Select" size="small" @change="timeChange">
-                <el-option v-for="item in optionsData.data" :key="item.value" :label="item.label" :value="item.value" />
-              </el-select>
-            </div>
-          </div>
-        </div>
-        <!-- <div class="bottom-resource">
+                <div class="center-resource-right">
+                  <p class="commonbox-title areaClass">
+                  <div class="commonbox-title-icon dynamicbox">
+                    <svg class="commonbox-title-onlineIcon" aria-hidden="true">
+                      <use xlink:href="#icon-huoyue"></use>
+                    </svg>
+                  </div>
+                  <div class="commonbox-title-name">开课及上传数据</div>
+                  </p>
+                  <!-- <CommonBar :barData="totalArea.classAndactivity"></CommonBar> -->
+                  <div class="center-resource-line" v-loading="loading.active" element-loading-background="rgba(0, 0, 0, 0.2)">
+                    <CommonLine :lineData="totalArea.dynamic"></CommonLine>
+                  </div>
+                  <div class="selectboxs">
+                    <el-select v-model="optionsData.values" class="m-2" placeholder="Select" size="small" @change="timeChange">
+                      <el-option v-for="item in optionsData.data" :key="item.value" :label="item.label" :value="item.value" />
+                    </el-select>
+                  </div>
+                </div>
+              </div>
+              <!-- <div class="bottom-resource">
                 <p class="commonbox-title active">课例活跃</p>
                 <div class="areabottom-resource" v-loading="loading.active" element-loading-background="rgba(0, 0, 0, 0.2)">
                     <CommonLine :lineData="totalArea.dynamic"></CommonLine>
                 </div>
             </div> -->
-        <div class="areaList">
-          <!-- <p class="commonbox-title arealists">区域列表</p>
+              <div class="areaList">
+                <!-- <p class="commonbox-title arealists">区域列表</p>
                 <div class="area-listinfo">
                     <div class="area-item" v-for="(item,index) in areaLists" :key="item.id">
                         <div class="area-item-list">
@@ -157,73 +160,77 @@
                         </div>
                     </div>
                 </div> -->
-          <div class="bottom-leftbox">
-            <div class="centerbox-right-leftbox">
-              <p class="commonbox-title usesize">
-              <div class="commonbox-title-icon versions-list">
-                <svg class="commonbox-title-onlineIcon sizeList" aria-hidden="true">
-                  <use xlink:href="#icon-guanfangbanben"></use>
-                </svg>
-              </div>
-              <div class="commonbox-title-name">版本占比</div>
-              </p>
-              <div class="commonbox-versions" v-loading="loading.versions" element-loading-background="rgba(0, 0, 0, 0.2)">
-                <div class="commonbox-versions-title">
-                  <p class="versions-title-name">基础版</p>
-                  <p class="versions-title-num">数量:
-                    <span>{{ versionsData.basics.num }}</span>
-                  </p>
-                </div>
-                <div class="commonbox-versions-chart">
-                  <CommonPie :proportionData="totalArea.pie1"></CommonPie>
-                </div>
+                <div class="bottom-leftbox">
+                  <div class="centerbox-right-leftbox">
+                    <p class="commonbox-title usesize">
+                    <div class="commonbox-title-icon versions-list">
+                      <svg class="commonbox-title-onlineIcon sizeList" aria-hidden="true">
+                        <use xlink:href="#icon-guanfangbanben"></use>
+                      </svg>
+                    </div>
+                    <div class="commonbox-title-name">版本占比</div>
+                    </p>
+                    <div class="commonbox-versions" v-loading="loading.versions" element-loading-background="rgba(0, 0, 0, 0.2)">
+                      <div class="commonbox-versions-title">
+                        <p class="versions-title-name">基础版</p>
+                        <p class="versions-title-num">数量:
+                          <span>{{ versionsData.basics.num }}</span>
+                        </p>
+                      </div>
+                      <div class="commonbox-versions-chart">
+                        <CommonPie :proportionData="totalArea.pie1"></CommonPie>
+                      </div>
 
-              </div>
-              <div class="commonbox-versions" v-loading="loading.versions" element-loading-background="rgba(0, 0, 0, 0.2)">
-                <div class="commonbox-versions-title">
-                  <p class="versions-title-name">标准版</p>
-                  <p class="versions-title-num">数量:
-                    <span>{{ versionsData.standard.num }}</span>
-                  </p>
-                </div>
-                <div class="commonbox-versions-chart">
-                  <CommonPie :proportionData="totalArea.pie2"></CommonPie>
-                </div>
+                    </div>
+                    <div class="commonbox-versions" v-loading="loading.versions" element-loading-background="rgba(0, 0, 0, 0.2)">
+                      <div class="commonbox-versions-title">
+                        <p class="versions-title-name">标准版</p>
+                        <p class="versions-title-num">数量:
+                          <span>{{ versionsData.standard.num }}</span>
+                        </p>
+                      </div>
+                      <div class="commonbox-versions-chart">
+                        <CommonPie :proportionData="totalArea.pie2"></CommonPie>
+                      </div>
 
-              </div>
-              <div class="commonbox-versions" v-loading="loading.versions" element-loading-background="rgba(0, 0, 0, 0.2)">
-                <div class="commonbox-versions-title">
-                  <p class="versions-title-name">专业版</p>
-                  <p class="versions-title-num">数量:
-                    <span>{{ versionsData.major.num }}</span>
-                  </p>
+                    </div>
+                    <div class="commonbox-versions" v-loading="loading.versions" element-loading-background="rgba(0, 0, 0, 0.2)">
+                      <div class="commonbox-versions-title">
+                        <p class="versions-title-name">专业版</p>
+                        <p class="versions-title-num">数量:
+                          <span>{{ versionsData.major.num }}</span>
+                        </p>
+                      </div>
+                      <div class="commonbox-versions-chart">
+                        <CommonPie :proportionData="totalArea.pie3"></CommonPie>
+                      </div>
+
+                    </div>
+                  </div>
                 </div>
-                <div class="commonbox-versions-chart">
-                  <CommonPie :proportionData="totalArea.pie3"></CommonPie>
+                <div class="bottom-rightbox">
+                  <p class="commonbox-title sizeinfo">
+                  <div class="commonbox-title-icon">
+                    <svg class="commonbox-title-onlineIcon" aria-hidden="true">
+                      <use xlink:href="#icon-32cunchuguanli"></use>
+                    </svg>
+                  </div>
+                  <div class="commonbox-title-name">空间使用</div>
+                  </p>
+                  <div class="bottom-rightbox-right" v-loading="loading.sizeType" element-loading-background="rgba(0, 0, 0, 0.2)">
+                    <Size :lineData="totalArea.sizePie" :sizenum="allSize"></Size>
+                  </div>
                 </div>
-
               </div>
             </div>
-          </div>
-          <div class="bottom-rightbox">
-            <p class="commonbox-title sizeinfo">
-            <div class="commonbox-title-icon">
-              <svg class="commonbox-title-onlineIcon" aria-hidden="true">
-                <use xlink:href="#icon-32cunchuguanli"></use>
-              </svg>
-            </div>
-            <div class="commonbox-title-name">空间使用</div>
-            </p>
-            <div class="bottom-rightbox-right" v-loading="loading.sizeType" element-loading-background="rgba(0, 0, 0, 0.2)">
-              <Size :lineData="totalArea.sizePie" :sizenum="allSize"></Size>
-            </div>
-          </div>
+          </c-scrollbar>
+          <!-- <div class="participationbox"><span>查看我参与的</span></div> -->
         </div>
-      </div>
-    </c-scrollbar>
-    <!-- <div class="participationbox"><span>查看我参与的</span></div> -->
+      </el-tab-pane>
+      <el-tab-pane label="国际" name="second">国际</el-tab-pane>
+    </el-tabs>
   </div>
-  <AssitInterface v-else-if="showPattern.includes('assist')"></AssitInterface>
+  <AssitInterface v-if="showPattern.includes('assist')"></AssitInterface>
 </template>
 <script>
 import { ref, getCurrentInstance, onMounted } from 'vue'
@@ -268,6 +275,7 @@ export default {
   setup () {
     let { proxy } = getCurrentInstance()
     const routers = useRouter()
+    const activeName = ref('first')
     let showPattern = ref([])
     let aspectsData = ref([
       { id: 1, title: '区内学校', num: 0, icon: '#icon-renshixuexiao', classname: 'school' },
@@ -388,8 +396,8 @@ export default {
       todayUpload: [],
       classinYesterday: [],
       yesterdayUpload: [],
-      alldays:[],
-      allmonths:[],
+      alldays: [],
+      allmonths: [],
       allyear: [],
     })
     let versionsData = ref({
@@ -1295,6 +1303,8 @@ export default {
             // alert('全屏')
             fullScreen()
             screen.value.icon = '#icon-tuichuquanping-fill-copy'
+            // let a = document.getElementsByClassName('statisticsbox-all')
+            // a[0].style.marginTop = '0px'
           } else {
             // alert('不全屏')
             exitFullscreen()
@@ -1354,9 +1364,9 @@ export default {
     }
     //获取在线人数的数据(成员和趋势图)
     function getOnlineData () {
-      let data = { hour: discrepancy }
+      // let data = { hour: discrepancy }
       proxy.$api
-        .getOnline(data)
+        .getOnline({})
         .then((res) => {
           console.log(res, '在线人数')
           if (res.state === 200) {
@@ -1366,45 +1376,59 @@ export default {
             let onlineStudent = res.stuDays.sort(function (a, b) { return a.key - b.key })
             let onlineTmdstudent = res.tmdDays.sort(function (a, b) { return a.key - b.key })
             let replaceTime = allTime.value
-            let hoursInfo=new Date().getHours()
+            let hoursInfo = new Date().getHours()
             console.log(hoursInfo)
-            if(onlineData.length ===0){
-                for (let i in replaceTime) {
+            if (onlineData.length === 0) {
+              for (let i in replaceTime) {
                 let nums = parseInt(i) + 1
                 replaceTime[i].value === hoursInfo ? replaceTime.splice(nums) : ''
               }
-              replaceTime.forEach((x)=>{
-                x.num=0;x.teach=0;x.student=0;x.tmdStudent=0
+              replaceTime.forEach((x) => {
+                x.num = 0; x.teach = 0; x.student = 0; x.tmdStudent = 0
+              })
+            } else {
+              //处理UTC时间差
+              res.allDays.forEach((item) => {
+                item.key + discrepancy > 24 ? item.key = (item.key + discrepancy) - 24 : item.key = item.key + discrepancy
+              })
+              res.tchDays.forEach((item) => {
+                item.key + discrepancy > 24 ? item.key = (item.key + discrepancy) - 24 : item.key = item.key + discrepancy
               })
-            }else{
-                for (let i in replaceTime) {
+              res.stuDays.forEach((item) => {
+                item.key + discrepancy > 24 ? item.key = (item.key + discrepancy) - 24 : item.key = item.key + discrepancy
+              })
+              res.tmdDays.forEach((item) => {
+                item.key + discrepancy > 24 ? item.key = (item.key + discrepancy) - 24 : item.key = item.key + discrepancy
+              })
+              console.log(res, '处理过后的时间')
+              for (let i in replaceTime) {
                 let nums = parseInt(i) + 1
                 replaceTime[i].value == onlineData[onlineData.length - 1].key ? replaceTime.splice(nums) : ''
               }
-                //在线人数趋势
-                onlineData.forEach((x) => {
-                  for (let s in replaceTime) {
-                    replaceTime[s].value === x.key ? (replaceTime[s].num = x.value) : ''
-                  }
-                })
-                //处理在线人员区分(教师)
-                onlineTeach.forEach((a) => {
-                  for (let s in replaceTime) {
-                    replaceTime[s].value === a.key ? (replaceTime[s].teach = a.value) : ''
-                  }
-                })
-                //处理在线人员区分(学生)
-                onlineStudent.forEach((u) => {
-                  for (let s in replaceTime) {
-                    replaceTime[s].value === u.key ? (replaceTime[s].student = u.value) : ''
-                  }
-                })
-                //处理在线人员区分(醍摩豆学生)
-                onlineTmdstudent.forEach((t) => {
-                  for (let s in replaceTime) {
-                    replaceTime[s].value === t.key ? (replaceTime[s].tmdStudent = t.value) : ''
-                  }
-                })
+              //在线人数趋势
+              onlineData.forEach((x) => {
+                for (let s in replaceTime) {
+                  replaceTime[s].value === x.key ? (replaceTime[s].num = x.value) : ''
+                }
+              })
+              //处理在线人员区分(教师)
+              onlineTeach.forEach((a) => {
+                for (let s in replaceTime) {
+                  replaceTime[s].value === a.key ? (replaceTime[s].teach = a.value) : ''
+                }
+              })
+              //处理在线人员区分(学生)
+              onlineStudent.forEach((u) => {
+                for (let s in replaceTime) {
+                  replaceTime[s].value === u.key ? (replaceTime[s].student = u.value) : ''
+                }
+              })
+              //处理在线人员区分(醍摩豆学生)
+              onlineTmdstudent.forEach((t) => {
+                for (let s in replaceTime) {
+                  replaceTime[s].value === t.key ? (replaceTime[s].tmdStudent = t.value) : ''
+                }
+              })
             }
             replaceTime.forEach((e) => {
               nowTimeQuantum.value.time.push(e.time)
@@ -1449,15 +1473,14 @@ export default {
                 replaceTime[e].value === t.key ? (replaceTime[e].teachClass = t.value) : ''
               }
             })
-            console.log(replaceTime,'345345')
+            console.log(replaceTime, '345345')
             replaceTime.forEach((q) => {
-              console.log(q)
               nowTimeQuantum.value.classTime.push(q.time)
               nowTimeQuantum.value.classinToday.push(q.schoolClass)
               nowTimeQuantum.value.todayUpload.push(q.teachClass)
               nowTimeQuantum.value.totalattendClass.push(parseInt(q.schoolClass))
             })
-            console.log(nowTimeQuantum.value.todayUpload,'999999999999999999')
+            console.log(nowTimeQuantum.value.todayUpload, '999999999999999999')
             //处理课例活跃数据(今日)
             // let classToday = nowTimeQuantum.value.schoolClass.map((index, item) => {
             //     return index + nowTimeQuantum.value.teachClass[item]
@@ -1856,35 +1879,35 @@ export default {
       header[0].style.display = 'block'
       screen.value.state = false
     }
-    function timeChange(val){
+    function timeChange (val) {
       console.log(val)
       var date = new Date();
-      if(val ==='now'){
-        let datas=[ '0:00',
-            '1:00',
-            '2:00',
-            '3:00',
-            '4:00',
-            '5:00',
-            '6:00',
-            '7:00',
-            '8:00',
-            '9:00',
-            '10:00',
-            '11:00',
-            '12:00',
-            '13:00',
-            '14:00',
-            '15:00',
-            '16:00',
-            '17:00',
-            '18:00',
-            '19:00',
-            '20:00',
-            '21:00',
-            '22:00',
-            '23:00',]
-            let serialDatas=[
+      if (val === 'now') {
+        let datas = ['0:00',
+          '1:00',
+          '2:00',
+          '3:00',
+          '4:00',
+          '5:00',
+          '6:00',
+          '7:00',
+          '8:00',
+          '9:00',
+          '10:00',
+          '11:00',
+          '12:00',
+          '13:00',
+          '14:00',
+          '15:00',
+          '16:00',
+          '17:00',
+          '18:00',
+          '19:00',
+          '20:00',
+          '21:00',
+          '22:00',
+          '23:00',]
+        let serialDatas = [
           {
             name: '上传课堂记录',
             type: 'bar',
@@ -2008,73 +2031,73 @@ export default {
             // data: nowTimeQuantum.value.classinYesterday,
             data: nowTimeQuantum.value.classinYesterday,
           },
-         ]
-         totalArea.value.dynamic.legend.data=['上传课堂记录','开课','昨日上传课堂记录','昨日开课']
-         totalArea.value.dynamic.xAxis.data=datas
-         totalArea.value.dynamic.series=serialDatas
-         totalArea.value.dynamic.series[0].data=nowTimeQuantum.value.todayUpload
-         totalArea.value.dynamic.series[1].data=nowTimeQuantum.value.classinToday
-         totalArea.value.dynamic.series[2].data=nowTimeQuantum.value.yesterdayUpload
-         totalArea.value.dynamic.series[3].data=nowTimeQuantum.value.classinYesterday
+        ]
+        totalArea.value.dynamic.legend.data = ['上传课堂记录', '开课', '昨日上传课堂记录', '昨日开课']
+        totalArea.value.dynamic.xAxis.data = datas
+        totalArea.value.dynamic.series = serialDatas
+        totalArea.value.dynamic.series[0].data = nowTimeQuantum.value.todayUpload
+        totalArea.value.dynamic.series[1].data = nowTimeQuantum.value.classinToday
+        totalArea.value.dynamic.series[2].data = nowTimeQuantum.value.yesterdayUpload
+        totalArea.value.dynamic.series[3].data = nowTimeQuantum.value.classinYesterday
         getClassLivelys()
-      }else if(val ==='allmonth'){
+      } else if (val === 'allmonth') {
         let month = date.getMonth() + 1
         console.log(month)
-        let xdata=[]
-        let dataupload=[]
-        let statClass=[]
-        for(let i=0;i<month;i++){
-          xdata.push(parseInt(i)+1+'月')
-          statClass.push(random(40,100))
-          dataupload.push(random(1,39))
+        let xdata = []
+        let dataupload = []
+        let statClass = []
+        for (let i = 0; i < month; i++) {
+          xdata.push(parseInt(i) + 1 + '月')
+          statClass.push(random(40, 100))
+          dataupload.push(random(1, 39))
         }
-        totalArea.value.dynamic.legend.data=['上传课堂记录','开课']
-        totalArea.value.dynamic.xAxis.data=xdata
-        totalArea.value.dynamic.series[0].data=dataupload
-        totalArea.value.dynamic.series[1].data=statClass
+        totalArea.value.dynamic.legend.data = ['上传课堂记录', '开课']
+        totalArea.value.dynamic.xAxis.data = xdata
+        totalArea.value.dynamic.series[0].data = dataupload
+        totalArea.value.dynamic.series[1].data = statClass
         totalArea.value.dynamic.series.splice(2)
         // nowTimeQuantum.value.todayUpload=dataupload
         // nowTimeQuantum.value.classinToday=statClass
         // nowTimeQuantum.value.yesterdayUpload=[]
         // nowTimeQuantum.value.classinYesterday=[]
         console.log(xdata)
-      }else if(val ==='month'){
+      } else if (val === 'month') {
         var day = date.getDate();
-        console.log(day,'日期')
-        let xdata=[]
-        let dataupload=[]
-        let statClass=[]
-         for(let i=0;i<day;i++){
+        console.log(day, '日期')
+        let xdata = []
+        let dataupload = []
+        let statClass = []
+        for (let i = 0; i < day; i++) {
           xdata.push(parseInt(i))
-          statClass.push(random(56,120))
-          dataupload.push(random(1,52))
+          statClass.push(random(56, 120))
+          dataupload.push(random(1, 52))
         }
-        totalArea.value.dynamic.legend.data=['上传课堂记录','开课']
-        totalArea.value.dynamic.xAxis.data=xdata
-       totalArea.value.dynamic.series[0].data=dataupload
-        totalArea.value.dynamic.series[1].data=statClass
+        totalArea.value.dynamic.legend.data = ['上传课堂记录', '开课']
+        totalArea.value.dynamic.xAxis.data = xdata
+        totalArea.value.dynamic.series[0].data = dataupload
+        totalArea.value.dynamic.series[1].data = statClass
         totalArea.value.dynamic.series.splice(2)
         // nowTimeQuantum.value.yesterdayUpload=[]
         // nowTimeQuantum.value.classinYesterday=[]
-      }else if(val ==='year'){
-        var year = date.getFullYear(); 
-        let xdata=['2020','2021','2022']
-        let dataupload=[]
-        let statClass=[]
-         for(let i=0;i<4;i++){
-          statClass.push(random(458,1200))
-          dataupload.push(random(200,434))
+      } else if (val === 'year') {
+        var year = date.getFullYear();
+        let xdata = ['2020', '2021', '2022']
+        let dataupload = []
+        let statClass = []
+        for (let i = 0; i < 4; i++) {
+          statClass.push(random(458, 1200))
+          dataupload.push(random(200, 434))
         }
-         totalArea.value.dynamic.legend.data=['上传课堂记录','开课']
-        totalArea.value.dynamic.xAxis.data=xdata
-        totalArea.value.dynamic.series[0].data=dataupload
-        totalArea.value.dynamic.series[1].data=statClass
+        totalArea.value.dynamic.legend.data = ['上传课堂记录', '开课']
+        totalArea.value.dynamic.xAxis.data = xdata
+        totalArea.value.dynamic.series[0].data = dataupload
+        totalArea.value.dynamic.series[1].data = statClass
         totalArea.value.dynamic.series.splice(2)
         // nowTimeQuantum.value.yesterdayUpload=[]
         // nowTimeQuantum.value.classinYesterday=[]
       }
     }
-    function random(min, max) { return Math.floor(Math.random() * (max - min)) + min; }
+    function random (min, max) { return Math.floor(Math.random() * (max - min)) + min; }
     getUseridentity()
     getAll()
     return {
@@ -2104,12 +2127,19 @@ export default {
       discrepancy,
       optionsData,
       timeChange,
-      random
+      random,
+      activeName
     }
   },
 }
 </script>
 <style scoped>
+.eachSite {
+  line-height: 20px;
+  background: url("../../assets/img/bg-index.jpg") no-repeat;
+  background-size: 100% 100%;
+  position: relative;
+}
 .statisticsbox,
 .statisticsbox-all {
   width: 100%;
@@ -2117,8 +2147,7 @@ export default {
   /* padding: 1% 1%; */
   line-height: 20px;
   position: relative;
-  background: url("../../assets/img/bg-index.jpg") no-repeat;
-  background-size: 100% 100%;
+  /* margin-top: 35px; */
 }
 
 .top-resource {
@@ -2892,21 +2921,66 @@ export default {
   height: 100%;
   background-color: #ccc;
 }
-.selectboxs{
+.selectboxs {
   position: absolute;
-  top:0%;
-  right:1%;
+  top: 0%;
+  right: 1%;
 }
 </style>
 <style>
 .statisticsbox-all .el-loading-spinner .circular {
   display: inline;
 }
-.selectboxs .el-input__inner{
-  padding-right:0px;
-  height:30px;
-  line-height:30px;
-  background-color:#dfe6e9;
-  border:0px;
+.selectboxs .el-input__inner {
+  padding-right: 0px;
+  height: 30px;
+  line-height: 30px;
+  background-color: #dfe6e9;
+  border: 0px;
+}
+/*处理header 切换*/
+.eachSite .el-tabs--card > .el-tabs__header {
+  border-bottom: 0px;
+}
+.eachSite .el-tabs--card > .el-tabs__header .el-tabs__nav {
+  border: 0px;
+  float: none;
+  height: 30px;
+}
+.eachSite .el-tabs--card > .el-tabs__header .el-tabs__item.is-active {
+  border-bottom-color: none;
+}
+.eachSite .el-tabs--card > .el-tabs__header .el-tabs__item:first-child {
+  border-left: 1px solid #ccc;
+  border-right: 1px solid #ccc;
+  border-bottom-left-radius: 15px;
+  height: 25px;
+  line-height: 25px;
+  background-color: rgba(223, 230, 233, 0.6);
+}
+.eachSite .el-tabs--card > .el-tabs__header .el-tabs__item:nth-child(2) {
+  border-left: 0px solid #ccc;
+  border-right: 1px solid #ccc;
+  border-bottom: 1px solid #ccc;
+  border-bottom-right-radius: 15px;
+  height: 25px;
+  line-height: 25px;
+  background-color: rgba(223, 230, 233, 0.6);
+}
+.eachSite .is-active {
+  color: #fff;
+  background: rgba(9, 132, 227, 0.3) !important;
+  border-left: 1px solid rgba(116, 185, 255, 1) !important;
+  border-right: 1px solid rgba(116, 185, 255, 1) !important;
+  border-bottom: 1px solid rgba(116, 185, 255, 1) !important;
+}
+.eachSite .el-tabs__header {
+  margin: 0px;
+}
+.eachSite .el-tabs__nav-prev {
+  display: none;
+}
+.eachSite .el-tabs__nav-wrap.is-scrollable {
+  padding: 0px;
 }
 </style>

File diff suppressed because it is too large
+ 868 - 878
TEAMModelBI/ClientApp/src/view/schoolServe/school.vue


+ 241 - 207
TEAMModelBI/ClientApp/src/view/systemConfig/operate.vue

@@ -1,258 +1,292 @@
 <template>
-    <div class="operatebox">
-        <div class="nowuser">
-            <p class="identitybox-title">当前操作用户:</p>
-            <div class="userlist">
-                <div class="photobox">
-                    <PersonalPhoto style="cursor: pointer;" :name="nowUsers.name" width="40px" height="40px" fontSize="12px" class="pigpicture" v-if="!nowUsers.picture"></PersonalPhoto>
-                    <el-image style="width: 40px; height: 40px;border-radius:50%" :src="nowUsers.picture" fit="fill" v-else></el-image>
-                </div>
-                <div class="userlist-name">{{nowUsers.name}}({{nowUsers.mobile}})</div>
-            </div>
+  <div class="operatebox">
+    <div class="nowuser">
+      <p class="identitybox-title">当前操作用户:</p>
+      <div class="userlist">
+        <div class="photobox">
+          <PersonalPhoto style="cursor: pointer;" :name="nowUsers.name" width="40px" height="40px" fontSize="12px" class="pigpicture" v-if="!nowUsers.picture"></PersonalPhoto>
+          <el-image style="width: 40px; height: 40px;border-radius:50%" :src="nowUsers.picture" fit="fill" v-else></el-image>
         </div>
-        <div class="identitybox">
-            <p class="identitybox-title">当前身份:</p>
-            <div class="identitybox-select">
-                <el-select v-model="identityValue" size="small" placeholder="当前身份" multiple @change="changeIdentity">
-                    <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
-                </el-select>
-            </div>
+        <div class="userlist-name">{{nowUsers.name}}({{nowUsers.mobile}})</div>
+      </div>
+    </div>
+    <div class="identitybox">
+      <p class="identitybox-title">当前身份:</p>
+      <div class="identitybox-select">
+        <el-select v-model="identityValue" size="small" placeholder="当前身份" multiple @change="changeIdentity">
+          <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
+        </el-select>
+      </div>
+    </div>
+    <div class="authoritybox">
+      <p class="identitybox-title">权限列表:</p>
+      <div class="authority-block" v-for="(item,index) in authorityList" :key="index">
+        <p class="authority-block-title">{{item.name}}</p>
+        <div class="authority-block-item" v-for="(items,indexs) in item.children" :key="indexs">
+          <div class="authority-block-item-name">{{items.name}}</div>
+          <div class="authority-block-item-state">
+            <el-switch v-model="items.state" @change="changeAuthority" />
+          </div>
         </div>
-        <div class="authoritybox">
-            <p class="identitybox-title">权限列表:</p>
-            <div class="authority-block" v-for="(item,index) in authorityList" :key="index">
-                <p class="authority-block-title">{{item.name}}</p>
-                <div class="authority-block-item" v-for="(items,indexs) in item.children" :key="indexs">
-                    <div class="authority-block-item-name">{{items.name}}</div>
-                    <div class="authority-block-item-state">
-                        <el-switch v-model="items.state" @change="changeAuthority" />
-                    </div>
-                </div>
-            </div>
+      </div>
+      <div class="authority-block">
+        <p class="authority-block-title">站点数据</p>
+        <div class="sitebox" v-for="item in siteList" :key="item.id">
+          <div class="sitebox-name">{{item.label}}</div>
+          <div class="sitebox-state">
+            <el-switch v-model="item.state" />
+            <!-- <el-switch v-model="item.state" class="ml-2" active-color="#13ce66" inactive-color="#ff4949" /> -->
+          </div>
         </div>
+      </div>
     </div>
+  </div>
 </template>
 <script>
 import { ref, watch, getCurrentInstance } from 'vue'
 import { ElMessage, ElLoading, ElMessageBox } from 'element-plus'
 export default {
-    emits: ['changeShow'],
-    props: {
-        userdata: {
-            type: Object,
-            default: () => {},
-        },
+  emits: ['changeShow'],
+  props: {
+    userdata: {
+      type: Object,
+      default: () => { },
     },
-    setup(props, context) {
-        console.log(props)
-        let { proxy } = getCurrentInstance()
-        const identityValue = ref([])
-        const options = [
-            {
-                value: 'channelcrew',
-                label: '渠道人员',
-            },
-            {
-                value: 'sellcrew',
-                label: '销售',
-            },
-            {
-                value: 'assist',
-                label: '顾问',
-            },
-            // {
-            //     value: 'research',
-            //     label: '研发部门人员',
-            // },
-            // {
-            //     value: 'leader',
-            //     label: '领导',
-            // },
-            {
-                value: 'admin',
-                label: '系统管理员',
-            },
-        ]
+  },
+  setup (props, context) {
+    console.log(props)
+    let { proxy } = getCurrentInstance()
+    const identityValue = ref([])
+    const options = [
+      {
+        value: 'channelcrew',
+        label: '渠道人员',
+      },
+      {
+        value: 'sellcrew',
+        label: '销售',
+      },
+      {
+        value: 'assist',
+        label: '顾问',
+      },
+      // {
+      //     value: 'research',
+      //     label: '研发部门人员',
+      // },
+      // {
+      //     value: 'leader',
+      //     label: '领导',
+      // },
+      {
+        value: 'admin',
+        label: '系统管理员',
+      },
+    ]
 
-        let authorityList = ref([
-            // { id: 1, name: '系统配置', children: [{ id: 1 - 1, name: '操作系统配置', key: '123456', state: false }] },
-            // {
-            //     id: 2,
-            //     name: '人员管理',
-            //     children: [
-            //         { id: 2 - 1, name: '查看人员名单信息', key: '123456', state: false },
-            //         { id: 2 - 2, name: '编辑人员名单信息', key: '123456', state: true },
-            //     ],
-            // },
-            {
-                id: 3,
-                name: '学区情况',
-                children: [
-                    { id: 3 - 1, name: '查看学区数据信息', key: 'areadata-read', state: false },
-                    { id: 3 - 2, name: '修改学区相关信息', key: 'areadata-upd', state: false },
-                ],
-            },
-            {
-                id: 4,
-                name: '学校情况',
-                children: [
-                    { id: 4 - 1, name: '查看学校数据信息', key: 'schooldata-read', state: false },
-                    { id: 4 - 2, name: '修改学校相关信息', key: 'schooldata-upd', state: false },
-                ],
-            },
-        ])
-        let nowUsers = ref()
-        let nowAuthority = ref([])
-        let nowIdentity = ref([])
-        function init() {
-            let userInfo = nowUsers.value.handlePermissions
-            console.log(userInfo)
-            for (let i in userInfo) {
-                let authorityName = userInfo[i]
-                for (let y in authorityList.value) {
-                    let listData = authorityList.value[y].children
-                    for (let e in listData) {
-                        listData[e].key === authorityName ? (listData[e].state = true) : ''
-                    }
-                }
-            }
-        }
-        function initialize() {
-            for (let y in authorityList.value) {
-                let listData = authorityList.value[y].children
-                listData.map((x) => (x.state = false))
-            }
-            for (let i in nowUsers.value.handleRoles) {
-                let role = nowUsers.value.handleRoles[i]
-                for (let u in options) {
-                    options[u].value === role ? identityValue.value.push(options[u].value) : ''
-                }
-            }
-            identityValue.value = [...new Set(identityValue.value)]
-            nowAuthority.value = []
-            nowIdentity.value = []
+    let authorityList = ref([
+      // { id: 1, name: '系统配置', children: [{ id: 1 - 1, name: '操作系统配置', key: '123456', state: false }] },
+      // {
+      //     id: 2,
+      //     name: '人员管理',
+      //     children: [
+      //         { id: 2 - 1, name: '查看人员名单信息', key: '123456', state: false },
+      //         { id: 2 - 2, name: '编辑人员名单信息', key: '123456', state: true },
+      //     ],
+      // },
+      {
+        id: 3,
+        name: '学区情况',
+        children: [
+          { id: 3 - 1, name: '查看学区数据信息', key: 'areadata-read', state: false },
+          { id: 3 - 2, name: '修改学区相关信息', key: 'areadata-upd', state: false },
+        ],
+      },
+      {
+        id: 4,
+        name: '学校情况',
+        children: [
+          { id: 4 - 1, name: '查看学校数据信息', key: 'schooldata-read', state: false },
+          { id: 4 - 2, name: '修改学校相关信息', key: 'schooldata-upd', state: false },
+        ],
+      },
+    ])
+    let siteList = ref([
+      { id: 1, label: '中国站点', state: true },
+      { id: 2, label: '国际站点', state: false }
+    ])
+    let nowUsers = ref()
+    let nowAuthority = ref([])
+    let nowIdentity = ref([])
+    function init () {
+      let userInfo = nowUsers.value.handlePermissions
+      console.log(userInfo)
+      for (let i in userInfo) {
+        let authorityName = userInfo[i]
+        for (let y in authorityList.value) {
+          let listData = authorityList.value[y].children
+          for (let e in listData) {
+            listData[e].key === authorityName ? (listData[e].state = true) : ''
+          }
         }
-        function changeIdentity(value) {
-            console.log(value)
-            nowIdentity.value = value
-            console.log(identityValue.value)
-            context.emit('changeShow', true)
+      }
+    }
+    function initialize () {
+      for (let y in authorityList.value) {
+        let listData = authorityList.value[y].children
+        listData.map((x) => (x.state = false))
+      }
+      for (let i in nowUsers.value.handleRoles) {
+        let role = nowUsers.value.handleRoles[i]
+        for (let u in options) {
+          options[u].value === role ? identityValue.value.push(options[u].value) : ''
         }
-        function changeAuthority(value) {
-            console.log(value)
-            for (let i in authorityList.value) {
-                let data = authorityList.value[i].children
-                for (let u in data) {
-                    data[u].state === true ? nowAuthority.value.push(data[u].key) : ''
-                }
-            }
-            context.emit('changeShow', true)
-            nowAuthority.value = [...new Set(nowAuthority.value)]
+      }
+      identityValue.value = [...new Set(identityValue.value)]
+      nowAuthority.value = []
+      nowIdentity.value = []
+    }
+    function changeIdentity (value) {
+      console.log(value)
+      nowIdentity.value = value
+      console.log(identityValue.value)
+      context.emit('changeShow', true)
+    }
+    function changeAuthority (value) {
+      console.log(value)
+      for (let i in authorityList.value) {
+        let data = authorityList.value[i].children
+        for (let u in data) {
+          data[u].state === true ? nowAuthority.value.push(data[u].key) : ''
         }
-        function notarizeChange() {
-            changeAuthority()
-            console.log('触发子组件的修改方法')
-            let rolesinfo = identityValue.value
-            let permissionsinfo = nowAuthority.value
-            let data = { partitionKey: nowUsers.value.partitionKey, userId: nowUsers.value.userId, tmdId: nowUsers.value.tmdId, permissions: permissionsinfo, roles: rolesinfo }
-            proxy.$api
-                .setRolesandPower(data)
-                .then((res) => {
-                    console.log(res, '变更返回')
-                    res.state === 200 ? (ElMessage.success('保存成功'), context.emit('changeShow', false)) : ElMessage.error('变更保存失败')
-                })
-                .catch((err) => {
-                    ElMessage.error('变更API保存失败')
-                })
-        }
-        watch(
-            props,
-            (newuser) => {
-                newuser ? (nowUsers.value = newuser.userdata) : ''
-                initialize(), init()
-            },
-            { immediate: true, deep: true }
-        )
-        return { identityValue, options, authorityList, nowUsers, init, changeIdentity, changeAuthority, notarizeChange }
-    },
+      }
+      context.emit('changeShow', true)
+      nowAuthority.value = [...new Set(nowAuthority.value)]
+    }
+    function notarizeChange () {
+      changeAuthority()
+      console.log('触发子组件的修改方法')
+      let rolesinfo = identityValue.value
+      let permissionsinfo = nowAuthority.value
+      let data = { partitionKey: nowUsers.value.partitionKey, userId: nowUsers.value.userId, tmdId: nowUsers.value.tmdId, permissions: permissionsinfo, roles: rolesinfo }
+      proxy.$api
+        .setRolesandPower(data)
+        .then((res) => {
+          console.log(res, '变更返回')
+          res.state === 200 ? (ElMessage.success('保存成功'), context.emit('changeShow', false)) : ElMessage.error('变更保存失败')
+        })
+        .catch((err) => {
+          ElMessage.error('变更API保存失败')
+        })
+    }
+    watch(
+      props,
+      (newuser) => {
+        newuser ? (nowUsers.value = newuser.userdata) : ''
+        initialize(), init()
+      },
+      { immediate: true, deep: true }
+    )
+    return { identityValue, options, authorityList, nowUsers, init, changeIdentity, changeAuthority, notarizeChange, siteList }
+  },
 }
 </script>
 <style scoped>
 .operatebox {
-    width: 100%;
-    line-height: 20px;
-    text-align: left;
+  width: 100%;
+  line-height: 20px;
+  text-align: left;
 }
 .identitybox {
-    padding: 1% 0% 4% 0%;
-    border-bottom: 1px solid #b2bec3;
+  padding: 1% 0% 4% 0%;
+  border-bottom: 1px solid #b2bec3;
 }
 .identitybox-title {
-    font-size: 14px;
-    color: #7f8fa6;
+  font-size: 14px;
+  color: #7f8fa6;
 }
 .identitybox-select {
-    width: 100%;
+  width: 100%;
 }
 .authoritybox {
-    width: 100%;
-    padding: 1% 0%;
+  width: 100%;
+  padding: 1% 0%;
 }
 .authority-block {
-    width: 100%;
-    padding: 1%;
-    line-height: 30px;
+  width: 100%;
+  padding: 1%;
+  line-height: 30px;
 }
 .authority-block-title {
-    font-size: 14px;
-    color: #b2bec3;
+  font-size: 14px;
+  color: #b2bec3;
 }
 .authority-block-item-name {
-    width: 67%;
-    display: inline-block;
-    vertical-align: top;
-    font-size: 16px;
-    padding-left: 3%;
-    color: #636e72;
+  width: 67%;
+  display: inline-block;
+  vertical-align: top;
+  font-size: 16px;
+  padding-left: 10px;
+  color: #636e72;
 }
 .authority-block-item-state {
-    width: 25%;
-    display: inline-block;
-    vertical-align: top;
-    text-align: right;
-    padding-right: 5%;
+  width: 25%;
+  display: inline-block;
+  vertical-align: top;
+  text-align: right;
+  padding-right: 5%;
 }
 .authority-block-item {
-    padding: 1% 0%;
+  padding: 1% 0%;
 }
 .userlist {
-    width: 100%;
-    text-align: center;
-    display: flex;
-    justify-content: center;
-    align-items: center;
+  width: 100%;
+  text-align: center;
+  display: flex;
+  justify-content: center;
+  align-items: center;
 }
 .userlist-name {
-    font-size: 14px;
-    color: #7f8c8d;
+  font-size: 14px;
+  color: #7f8c8d;
 }
 .photobox {
-    display: inline-block;
-    width: 20%;
-    vertical-align: top;
+  display: inline-block;
+  width: 20%;
+  vertical-align: top;
 }
 .userlist-name {
-    text-align: left;
-    display: inline-block;
-    width: 45%;
-    vertical-align: top;
-    line-height: 40px;
-    margin-left: 0%;
-    font-size: 18px;
+  text-align: left;
+  display: inline-block;
+  width: 45%;
+  vertical-align: top;
+  line-height: 40px;
+  margin-left: 0%;
+  font-size: 18px;
+}
+.sitebox {
+  display: inline-block;
+  vertical-align: top;
+  width: 50%;
+  border-right: 1px solid #ccc;
+}
+.sitebox:last-child {
+  border: 0px;
+}
+.sitebox-name,
+.sitebox-state {
+  display: inline-block;
+  vertical-align: top;
+}
+.sitebox-name {
+  width: 70%;
+  font-size: 16px;
+  padding-left: 10px;
+  color: #636e72;
 }
 </style>
 <style>
 .identitybox-select .el-select--small {
-    width: 50%;
+  width: 50%;
 }
 </style>

File diff suppressed because it is too large
+ 1341 - 1318
TEAMModelBI/ClientApp/src/view/teachermanage/traitmanage.vue


+ 43 - 19
TEAMModelBI/Controllers/BIHome/OnLineController.cs

@@ -8,6 +8,7 @@ using System.Linq;
 using System.Text.Json;
 using System.Threading.Tasks;
 using TEAMModelBI.Tool;
+using TEAMModelBI.Tool.Context;
 using TEAMModelOS.SDK.DI;
 using TEAMModelOS.SDK.Extension;
 using TEAMModelOS.SDK.Models;
@@ -35,10 +36,17 @@ namespace TEAMModelBI.Controllers.BIHome
         /// </summary>
         /// <returns></returns>
         [HttpPost("get-count")]
-        public async Task<IActionResult> GetCount() 
+        public async Task<IActionResult> GetCount(JsonElement jsonElement) 
         {
             var cosmosClient = _azureCosmos.GetCosmosClient();
             var table = _azureStorage.GetCloudTableClient().GetTableReference("IESLogin");
+            jsonElement.TryGetProperty("site", out JsonElement site);
+            if ($"{site}".Equals(BIConst.GlobalSite)) 
+            {
+                cosmosClient = _azureCosmos.GetCosmosClient(name: BIConst.GlobalSite);
+                table = _azureStorage.GetCloudTableClient(BIConst.GlobalSite).GetTableReference("IESLogin");
+            }
+
             DateTimeOffset dateTime = DateTimeOffset.UtcNow;
 
             var (daySt, dayEt) = TimeHelper.GetStartOrEnd(dateTime);  //今天开始时间    13位
@@ -105,13 +113,18 @@ namespace TEAMModelBI.Controllers.BIHome
         [HttpPost("get-trend")]
         public async Task<IActionResult> GetTrend(JsonElement jsonElement) 
         {
-            jsonElement.TryGetProperty("hour", out JsonElement hour);
+
             var table = _azureStorage.GetCloudTableClient().GetTableReference("IESLogin");
-            DateTimeOffset dateTime =  DateTimeOffset.UtcNow;
-            if (!string.IsNullOrEmpty($"{hour}")) 
+            var redisClinet = _azureRedis.GetRedisClient(8);
+            jsonElement.TryGetProperty("site", out JsonElement site);
+            if ($"{site}".Equals(BIConst.GlobalSite))
             {
-                DateTimeOffset.UtcNow.AddHours(hour.GetInt32());
+                table = _azureStorage.GetCloudTableClient(BIConst.GlobalSite).GetTableReference("IESLogin");
+                redisClinet = _azureRedis.GetRedisClient(dbnum: 8, name: BIConst.GlobalSite);
             }
+
+            DateTimeOffset dateTime =  DateTimeOffset.UtcNow;
+
             var (daySt, dayEt) = TimeHelper.GetStartOrEnd(dateTime);  //今天开始时间    13位
             var (strDaySt, strDayEt) = TimeHelper.GetUnixToDate(daySt, dayEt, "yyyyMMddHH");
             var dateDay = dateTime.ToString("yyyyMMdd"); //获取当天的日期
@@ -122,14 +135,14 @@ namespace TEAMModelBI.Controllers.BIHome
             Dictionary<long, int> stuDays = new();  //学生在线人数
             Dictionary<long, int> tmdDays = new();  //醍摩豆账户学生
 
-            SortedSetEntry[] tchDay = _azureRedis.GetRedisClient(8).SortedSetRangeByScoreWithScores($"Login:IES:teacher:{dateDay}");
+            SortedSetEntry[] tchDay = redisClinet.SortedSetRangeByScoreWithScores($"Login:IES:teacher:{dateDay}");
             if (tchDay.Length > 0)
             {
                 foreach (var item in tchDay)
                 {
                     int val = ((int)item.Score);
                     int key = ((int)item.Element);
-                    var utcTo = new DateTimeOffset(new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, hour.GetInt32() + key, 0, 0)).Hour;
+                    var utcTo = new DateTimeOffset(new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, key, 0, 0)).Hour;
                     //var hour = int.Parse(DateTime.SpecifyKind(Convert.ToDateTime($"{dateTime.Year}/{dateTime.Month}/{ dateTime.Day} {key}:00:00"), DateTimeKind.Utc).ToLocalTime().ToString("HH"));
                     tchDays.Add(utcTo, val);
                     if (allDays.ContainsKey(utcTo))
@@ -147,8 +160,8 @@ namespace TEAMModelBI.Controllers.BIHome
                 {
                     foreach (var item in hourLoginsTch)
                     {
-                        await _azureRedis.GetRedisClient(8).SortedSetIncrementAsync($"Login:IES:teacher:{dateDay}", $"{item.Hour}", item.Teacher);//存一天24小时
-                        var utcTo = new DateTimeOffset(new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, hour.GetInt32() + item.Hour, 0, 0)).Hour;
+                        await redisClinet.SortedSetIncrementAsync($"Login:IES:teacher:{dateDay}", $"{item.Hour}", item.Teacher);//存一天24小时
+                        var utcTo = new DateTimeOffset(new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, item.Hour, 0, 0)).Hour;
                         //var hour = int.Parse(DateTime.SpecifyKind(Convert.ToDateTime($"{dateTime.Year}/{dateTime.Month}/{ dateTime.Day} {item.Hour}:00:00"), DateTimeKind.Utc).ToLocalTime().ToString("HH"));
                         tchDays.Add(utcTo, item.Teacher);
                         if (allDays.ContainsKey(utcTo))
@@ -159,14 +172,14 @@ namespace TEAMModelBI.Controllers.BIHome
                 }
             }
 
-            SortedSetEntry[] stuDay = _azureRedis.GetRedisClient(8).SortedSetRangeByScoreWithScores($"Login:IES:student:{dateDay}");
+            SortedSetEntry[] stuDay = redisClinet.SortedSetRangeByScoreWithScores($"Login:IES:student:{dateDay}");
             if (stuDay.Length > 0)
             {
                 foreach (var item in stuDay)
                 {
                     int val = (int)item.Score;
                     int key = (int)item.Element;
-                    var utcTo = new DateTimeOffset(new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, hour.GetInt32() + key, 0, 0)).Hour;
+                    var utcTo = new DateTimeOffset(new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, key, 0, 0)).Hour;
                     //var hour = int.Parse(DateTime.SpecifyKind(Convert.ToDateTime($"{dateTime.Year}/{dateTime.Month}/{ dateTime.Day} {key}:00:00"), DateTimeKind.Utc).ToLocalTime().ToString("HH"));
                     stuDays.Add(utcTo, val);
                     if (allDays.ContainsKey(utcTo))
@@ -185,8 +198,8 @@ namespace TEAMModelBI.Controllers.BIHome
                 {
                     foreach (var item in hourLoginsStu)
                     {
-                        await _azureRedis.GetRedisClient(8).SortedSetIncrementAsync($"Login:IES:student:{dateDay}", $"{item.Hour}", item.Student);//存一天24小时
-                        var utcTo = new DateTimeOffset(new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, hour.GetInt32() + item.Hour, 0, 0)).Hour;
+                        await redisClinet.SortedSetIncrementAsync($"Login:IES:student:{dateDay}", $"{item.Hour}", item.Student);//存一天24小时
+                        var utcTo = new DateTimeOffset(new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, item.Hour, 0, 0)).Hour;
                         //var hour = int.Parse(DateTime.SpecifyKind(Convert.ToDateTime($"{dateTime.Year}/{dateTime.Month}/{ dateTime.Day} {item.Hour}:00:00"), DateTimeKind.Utc).ToLocalTime().ToString("HH"));
                         stuDays.Add(utcTo, item.Student);
                         if (allDays.ContainsKey(utcTo))
@@ -197,14 +210,14 @@ namespace TEAMModelBI.Controllers.BIHome
                 }
             }
 
-            SortedSetEntry[] tmdDay = _azureRedis.GetRedisClient(8).SortedSetRangeByScoreWithScores($"Login:IES:tmduser:{dateDay}");
+            SortedSetEntry[] tmdDay = redisClinet.SortedSetRangeByScoreWithScores($"Login:IES:tmduser:{dateDay}");
             if (tmdDay.Length > 0)
             {
                 foreach (var item in stuDay)
                 {
                     int val = (int)item.Score;
                     int key = (int)item.Element;
-                    var utcTo = new DateTimeOffset(new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, hour.GetInt32() + key, 00, 00)).Hour;
+                    var utcTo = new DateTimeOffset(new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, key, 00, 00)).Hour;
                     //var hour = int.Parse(DateTime.SpecifyKind(Convert.ToDateTime($"{dateTime.Year}/{dateTime.Month}/{ dateTime.Day} {key}:00:00"), DateTimeKind.Utc).ToLocalTime().ToString("HH"));
                     tmdDays.Add(utcTo, val);
                     if (allDays.ContainsKey(utcTo))
@@ -223,8 +236,8 @@ namespace TEAMModelBI.Controllers.BIHome
                 {
                     foreach (var item in hourLoginsTmd)
                     {
-                        await _azureRedis.GetRedisClient(8).SortedSetIncrementAsync($"Login:IES:tmduser:{dateDay}", $"{item.Hour}", item.TmdUser);//存一天24小时
-                        var utcTo = new DateTimeOffset(new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, hour.GetInt32() + item.Hour, 00, 00)).Hour;
+                        await redisClinet.SortedSetIncrementAsync($"Login:IES:tmduser:{dateDay}", $"{item.Hour}", item.TmdUser);//存一天24小时
+                        var utcTo = new DateTimeOffset(new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, item.Hour, 00, 00)).Hour;
                         //var hour = int.Parse(DateTime.SpecifyKind(Convert.ToDateTime($"{dateTime.Year}/{dateTime.Month}/{ dateTime.Day} {item.Hour}:00:00"), DateTimeKind.Utc).ToLocalTime().ToString("HH"));
                         tmdDays.Add(utcTo, item.TmdUser);
                         if (allDays.ContainsKey(utcTo))
@@ -243,10 +256,15 @@ namespace TEAMModelBI.Controllers.BIHome
         /// </summary>
         /// <returns></returns>
         [HttpPost("get-lessontrend")]
-        public async Task<IActionResult> GetLessonTrend()
+        public async Task<IActionResult> GetLessonTrend(JsonElement jsonElement)
         {
             DateTimeOffset dateTime = DateTimeOffset.UtcNow;
             var cosmosClient = _azureCosmos.GetCosmosClient();
+            jsonElement.TryGetProperty("site", out JsonElement site);
+            if ($"{site}".Equals(BIConst.GlobalSite))
+            {
+                cosmosClient = _azureCosmos.GetCosmosClient(name: BIConst.GlobalSite);
+            }
             int year = dateTime.Year;   //当前年
             int month = dateTime.Month;  //当前月
             int day = dateTime.Day;      //当天
@@ -316,9 +334,15 @@ namespace TEAMModelBI.Controllers.BIHome
         /// </summary>
         /// <returns></returns>
         [HttpPost("get-edition")]
-        public async Task<IActionResult> GetEdition() 
+        public async Task<IActionResult> GetEdition(JsonElement jsonElement) 
         {
             var cosmosClient = _azureCosmos.GetCosmosClient();
+            jsonElement.TryGetProperty("site", out JsonElement site);
+            if ($"{site}".Equals(BIConst.GlobalSite))
+            {
+                cosmosClient = _azureCosmos.GetCosmosClient(name: BIConst.GlobalSite);
+            }
+
             int beCnt = 0; //基础班
             int seCnt = 0; //标准版
             int peCnt = 0; //专业版

+ 97 - 3
TEAMModelBI/Controllers/BITest/TestController.cs

@@ -45,6 +45,7 @@ using System.Net.Http.Json;
 using System.Net;
 using TEAMModelBI.Tool.CosmosBank;
 using System.Diagnostics;
+using StackExchange.Redis;
 
 namespace TEAMModelBI.Controllers.BITest
 {
@@ -54,20 +55,22 @@ namespace TEAMModelBI.Controllers.BITest
     {
 
         private readonly AzureCosmosFactory _azureCosmos;
+        private readonly AzureStorageFactory _azureStorage;
+        private readonly AzureRedisFactory _azureRedis;
         private readonly DingDing _dingDing;
         private readonly Option _option;
-        private readonly AzureStorageFactory _azureStorage;
         private readonly IWebHostEnvironment _environment; //读取文件
         //读取配置文件
         private readonly IConfiguration _configuration;
         private readonly CoreAPIHttpService _coreAPIHttpService;
         private readonly HttpClient _httpClient;
 
-        public TestController(AzureCosmosFactory azureCosmos, DingDing dingDing, AzureStorageFactory azureStorage, IOptionsSnapshot<Option> option, IWebHostEnvironment hostingEnvironment, IConfiguration configuration, CoreAPIHttpService coreAPIHttpService, HttpClient httpClient)
+        public TestController(AzureCosmosFactory azureCosmos, AzureStorageFactory azureStorage, AzureRedisFactory azureRedis, DingDing dingDing, IOptionsSnapshot<Option> option, IWebHostEnvironment hostingEnvironment, IConfiguration configuration, CoreAPIHttpService coreAPIHttpService, HttpClient httpClient)
         {
             _azureCosmos = azureCosmos;
-            _dingDing = dingDing;
             _azureStorage = azureStorage;
+            _azureRedis = azureRedis;
+            _dingDing = dingDing;
             _option = option?.Value;
             _environment = hostingEnvironment;
             _configuration = configuration;
@@ -1263,7 +1266,98 @@ namespace TEAMModelBI.Controllers.BITest
             return Ok(new { state = 200, linqTests, set });
         }
 
+        /// <summary>
+        /// 多个连接字符串方式
+        /// </summary>
+        /// <returns></returns>
+        [HttpPost("get-manydb")]
+        public async Task<IActionResult> GetMany()
+        {
+
+            #region  依赖注入的方式 
+
+            DateTimeOffset dateTime = DateTimeOffset.UtcNow;
+            var dateDay = dateTime.ToString("yyyyMMdd"); //获取当天的日期
+            Dictionary<long, int> allDays = new();  //所有在线人数
+            Dictionary<long, int> tchDays = new();  //教师在线人数
+            SortedSetEntry[] tchDay = _azureRedis.GetRedisClient(8).SortedSetRangeByScoreWithScores($"Login:IES:teacher:{dateDay}");
+            if (tchDay.Length > 0)
+            {
+                foreach (var item in tchDay)
+                {
+                    int val = ((int)item.Score);
+                    int key = ((int)item.Element);
+                    var utcTo = new DateTimeOffset(new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, key, 0, 0)).Hour;
+                    //var hour = int.Parse(DateTime.SpecifyKind(Convert.ToDateTime($"{dateTime.Year}/{dateTime.Month}/{ dateTime.Day} {key}:00:00"), DateTimeKind.Utc).ToLocalTime().ToString("HH"));
+                    tchDays.Add(utcTo, val);
+                    if (allDays.ContainsKey(utcTo))
+                        allDays[utcTo] = (allDays[utcTo] + val);
+                    else
+                        allDays.Add(utcTo, val);
+                }
+            }
+            var redisGl = _azureRedis.GetRedisClient(dbnum: 0, name: "Global");
+
+            var temps= await _azureRedis.GetRedisClient(dbnum: 0, name: "Global").SortedSetIncrementAsync($"Login:IES:Test", $"1", 1);//一天24小时  小时为单位
+
+
 
+            var cosmosDefaulat = _azureCosmos.GetCosmosClient(); //默认数据库
+            List<School> schools = new();
+            await foreach (var item in cosmosDefaulat.GetContainer("TEAMModelOS", "School").GetItemQueryIterator<School>(queryText: "select value(c) from c", requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey("Base") }))
+            {
+                schools.Add(item);
+            }
+            var cosmosGlobal = _azureCosmos.GetCosmosClient(name: "Global"); //默认数据库
+            List<School> schoolGlobals = new();
+            await foreach (var item in cosmosGlobal.GetContainer("TEAMModelOS", "School").GetItemQueryIterator<School>(queryText: "select value(c) from c", requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey("Base") }))
+            {
+                schoolGlobals.Add(item);
+            }
+            return Ok(new { state = 200, dcnt = schools.Count, schools, dgcnt = schoolGlobals.Count, schoolGlobals });
+
+
+            ////table多库连接
+            //var tableDefulat = _azureStorage.GetCloudTableClient().GetTableReference("IESLogin");  //Table默认表
+            //List<DayLogin> hourLoginsDefualt = await tableDefulat.FindListByDict<DayLogin>(new Dictionary<string, object> { { "PartitionKey", $"DayLogin" } });
+
+            //var table = _azureStorage.GetCloudTableClient("Global").GetTableReference("IESLogin");  //Table国际站
+            //List<DayLogin> hourLogins= await  table.FindListByDict<DayLogin>(new Dictionary<string, object> { { "PartitionKey", $"DayLogin" } });
+
+            //return Ok(new { state = 200, hourLogins, hourLoginsDefualt });
+            #endregion
+
+            #region  原始方式
+            //cosmosDB连接字符串
+            //var clientUrl = _configuration.GetValue<string>("Azure:CosmosUrl:ConnectionString");
+            //var clientKey = _configuration.GetValue<string>("Azure:CosmosKey:ConnectionString");
+            //CosmosClient cosmosClient = new(clientUrl, clientKey);
+            //var response = await cosmosClient.GetContainer("TEAMModelOS", "School").ReadItemStreamAsync("hbcn", new PartitionKey("Base"));
+            //School school = new();
+            //if (response.Status == 200)
+            //{
+            //    using var json = await JsonDocument.ParseAsync(response.ContentStream);
+            //    school = json.ToObject<School>();
+            //}
+
+            //Table连接字符串
+            //var tableUrl = _configuration.GetValue<string>("Azure:StorageUrl:ConnectionString");
+            //Uri url = new(tableUrl, UriKind.Absolute);
+            //var tableKey = _configuration.GetValue<string>("Azure:StorageKey:ConnectionString");
+            //StorageCredentials sta = new StorageCredentials("teammodeltest", tableKey);
+            //CloudTableClient cloudTableClient = new CloudTableClient(url, sta);
+            //var table1 = cloudTableClient.GetTableReference("IESLogin");
+            //var tableGlobal = _configuration.GetValue<string>("AzureGlobal");
+
+            //Dictionary<string, object> dic = new() { { "PartitionKey", $"HourLogin" } };
+            //List<HourLogin> hourLogin = await table1.FindListByDict<HourLogin>(dic);
+
+            //List<HourLogin> hourLogin1= await table.FindListByDict<HourLogin>(dic);
+
+            //var table = _azureStorage.GetCloudTableClient().GetTableReference("IESLogin");
+            //return Ok(new { state = 200 }); 
+            #endregion
+        }
 
         public class linqTest
         {

+ 32 - 0
TEAMModelBI/DI/BIAzureCosmosFactoryExtensions.cs

@@ -0,0 +1,32 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using TEAMModelOS.SDK.DI;
+
+namespace TEAMModelBI.DI
+{
+    public static class BIAzureCosmosFactoryExtensions
+    {
+        /// <summary>
+        /// cosmosDB注入
+        /// </summary>
+        /// <param name="services"></param>
+        /// <param name="connectionInfo"></param>
+        /// <returns></returns>
+        /// <exception cref="ArgumentNullException"></exception>
+        public static IServiceCollection AddBIAzureCosmos(this IServiceCollection services, List<(string name, string connectionString)> connectionInfo)
+        {
+            if (services == null) throw new ArgumentNullException(nameof(services));
+            if (connectionInfo == null) throw new ArgumentNullException(nameof(connectionInfo));
+            services.TryAddSingleton<AzureCosmosFactory>();
+            connectionInfo.ForEach(connection =>
+            {
+                services.Configure<AzureCosmosFactoryOptions>(connection.name, o => { o.Name = connection.name; o.CosmosConnectionString = connection.connectionString; });
+            });  //多个数据库注入
+            //services.Configure<AzureCosmosFactoryOptions>(name, o => { o.Name = name; o.CosmosConnectionString = connectionString; });  //单个数据库注入
+            return services;
+        }
+    }
+}

+ 36 - 0
TEAMModelBI/DI/BIAzureRedisFactoryExtensions.cs

@@ -0,0 +1,36 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using TEAMModelOS.SDK.DI;
+
+namespace TEAMModelBI.DI
+{
+    public static class BIAzureRedisFactoryExtensions
+    {
+        /// <summary>
+        /// Redis数据库注入
+        /// </summary>
+        /// <param name="services"></param>
+        /// <param name="connectionInfos"></param>
+        /// <returns></returns>
+        /// <exception cref="ArgumentNullException"></exception>
+        public static IServiceCollection AddBIAzureRedis(this IServiceCollection services, List<(string name ,string  connectionString)> connectionInfos)
+        {
+            if (services == null) throw new ArgumentNullException(nameof(services));
+            if (connectionInfos == null) throw new ArgumentNullException(nameof(connectionInfos));
+
+            services.TryAddSingleton<AzureRedisFactory>();
+            //多个连接字符串注入
+            connectionInfos.ForEach(connection =>
+            {
+                services.Configure<AzureRedisFactoryOptions>(connection.name, o => { o.Name = connection.name; o.RedisConnectionString = connection.connectionString; });
+            });
+            ////单个连接字符注入
+            //services.Configure<AzureRedisFactoryOptions>(name, o => { o.Name = name; o.RedisConnectionString = connectionString; });
+
+            return services;
+        }
+    }
+}

+ 37 - 0
TEAMModelBI/DI/BIAzureServiceBusFactoryExtensions.cs

@@ -0,0 +1,37 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using TEAMModelOS.SDK.DI;
+
+namespace TEAMModelBI.DI
+{
+    public static class BIAzureServiceBusFactoryExtensions
+    {
+        /// <summary>
+        /// Function注入
+        /// </summary>
+        /// <param name="services"></param>
+        /// <param name="connectInfos"></param>
+        /// <returns></returns>
+        /// <exception cref="ArgumentNullException"></exception>
+        /// <exception cref="AccessViolationException"></exception>
+        public static IServiceCollection AddBIAzureServiceBus(this IServiceCollection services, List<(string name, string connectionString)> connectInfos) 
+        {
+            if (services == null) throw new ArgumentNullException(nameof(services));
+            if (connectInfos == null) throw new AccessViolationException(nameof(connectInfos));
+
+            services.TryAddSingleton<AzureServiceBusFactory>();
+
+            //多个注入
+            connectInfos.ForEach(connect => {
+                services.Configure<AzureServiceBusFactoryOptions>(connect.name, o => { o.Name = connect.name; o.ServiceBusConnectionString = connect.connectionString; });            
+            });
+
+            //services.Configure<AzureServiceBusFactoryOptions>(name, o => { o.Name = name; o.ServiceBusConnectionString = connectionString; });//单一注入
+
+            return services;
+        }
+    }
+}

+ 32 - 0
TEAMModelBI/DI/BIAzureStorageFactoryExtensions.cs

@@ -0,0 +1,32 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using TEAMModelOS.SDK.DI;
+
+namespace TEAMModelBI.DI
+{
+    public static class BIAzureStorageFactoryExtensions
+    {
+        /// <summary>
+        /// Storage依赖注入
+        /// </summary>
+        /// <param name="services"></param>
+        /// <param name="connectionInfo"></param>
+        /// <returns></returns>
+        /// <exception cref="ArgumentNullException"></exception>
+        public static IServiceCollection AddBIAzureStorage(this IServiceCollection services, List<(string name, string connectionString)> connectionInfo)
+        {
+            if (services == null) throw new ArgumentNullException(nameof(services));
+            if (connectionInfo == null) throw new ArgumentNullException(nameof(connectionInfo));
+
+            services.TryAddSingleton<AzureStorageFactory>();
+            connectionInfo.ForEach(connection =>
+            {
+                services.Configure<AzureStorageFactoryOptions>(connection.name, o => { o.Name = connection.name; o.StorageAccountConnectionString = connection.connectionString; });
+            });
+            return services;
+        }
+    }
+}

+ 32 - 4
TEAMModelBI/Startup.cs

@@ -15,6 +15,7 @@ using System.Collections.Generic;
 using System.IdentityModel.Tokens.Jwt;
 using System.Linq;
 using System.Threading.Tasks;
+using TEAMModelBI.DI;
 using TEAMModelBI.Models;
 using TEAMModelOS.Models;
 using TEAMModelOS.SDK;
@@ -87,10 +88,37 @@ namespace TEAMModelBI
                     .AllowAnyMethod();
                 });
             });
-            services.AddAzureStorage(Configuration.GetValue<string>("Azure:Storage:ConnectionString"));
-            services.AddAzureRedis(Configuration.GetValue<string>("Azure:Redis:ConnectionString"));
-            services.AddAzureCosmos(Configuration.GetValue<string>("Azure:Cosmos:ConnectionString"));
-            services.AddAzureServiceBus(Configuration.GetValue<string>("Azure:ServiceBus:ConnectionString"));
+
+            //Table和blob注入
+            List<(string name, string connectionString)> storageConnects = new();
+            storageConnects.Add(("Default", Configuration.GetValue<string>("Azure:Storage:ConnectionString")));      //大路站ClientString
+            storageConnects.Add(("Global", Configuration.GetValue<string>("GlobalAzure:Storage:ConnectionString"))); //国际站ClientString
+            services.AddBIAzureStorage(storageConnects);
+
+            //cosmosDB注入
+            List<(string name,string connectionString)> cosmosDBConnects = new();
+            cosmosDBConnects.Add(("Default", Configuration.GetValue<string>("Azure:Cosmos:ConnectionString")));      //大路站ClientString
+            cosmosDBConnects.Add(("Global", Configuration.GetValue<string>("GlobalAzure:Cosmos:ConnectionString"))); //国际站ClientString
+            services.AddBIAzureCosmos(cosmosDBConnects);
+
+            //redis注入
+            List<(string name, string connectionString)> redisConnects = new();
+            redisConnects.Add(("Default", Configuration.GetValue<string>("Azure:Redis:ConnectionString")));
+            redisConnects.Add(("Global", Configuration.GetValue<string>("GlobalAzure:Redis:ConnectionString")));
+            services.AddBIAzureRedis(redisConnects);
+
+            //serverBus 注入 
+            List<(string name, string connectionString)> funConnects = new();
+            funConnects.Add(("Default", Configuration.GetValue<string>("Azure:Redis:ConnectionString")));
+            funConnects.Add(("Global", Configuration.GetValue<string>("GlobalAzure:Redis:ConnectionString")));
+            services.AddBIAzureServiceBus(funConnects);
+            
+            //单一注入
+            //services.AddAzureStorage(Configuration.GetValue<string>("Azure:Storage:ConnectionString"));
+            //services.AddAzureCosmos(Configuration.GetValue<string>("Azure:Cosmos:ConnectionString"));
+            //services.AddAzureRedis(Configuration.GetValue<string>("Azure:Redis:ConnectionString"));
+            //services.AddAzureServiceBus(Configuration.GetValue<string>("Azure:ServiceBus:ConnectionString"));     
+
             services.AddSnowflakeId(Convert.ToInt64(Configuration.GetValue<string>("Option:LocationNum")), 1);
             services.AddHttpClient();
             services.AddHttpClient<DingDing>(); 

+ 10 - 0
TEAMModelBI/Tool/Context/BIConst.cs

@@ -0,0 +1,10 @@
+namespace TEAMModelBI.Tool.Context
+{
+    public class BIConst
+    {
+        /// <summary>
+        /// 国际站点
+        /// </summary>
+        public static readonly string GlobalSite = "Global";
+    }
+}

+ 24 - 0
TEAMModelBI/appsettings.Development.json

@@ -19,6 +19,7 @@
     "HttpTrigger": "https://teammodelosfunction-test.chinacloudsites.cn/api/"
     //"HttpTrigger": "http://localhost:7071/api/"
   },
+  //大陆站连接字符串
   "Azure": {
     "Storage": {
       "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=teammodeltest;AccountKey=O2W2vadCqexDxWO+px+QK7y1sHwsYj8f/WwKLdOdG5RwHgW/Dupz9dDUb4c1gi6ojzQaRpFUeAAmOu4N9E+37A==;EndpointSuffix=core.chinacloudapi.cn"
@@ -26,6 +27,29 @@
     "Cosmos": {
       "ConnectionString": "AccountEndpoint=https://cdhabookdep-free.documents.azure.cn:443/;AccountKey=JTUVk92Gjsx17L0xqxn0X4wX2thDPMKiw4daeTyV1HzPb6JmBeHdtFY1MF1jdctW1ofgzqkDMFOtcqS46by31A==;"
     },
+    "CosmosUrl": {
+      "ConnectionString": "https://cdhabookdep-free.documents.azure.cn:443/"
+    },
+    "CosmosKey": {
+      "ConnectionString": "JTUVk92Gjsx17L0xqxn0X4wX2thDPMKiw4daeTyV1HzPb6JmBeHdtFY1MF1jdctW1ofgzqkDMFOtcqS46by31A=="
+    },
+    "Redis": {
+      "ConnectionString": "52.130.252.100:6379,password=habook,ssl=false,abortConnect=False,writeBuffer=10240"
+    },
+    "ServiceBus": {
+      "ConnectionString": "Endpoint=sb://teammodelos.servicebus.chinacloudapi.cn/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=Sy4h4EQ8zP+7w/lOLi1X3tGord/7ShFHimHs1vC50Dc=",
+      "ActiveTask": "dep-active-task",
+      "ItemCondQueue": "dep-itemcond"
+    }
+  },
+  //国际站连接字符串  暂时是本地
+  "GlobalAzure": {
+    "Storage": {
+      "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=teammodelstorage;AccountKey=Yq7D4dE6cFuer2d2UZIccTA/i0c3sJ/6ITc8tNOyW+K5f+/lWw9GCos3Mxhj47PyWQgDL8YbVD63B9XcGtrMxQ==;EndpointSuffix=core.chinacloudapi.cn" // 之前未删除
+    },
+    "Cosmos": {
+      "ConnectionString": "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==" //本地
+    },
     "Redis": {
       "ConnectionString": "52.130.252.100:6379,password=habook,ssl=false,abortConnect=False,writeBuffer=10240"
     },

+ 18 - 0
TEAMModelBI/appsettings.json

@@ -25,6 +25,7 @@
     "HttpTrigger": "https://teammodelosfunction-test.chinacloudsites.cn/api/"
     //"HttpTrigger": "http://localhost:7071/api/"
   },
+  //大陆站连接字符串
   "Azure": {
     //
     "Storage": {
@@ -47,6 +48,23 @@
       "ItemCondQueue": "dep-itemcond" //队列消息
     }
   },
+  //国际站连接字符串
+  "GlobalAzure": {
+    "Storage": {
+      "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=teammodelstorage;AccountKey=Yq7D4dE6cFuer2d2UZIccTA/i0c3sJ/6ITc8tNOyW+K5f+/lWw9GCos3Mxhj47PyWQgDL8YbVD63B9XcGtrMxQ==;EndpointSuffix=core.chinacloudapi.cn" // 之前未删除
+    },
+    "Cosmos": {
+      "ConnectionString": "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==" //本地
+    },
+    "Redis": {
+      "ConnectionString": "52.130.252.100:6379,password=habook,ssl=false,abortConnect=False,writeBuffer=10240"
+    },
+    "ServiceBus": {
+      "ConnectionString": "Endpoint=sb://teammodelos.servicebus.chinacloudapi.cn/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=Sy4h4EQ8zP+7w/lOLi1X3tGord/7ShFHimHs1vC50Dc=",
+      "ActiveTask": "dep-active-task",
+      "ItemCondQueue": "dep-itemcond"
+    }
+  },
   "HaBookAuth": {
     "CoreId": {
       "userinfo": "https://api2.teammodel.cn/Oauth2/GetUserInfos"

+ 3 - 3
TEAMModelOS.FunctionV4/HttpTrigger/IESHttpTrigger.cs

@@ -215,7 +215,7 @@ namespace TEAMModelOS.FunctionV4.HttpTrigger
                         {
                             Student student = await cosmosClient.GetContainer("TEAMModelOS", Constant.Student).ReadItemAsync<Student>(id, new PartitionKey($"Base-{school}"));
                             student.loginInfos = new List<LoginInfo>() { new LoginInfo { expire = Expire, ip = ip, time = now } };
-                            await cosmosClient.GetContainer("TEAMModelOS", Constant.Student).ReplaceItemAsync<Student>(student, student.id, new PartitionKey("Base"));
+                            await cosmosClient.GetContainer("TEAMModelOS", Constant.Student).ReplaceItemAsync<Student>(student, student.id, new PartitionKey($"Base-{school}"));
                         }
                         catch { }
                         break;
@@ -466,10 +466,10 @@ namespace TEAMModelOS.FunctionV4.HttpTrigger
                     }
 
                     string tbScHourSql = $"PartitionKey eq 'HourLogin-{school}' and RowKey le '{delTbHour}'";
-                    await table.DeleteStringWhere<BIOptLog>(rowKey: tbHourSql); //删除学校168小时前的数据
+                    await table.DeleteStringWhere<BIOptLog>(rowKey: tbScHourSql); //删除学校168小时前的数据
 
                     string tbScDaySql = $"PartitionKey eq 'DayLogin-{school}' and RowKey le '{delTbDay}'";
-                    await table.DeleteStringWhere<BIOptLog>(rowKey: tbDaySql); //删除学校180天前的数据
+                    await table.DeleteStringWhere<BIOptLog>(rowKey: tbScDaySql); //删除学校180天前的数据
                 }
 
                 await response.WriteAsJsonAsync(new { data = json });

+ 10 - 0
TEAMModelOS.SDK/Models/Cosmos/Common/Inner/CodeValue.cs

@@ -9,4 +9,14 @@ namespace TEAMModelOS.SDK.Models
         public string code { get; set; }
         public string value { get; set; }
     }
+    public class IdSchool
+    {
+        public string id { get; set; }
+        public string school { get; set; }
+    }
+    public class IdCode
+    {
+        public string id { get; set; }
+        public string code { get; set; }
+    }
 }

+ 9 - 7
TEAMModelOS.SDK/Models/Cosmos/Common/LessonStudentRecord.cs

@@ -7,21 +7,23 @@ using System.Threading.Tasks;
 namespace TEAMModelOS.SDK.Models.Cosmos.Common
 {
     /// <summary>
-    /// 课堂记录简要信息。
+    /// 学生课堂记录简要信息。
     /// </summary>
     public class LessonStudentRecord : CosmosEntity
     {
-        // id 学生id、醍摩豆id  ,
-        //code  LessonStudentRecord[-学校编码],
+        // id 学生id、                            醍摩豆id  ,
+        //code  LessonStudentRecord[-学校编码],  LessonStudentRecord
         //其他基础信息
         public LessonStudentRecord() {
             pk = "LessonStudentRecord";
         }
-        public List<StudentRecord> records { get; set; } = new List<StudentRecord>();
+       // public List<StudentRecord> records { get; set; } = new List<StudentRecord>();
         //单独记录 组计分,个人积分,互动分,
-        public double groupScore { get; set; }
-        public double personScore { get; set; }
-        public double tnteractScore { get; set; }
+        public List<double> groupScore { get; set; }= new List<double>();
+        public List<double> personScore { get; set; } = new List<double>();
+        public List<double> tnteractScore { get; set; } = new List<double>();
+        public List<IdSchool> schoolRecord { get; set; } = new List<IdSchool>();
+        public HashSet<string> privateRecord { get; set; } = new HashSet<string>();
     }
 
 

+ 67 - 17
TEAMModelOS.SDK/Models/Service/LessonService.cs

@@ -20,13 +20,18 @@ namespace TEAMModelOS.SDK.Models.Service
         public static async void DoLessonStudentRecord(LessonRecord lessonRecord, string scope, CosmosClient client, string school, string tmdid,
             Teacher teacher, NotificationService _notificationService, AzureServiceBusFactory _serviceBus, AzureStorageFactory _azureStorage, IConfiguration _configuration, LessonBase lessonBase)
         {
-            var stuids = lessonBase.student.Where(x => x.type == 2).Select(x => new IdNameCode { id = x.id ,code=x.school});
+            var clientSummaryList =lessonBase.report.clientSummaryList.Where(x => x.groupTaskCompleteCount!=0 ||  x.groupScore != 0 || x.score != 0 || x.tnteractScore != 0 || x.taskCompleteCount!=0);
+            IEnumerable<LessonStudent> students = new List<LessonStudent>();
+            if (clientSummaryList.Any()) {
+                students= lessonBase.student.Where(x => clientSummaryList.Select(x => x.seatID).Contains(x.seatID));
+            }
+            var stuids = students.Where(x => x.type == 2);
             if (stuids.Any()) {
                 stuids.ToList().ForEach(x => {
-                    x.code = string.IsNullOrWhiteSpace(x.code) ? school : x.code;
+                    x.school = string.IsNullOrWhiteSpace(x.school) ? school : x.school;
                 });
             }
-            var groups= stuids.Where(z=>!string.IsNullOrWhiteSpace(z.code)).GroupBy(x => x.code).Select(y=>new { code=y.Key,list=y.ToList()});
+            var groups= stuids.Where(z=>!string.IsNullOrWhiteSpace(z.school)).GroupBy(x => x.school).Select(y=>new { code=y.Key,list=y.ToList()});
             List<LessonStudentRecord> lessonStudentRecords = new List<LessonStudentRecord>();
             foreach (var group in groups) {
                
@@ -36,7 +41,7 @@ namespace TEAMModelOS.SDK.Models.Service
                 }
 
             }
-            var tmdids = lessonBase.student.Where(x => x.type == 1).Select(x => new { id = x.id });
+            var tmdids = students.Where(x => x.type == 1);
             if (tmdids.Any()) {
                 string tmdsql = $"select value(c) from c where c.id in({string.Join(",", tmdids.Select(x => $"'{x}'"))})";
                 await foreach (var item in client.GetContainer(Constant.TEAMModelOS, Constant.Student).GetItemQueryIterator<LessonStudentRecord>(queryText: tmdsql, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"LessonStudentRecord") }))
@@ -46,18 +51,61 @@ namespace TEAMModelOS.SDK.Models.Service
             }
             List<Task<ItemResponse<LessonStudentRecord>>> records = new List<Task<ItemResponse<LessonStudentRecord>>>();
             stuids.ToList().ForEach(x => {
-                var record = lessonStudentRecords.Find(l => l.id.Equals(x.id) && l.code.Equals($"LessonStudentRecord-{x.code}"));
+                var record = lessonStudentRecords.Find(l => l.id.Equals(x.id) && l.code.Equals($"LessonStudentRecord-{x.school}"));
+                ClientSummaryList clientSummaryList = lessonBase.report.clientSummaryList.Find(c => c.seatID == x.seatID);
                 if (record != null)
                 {
-                    record.records.Add(new StudentRecord { });
+                    if (clientSummaryList != null) {
+                        if (clientSummaryList.groupScore > 0) 
+                        { record.groupScore.Add(clientSummaryList.groupScore); }
+                        if (clientSummaryList.score > 0)
+                        { record.personScore.Add(clientSummaryList.score); }
+                        if (clientSummaryList.tnteractScore > 0)
+                        { record.tnteractScore.Add(clientSummaryList.tnteractScore); }
+                    }
                 }
-                else { 
+                else {
                     
+                    record = new LessonStudentRecord {id=x.id,code=$"LessonStudentRecord-{x.school}",pk= "LessonStudentRecord",ttl=-1 };
+                    if (clientSummaryList.groupScore > 0)
+                    { record.groupScore.Add(clientSummaryList.groupScore); }
+                    if (clientSummaryList.score > 0)
+                    { record.personScore.Add(clientSummaryList.score); }
+                    if (clientSummaryList.tnteractScore > 0)
+                    { record.tnteractScore.Add(clientSummaryList.tnteractScore); }
                 }
+                records.Add(client.GetContainer(Constant.TEAMModelOS, Constant.Student).UpsertItemAsync(record, partitionKey: new PartitionKey(record.code)));
             });
             tmdids.ToList().ForEach(x => {
                 var record = lessonStudentRecords.Find(l => l.id.Equals(x.id) && l.code.Equals($"LessonStudentRecord"));
+                ClientSummaryList clientSummaryList = lessonBase.report.clientSummaryList.Find(c => c.seatID == x.seatID);
+                if (record != null)
+                {
+                    if (clientSummaryList != null)
+                    {
+                        if (clientSummaryList.groupScore > 0)
+                        { record.groupScore.Add(clientSummaryList.groupScore); }
+                        if (clientSummaryList.score > 0)
+                        { record.personScore.Add(clientSummaryList.score); }
+                        if (clientSummaryList.tnteractScore > 0)
+                        { record.tnteractScore.Add(clientSummaryList.tnteractScore); }
+                    }
+                }
+                else
+                {
+                    record = new LessonStudentRecord { id = x.id, code = $"LessonStudentRecord", pk = "LessonStudentRecord", ttl = -1 };
+                    if (clientSummaryList.groupScore > 0)
+                    { record.groupScore.Add(clientSummaryList.groupScore); }
+                    if (clientSummaryList.score > 0)
+                    { record.personScore.Add(clientSummaryList.score); }
+                    if (clientSummaryList.tnteractScore > 0)
+                    { record.tnteractScore.Add(clientSummaryList.tnteractScore); }
+                }
+                records.Add(client.GetContainer(Constant.TEAMModelOS, Constant.Student).UpsertItemAsync(record, partitionKey: new PartitionKey(record.code)));
             });
+            if (records.Any()) { 
+                await  Task.WhenAll(records);
+            }
         }
 
         public static async void DoAutoDeleteSchoolLessonRecord(LessonRecord lessonRecord,string  scope ,CosmosClient client,string school,string tmdid,
@@ -420,7 +468,7 @@ namespace TEAMModelOS.SDK.Models.Service
                 lessonDis.record = 1;
             }
             //删除数据的情况
-            else if (oldRecord != null && newRecord == null)
+            /*else if (oldRecord != null && newRecord == null)
             {
                 lessonDis.record = -1;
                 //P分数量加减
@@ -437,7 +485,7 @@ namespace TEAMModelOS.SDK.Models.Service
                 {
                     lessonDis.disTCount = -1;
                 }
-            }
+            }*/
             //无效操作
             else if (oldRecord == null && newRecord == null)
             {
@@ -557,8 +605,16 @@ namespace TEAMModelOS.SDK.Models.Service
                 string code = string.Empty;
                 if (data.scope != null && data.scope.Equals("school"))
                 {
-                    code = $"LessonCount-{data.school}-{year}-{data.periodId}";
-                    tbname = "School";
+                    if (string.IsNullOrEmpty(data.periodId))
+                    {
+                        code = $"LessonCount-{data.school}-{year}";
+                        tbname = "School";
+                    }
+                    else {
+                        code = $"LessonCount-{data.school}-{year}-{data.periodId}";
+                        tbname = "School";
+                    }
+                    
                 }
                 else
                 {
@@ -575,12 +631,6 @@ namespace TEAMModelOS.SDK.Models.Service
                     count.pCount[day - 1] += lessonDis.disPCount;
                     count.ptCount[day - 1] += lessonDis.disDCount;
                     count.beginCount[day - 1] += lessonDis.record;
-                    /*if (!count.courseIds.Contains(data.courseId))
-                    {
-                        count.courseIds.Add(data.courseId);
-                        count.beginCount[day] += 1;
-                    }*/
-
 
                     await client.GetContainer("TEAMModelOS", tbname).ReplaceItemAsync(count, count.id, new PartitionKey(code));
                 }

+ 6 - 0
TEAMModelOS/ClientApp/public/index.html

@@ -5,6 +5,12 @@
 	<meta charset="utf-8">
 	<meta http-equiv="X-UA-Compatible" content="IE=edge">
 	<meta name="viewport" content="width=device-width,initial-scale=1.0">
+	<!--设置不缓存页面-->
+	<meta http-equiv="Pragma" content="no-cache">
+	<!--设置不修改消息存储-->
+	<meta http-equiv="Cache-control" content="no-cache">
+	<!--同上-->
+	<meta http-equiv="Cache" content="no-cache">
 	<link rel="icon" href="<%= BASE_URL %>favicon.ico">
 	<link id="theme" type="text/css" rel="stylesheet" href="<%= BASE_URL %>theme/dark-theme.css">
 	<!-- <script src="<%= BASE_URL %>lang/zh-CN.js"></script>

+ 59 - 3
TEAMModelOS/ClientApp/public/lang/en-US.js

@@ -404,7 +404,6 @@ const LANG_EN_US = {
         pdName4:'AClass ONE智慧學伴',
         pdName5:'數據儲存服務空間',
         pdName6:'Haboard醍摩豆智慧大屏',  
-        serialLabel:'序列号:',
         authDate:'有效期:',
         fuAuth:'功能权限:',
         IRSnumber:'IRS链接数:',
@@ -418,6 +417,28 @@ const LANG_EN_US = {
         text2:'硬件:',
         text3:'服務類型:',
         text4:'智慧教室:',
+        contactUs:'聯系我們',
+        learnMore:'了解更多',
+        ctFrom:'聯系表單',
+        ctFmText1:'您好,歡迎留下聯絡事項,我們將盡快與您聯系。',
+        ctFmText2:'姓名',
+        ctFmText3:'請輸入姓名',
+        ctFmText4:'性別',
+        ctFmText5:'先生',
+        ctFmText6:'女士',
+        ctFmText7:'聯系電話',
+        ctFmText8:'請輸入聯系電話',
+        ctFmText9:'請選擇性別',
+        ctFmText10:'請輸入聯系電話',
+        ctFmText11:'請完善表單信息',
+        ctFmText12:'所在地區',
+        ctFmText13:'咨詢內容',
+        ctFmText14:'服務存儲空間',
+        ctFmText15:'其他',
+        ctFmText16:'請選擇所在地區',
+        ctFmText17:'請選擇咨詢內容',
+        ctFmText18:'保存成功,稍後我們將與您聯系',
+        ctFmText19:'保存失敗'
     },
     // 班级管理
     classMgmt: {
@@ -542,7 +563,8 @@ const LANG_EN_US = {
         cancel: 'Cancel',
         ok: 'OK',
         generatOk: 'Generate successfully',
-        generateErr: 'Failed to generate'
+        generateErr: 'Failed to generate',
+        noStuTip:'未獲取到學生數據',
     },
     // 课堂管理
     courseManage: {
@@ -825,6 +847,20 @@ const LANG_EN_US = {
         evRcd: 'Assessment',
         gradeErr: 'Failed to obtain grade data',
         rcdExpired:'expired',
+        cusTab1:'評測活動',
+        cusTab2:'作業活動',
+        cusTab3:'投票活動',
+        cusTab4:'問卷活動',
+        cusTab5:'班級公告',
+        cusTab6:'評測列表',
+        cusTab7:'成績統計',
+        cusTab8:'新建評測',
+        cusTab9:'新建作業',
+        cusTab10:'新建公告',
+        cusTab11:'新建問卷',
+        cusTab12:'新建投票',
+        fullTips1:'學校空間已滿,無法創建學校課程評測',
+        fullTips2:'個人空間已滿,無法創建個人課程評測',
 
         //ManageClass.vue
         stuMgt: 'Student Management',
@@ -1074,6 +1110,11 @@ const LANG_EN_US = {
             optionCount: 'Option Distribution',
             tPushLabel: 'Push:',
             noAns: 'Not Answered',
+            filter1:'所有',
+            filter2:'推送',
+            filter3:'任務',
+            filter4:'互動',
+            filter5:'測驗',
         }
     },
     // ElementUI相关
@@ -2208,6 +2249,9 @@ const LANG_EN_US = {
             shareText6: 'If you are already logged in to AClass ONE, quickly go to the online activity at:',
             shareText7: 'Or log in to AClass ONE first here:',
             shareText8: 'Scan the code for online activities',
+            shareText9: '復製評量通知',
+            shareText10: '復製評量鏈接',
+            shareText11: '為了更好的用戶體驗,請在PC端完成線上活動!',
             copy: 'Copy',
             copyTitle: 'Copy Assessment',
             copyContent: 'Are you sure to copy the assessment?',
@@ -2640,6 +2684,7 @@ const LANG_EN_US = {
     },
     // 课堂记录
     lessonRecord: {
+        noRecordTip:'當前學段下未查詢到課堂記錄!',
         overdue:'expired',
         customSetting: 'Custom Setting',
         open: 'Open',
@@ -4536,8 +4581,10 @@ const LANG_EN_US = {
             noRoom: "Not yet set",
             courseCode: "Course QR Code",
             me: "me",
-            notes: "E-Note",
+            myWorks: "我的作品",
+            notes: "教師筆記",
             mynotes: "My Notes",
+            noExam: "暫無評量數據",
             type: {
                 all: "All",
                 msg: "Text",
@@ -5539,6 +5586,12 @@ const LANG_EN_US = {
     },
     // 学情分析
     totalAnalysis: {
+        backToTop:'返回頂部',
+        noMore:'沒有更多了',
+        reachBottomTip:'我也是有底線的',
+        filterOrigin:'評測來源',
+        examName:'評測名稱',
+        examNameTip:'請輸入評測名稱',
         totalIndex: {
             title: 'Learning Status Analysis Dashboard',
             tab1: 'Statistical Data',
@@ -6143,6 +6196,9 @@ const LANG_EN_US = {
     },
     // 投票活动
     vote: {
+        copyNotice:'復制活動通知',
+        copyLinkUrl:'復制活動鏈接',
+        fullTip:'資源空間已滿,無法創建新活動!',
         secret: 'Anonymous',
         allTeacher: 'All Teachers',
         noFindVote: 'No current activity found!',

+ 59 - 3
TEAMModelOS/ClientApp/public/lang/zh-CN.js

@@ -404,7 +404,6 @@ const LANG_ZH_CN = {
         pdName4:'AClass ONE智慧学伴',
         pdName5:'数据储存服务空间',
         pdName6:'Haboard醍摩豆智慧大屏',
-        serialLabel:'序列号:',
         authDate:'有效期:',
         fuAuth:'功能权限:',
         IRSnumber:'IRS链接数:',
@@ -418,6 +417,28 @@ const LANG_ZH_CN = {
         text2:'硬件:',
         text3:'服务类型:',
         text4:'智慧教室:',
+        contactUs:'联系我们',
+        learnMore:'了解更多',
+        ctFrom:'联系表单',
+        ctFmText1:'您好,欢迎留下联络事项,我们将尽快与您联系。',
+        ctFmText2:'姓名',
+        ctFmText3:'请输入姓名',
+        ctFmText4:'性别',
+        ctFmText5:'先生',
+        ctFmText6:'女士',
+        ctFmText7:'联系电话',
+        ctFmText8:'请输入联系电话',
+        ctFmText9:'请选择性别',
+        ctFmText10:'请输入联系电话',
+        ctFmText11:'请完善表单信息',
+        ctFmText12:'所在地区',
+        ctFmText13:'咨询内容',
+        ctFmText14:'服务存储空间',
+        ctFmText15:'其他',
+        ctFmText16:'请选择所在地区',
+        ctFmText17:'请选择咨询内容',
+        ctFmText18:'保存成功,稍后我们将与您联系',
+        ctFmText19:'保存失败'
     },
     // 班级管理
     classMgmt: {
@@ -542,7 +563,8 @@ const LANG_ZH_CN = {
         cancel: '取消',
         ok: '确定',
         generatOk: '生成成功',
-        generateErr: '生成失败'
+        generateErr: '生成失败',
+        noStuTip:'未获取到学生数据',
     },
     // 课堂管理
     courseManage: {
@@ -825,6 +847,20 @@ const LANG_ZH_CN = {
         evRcd: '历次评测',
         gradeErr: '查询成绩数据失败',
         rcdExpired:'到期',
+        cusTab1:'评测活动',
+        cusTab2:'作业活动',
+        cusTab3:'投票活动',
+        cusTab4:'问卷活动',
+        cusTab5:'班级公告',
+        cusTab6:'评测列表',
+        cusTab7:'成绩统计',
+        cusTab8:'新建评测',
+        cusTab9:'新建作业',
+        cusTab10:'新建公告',
+        cusTab11:'新建问卷',
+        cusTab12:'新建投票',
+        fullTips1:'学校空间已满,无法创建学校课程评测',
+        fullTips2:'个人空间已满,无法创建个人课程评测',
 
         //ManageClass.vue
         stuMgt: '学生管理',
@@ -1074,6 +1110,11 @@ const LANG_ZH_CN = {
             optionCount: '选项分布',
             tPushLabel: '推送:',
             noAns: '未答',
+            filter1:'所有',
+            filter2:'推送',
+            filter3:'任务',
+            filter4:'互动',
+            filter5:'测验',
         }
     },
     // ElementUI相关
@@ -2208,6 +2249,9 @@ const LANG_ZH_CN = {
             shareText6: '快速登入醍摩豆云平台IES学习主页,进行线上活动',
             shareText7: '或登入醍摩豆云平台IES学习主页,进行线上活动:',
             shareText8: '扫码进行线上活动',
+            shareText9: '复制评量通知',
+            shareText10: '复制评量链接',
+            shareText11: '为了更好的用户体验,请在PC端完成线上活动!',
             copy: '复制',
             copyTitle: '复制评测',
             copyContent: '确认复制当前评测吗?',
@@ -2640,6 +2684,7 @@ const LANG_ZH_CN = {
     },
     // 课堂记录
     lessonRecord: {
+        noRecordTip:'当前学段下未查询到课堂记录!',
         overdue:'到期',
         customSetting:'自定义设置',
         open:'开启',
@@ -4536,8 +4581,10 @@ const LANG_ZH_CN = {
             noRoom: "暂无教室",
             courseCode: "课程二维码",
             me: "我",
-            notes: "电子笔记",
+            myWorks: "我的作品",
+            notes: "老师笔记",
             mynotes: "我的笔记",
+            noExam: "暂无评测数据",
             type: {
                 all: "全部",
                 msg: "飞讯",
@@ -5539,6 +5586,12 @@ const LANG_ZH_CN = {
     },
     // 学情分析
     totalAnalysis: {
+        backToTop:'返回顶部',
+        noMore:'没有更多了',
+        reachBottomTip:'我也是有底线的',
+        filterOrigin:'评测来源',
+        examName:'评测名称',
+        examNameTip:'请输入评测名称',
         totalIndex: {
             title: '学情分析仪表盘',
             tab1: '统计数据',
@@ -6144,6 +6197,9 @@ const LANG_ZH_CN = {
     },
     // 投票活动
     vote: {
+        copyNotice:'复制活动通知',
+        copyLinkUrl:'复制活动链接',
+        fullTip:'资源空间已满,无法创建新活动!',
         secret: '匿名',
         allTeacher: '所有老师',
         noFindVote: '未查询到当前活动!',

+ 59 - 3
TEAMModelOS/ClientApp/public/lang/zh-TW.js

@@ -404,7 +404,6 @@ const LANG_ZH_TW = {
         pdName4:'AClass ONE智慧學伴',
         pdName5:'數據儲存服務空間',
         pdName6:'Haboard醍摩豆智慧大屏',
-        serialLabel:'序號:',
         authDate:'有效期:',
         fuAuth:'功能權限:',
         IRSnumber:'IRS連線數:',
@@ -418,6 +417,28 @@ const LANG_ZH_TW = {
         text2:'設備:',
         text3:'服務類型:',
         text4:'智慧教室:',
+        contactUs:'聯系我們',
+        learnMore:'了解更多',
+        ctFrom:'聯系表單',
+        ctFmText1:'您好,歡迎留下聯絡事項,我們將盡快與您聯系。',
+        ctFmText2:'姓名',
+        ctFmText3:'請輸入姓名',
+        ctFmText4:'性別',
+        ctFmText5:'先生',
+        ctFmText6:'女士',
+        ctFmText7:'聯系電話',
+        ctFmText8:'請輸入聯系電話',
+        ctFmText9:'請選擇性別',
+        ctFmText10:'請輸入聯系電話',
+        ctFmText11:'請完善表單信息',
+        ctFmText12:'所在地區',
+        ctFmText13:'咨詢內容',
+        ctFmText14:'服務存儲空間',
+        ctFmText15:'其他',
+        ctFmText16:'請選擇所在地區',
+        ctFmText17:'請選擇咨詢內容',
+        ctFmText18:'保存成功,稍後我們將與您聯系',
+        ctFmText19:'保存失敗'
     },
     // 班级管理
     classMgmt: {
@@ -542,7 +563,8 @@ const LANG_ZH_TW = {
         cancel: '取消',
         ok: '確定',
         generatOk: '生成成功',
-        generateErr: '生成失敗'
+        generateErr: '生成失敗',
+        noStuTip:'未獲取到學生數據',
     },
     // 课堂管理
     courseManage: {
@@ -825,6 +847,20 @@ const LANG_ZH_TW = {
         evRcd: '歷次評量',
         gradeErr: '查詢成績數據失敗',
         rcdExpired:'到期',
+        cusTab1:'評測活動',
+        cusTab2:'作業活動',
+        cusTab3:'投票活動',
+        cusTab4:'問卷活動',
+        cusTab5:'班級公告',
+        cusTab6:'評測列表',
+        cusTab7:'成績統計',
+        cusTab8:'新建評測',
+        cusTab9:'新建作業',
+        cusTab10:'新建公告',
+        cusTab11:'新建問卷',
+        cusTab12:'新建投票',
+        fullTips1:'學校空間已滿,無法創建學校課程評測',
+        fullTips2:'個人空間已滿,無法創建個人課程評測',
 
         //ManageClass.vue
         stuMgt: '學生管理',
@@ -1074,6 +1110,11 @@ const LANG_ZH_TW = {
             optionCount: '選項分佈',
             tPushLabel: '推送:',
             noAns: '未答',
+            filter1:'所有',
+            filter2:'推送',
+            filter3:'任務',
+            filter4:'互動',
+            filter5:'測驗',
         }
     },
     // ElementUI相关
@@ -2208,6 +2249,9 @@ const LANG_ZH_TW = {
             shareText6: '快速登入醍摩豆雲平臺IES學習主頁,進行線上活動',
             shareText7: '或登入醍摩豆雲平臺IES學習主頁,進行線上活動:',
             shareText8: '掃碼進行線上活動',
+            shareText9: '復製評量通知',
+            shareText10: '復製評量鏈接',
+            shareText11: '為了更好的用戶體驗,請在PC端完成線上活動!',
             copy: '複製',
             copyTitle: '複製評量',
             copyContent: '確認複製當前評量嗎?',
@@ -2640,6 +2684,7 @@ const LANG_ZH_TW = {
     },
     // 课堂记录
     lessonRecord: {
+        noRecordTip:'當前學段下未查詢到課堂記錄!',
         overdue:'到期',
         customSetting:'自定義設定',
         open:'開啟',
@@ -4536,8 +4581,10 @@ const LANG_ZH_TW = {
             noRoom: "暫無教室",
             courseCode: "課程QR Code",
             me: "我",
-            notes: "電子筆記",
+            myWorks: "我的作品",
+            notes: "教師筆記",
             mynotes: "我的筆記",
+            noExam: "暫無評量數據",
             type: {
                 all: "全部",
                 msg: "飛訊",
@@ -5539,6 +5586,12 @@ const LANG_ZH_TW = {
     },
     // 学情分析
     totalAnalysis: {
+        backToTop:'返回頂部',
+        noMore:'沒有更多了',
+        reachBottomTip:'我也是有底線的',
+        filterOrigin:'評測來源',
+        examName:'評測名稱',
+        examNameTip:'請輸入評測名稱',
         totalIndex: {
             title: '學情分析儀表',
             tab1: '統計資料',
@@ -6143,6 +6196,9 @@ const LANG_ZH_TW = {
     },
     // 投票活动
     vote: {
+        copyNotice:'復制活動通知',
+        copyLinkUrl:'復制活動鏈接',
+        fullTip:'資源空間已滿,無法創建新活動!',
         secret: '匿名',
         allTeacher: '所有老師',
         noFindVote: '未查詢到當前活動!',

+ 6 - 4
TEAMModelOS/ClientApp/public/web/viewer.html

@@ -31,10 +31,12 @@ See https://github.com/adobe-type-tools/cmap-resources
 
 <!-- This snippet is used in production (included from viewer.html) -->
 <link rel="resource" type="application/l10n" href="locale/locale.properties">
-<script src="../pdfjs/pdf.js"></script>
-
-  <script src="viewer.js"></script>
-
+<!-- <script src="../pdfjs/pdf.js"></script> -->
+<script>
+    var now = new Date().getTime();
+    document.write('<script language="javascript" src="../pdfjs/pdf.js?t=' + now + '"><\/script\>');
+    document.write('<script language="javascript" src="viewer.js?t=' + now + '"><\/script\>');
+ </script>
   </head>
 
   <body tabindex="1">

+ 4 - 0
TEAMModelOS/ClientApp/src/api/serviceDriveAuth.js

@@ -10,6 +10,10 @@ export default {
     },
     classroomBand: function (data) {
         return post('/school/classroom/hiteach-link-unlink', data)
+    },
+    //联系表单
+    sendContactInfo: function (data) {
+        return post('/core/apply-service', data)
     }
     
 }

+ 26 - 3
TEAMModelOS/ClientApp/src/assets/iconfont/demo_index.html

@@ -54,6 +54,12 @@
       <div class="content unicode" style="display: block;">
           <ul class="icon_lists dib-box">
           
+            <li class="dib">
+              <span class="icon iconfont">&#xe67c;</span>
+                <div class="name">作品</div>
+                <div class="code-name">&amp;#xe67c;</div>
+              </li>
+          
             <li class="dib">
               <span class="icon iconfont">&#xe6b6;</span>
                 <div class="name">意见反馈、记笔记</div>
@@ -1224,9 +1230,9 @@
 <pre><code class="language-css"
 >@font-face {
   font-family: 'iconfont';
-  src: url('iconfont.woff2?t=1650883304079') format('woff2'),
-       url('iconfont.woff?t=1650883304079') format('woff'),
-       url('iconfont.ttf?t=1650883304079') format('truetype');
+  src: url('iconfont.woff2?t=1653386415595') format('woff2'),
+       url('iconfont.woff?t=1653386415595') format('woff'),
+       url('iconfont.ttf?t=1653386415595') format('truetype');
 }
 </code></pre>
           <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -1252,6 +1258,15 @@
       <div class="content font-class">
         <ul class="icon_lists dib-box">
           
+          <li class="dib">
+            <span class="icon iconfont icon-zuopin"></span>
+            <div class="name">
+              作品
+            </div>
+            <div class="code-name">.icon-zuopin
+            </div>
+          </li>
+          
           <li class="dib">
             <span class="icon iconfont icon-myNote"></span>
             <div class="name">
@@ -3007,6 +3022,14 @@
       <div class="content symbol">
           <ul class="icon_lists dib-box">
           
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-zuopin"></use>
+                </svg>
+                <div class="name">作品</div>
+                <div class="code-name">#icon-zuopin</div>
+            </li>
+          
             <li class="dib">
                 <svg class="icon svg-icon" aria-hidden="true">
                   <use xlink:href="#icon-myNote"></use>

+ 7 - 3
TEAMModelOS/ClientApp/src/assets/iconfont/iconfont.css

@@ -1,8 +1,8 @@
 @font-face {
   font-family: "iconfont"; /* Project id 2000444 */
-  src: url('iconfont.woff2?t=1650883304079') format('woff2'),
-       url('iconfont.woff?t=1650883304079') format('woff'),
-       url('iconfont.ttf?t=1650883304079') format('truetype');
+  src: url('iconfont.woff2?t=1653386415595') format('woff2'),
+       url('iconfont.woff?t=1653386415595') format('woff'),
+       url('iconfont.ttf?t=1653386415595') format('truetype');
 }
 
 .iconfont {
@@ -13,6 +13,10 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.icon-zuopin:before {
+  content: "\e67c";
+}
+
 .icon-myNote:before {
   content: "\e6b6";
 }

File diff suppressed because it is too large
+ 1 - 1
TEAMModelOS/ClientApp/src/assets/iconfont/iconfont.js


+ 7 - 0
TEAMModelOS/ClientApp/src/assets/iconfont/iconfont.json

@@ -5,6 +5,13 @@
   "css_prefix_text": "icon-",
   "description": "",
   "glyphs": [
+    {
+      "icon_id": "20000263",
+      "name": "作品",
+      "font_class": "zuopin",
+      "unicode": "e67c",
+      "unicode_decimal": 59004
+    },
     {
       "icon_id": "24591518",
       "name": "意见反馈、记笔记",

BIN
TEAMModelOS/ClientApp/src/assets/iconfont/iconfont.ttf


BIN
TEAMModelOS/ClientApp/src/assets/iconfont/iconfont.woff


BIN
TEAMModelOS/ClientApp/src/assets/iconfont/iconfont.woff2


BIN
TEAMModelOS/ClientApp/src/assets/image/qrcode_en.png


BIN
TEAMModelOS/ClientApp/src/assets/image/qrcode_tw.png


+ 5 - 3
TEAMModelOS/ClientApp/src/common/BaseLayout.vue

@@ -487,7 +487,7 @@ export default {
                             tag: '',
                             role: 'admin',
                             permission: 'research-read|research-upd',
-                            menuName: 'courseCenter',
+                            menuName: 'sokrates',
                             isShow: true
                         },
                         {
@@ -735,12 +735,14 @@ export default {
                 {
                     icon: 'iconfont icon-course-self',
                     name: this.$t('system.menu.myCus'),
-                    router: '/home/myCourse',
+                    // router: '/home/myCourse',
+                    router: '/home/course',
                     tag: '',
                     role: 'teacher',
                     permission: '',
                     child: [],
-                    menuName: 'myCourse',
+                    menuName: 'course',
+                    // menuName: 'myCourse',
                     isShow: this.IES5Menu,
                     info: this.$t('tip.myCus')
                 },

+ 9 - 3
TEAMModelOS/ClientApp/src/components/dashboard/art/RightTop.vue

@@ -2,11 +2,11 @@
   <div id="bottomLeft">
 	  <p class="dashboard-block-title">
 	  	<Icon type="md-pulse" />
-	  	<span>{{ $store.state.dashboard.classType === 'all' ? '各班级测评情况统计' : '班级测评情况统计' }}</span>
+	  	<span>{{ classType === 'all' ? '各班级测评情况统计' : '班级测评情况统计' }}</span>
 		<dv-decoration-1 class="dv-dec-3" />
 	  </p>
     <div class="bg-color-black">
-        <BaseClassLineBar v-if="$store.state.dashboard.classType === 'all'"/>
+        <BaseClassLineBar v-if="classType === 'all'"/>
         <BaseStuLineBar v-else/>
     </div>
   </div>
@@ -19,7 +19,13 @@ export default {
   components: {
     BaseClassLineBar,
     BaseStuLineBar
-  }
+  },
+  computed: {
+    classType() {
+      console.log(this.$store.state.dashboard.classType);
+      return this.$store.state.dashboard.classType
+    }
+  },
 }
 </script>
 

File diff suppressed because it is too large
+ 515 - 530
TEAMModelOS/ClientApp/src/components/questionnaire/BaseQnForm.vue


+ 114 - 0
TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/Buzr.vue

@@ -0,0 +1,114 @@
+<template>
+    <div class="buzz-wrap">
+        <p class="event-type">{{ $t("talMgmt.text42") }}:</p>
+        <div class="buzz-box" v-for="(item,index) in buzzClients" :key="index">
+            <!-- <Icon custom="iconfont icon-buzz" size="24" /> -->
+            <span>{{getChinese(item.name)}}</span>
+        </div>
+        <StudentClient></StudentClient>
+    </div>
+</template>
+<script>
+import StudentClient from '@/view/classrecord/eventchart/StudentClient.vue'
+export default {
+    props: {
+        buzrData: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        },
+        students: {
+            type: Array,
+            default: () => {
+                return []
+            }
+        }
+    },
+    components: {
+        StudentClient
+    },
+    data() {
+        return {
+            // buzzClients: []
+        }
+    },
+    methods: {
+        getChinese(strValue) {
+            if (!strValue) return ''
+            var reg = /[\u4e00-\u9fa5]/g;
+            var names = strValue.match(reg);
+            if (names) {
+                return names.join("").substring(names.length - 2)
+            } else {
+                return strValue.substr(0, 2)
+            }
+        }
+    },
+    computed: {
+        buzzClients() {
+            // 学生端只展示自己的
+            let data = []
+            if (this.buzrData.buzzClients) {
+                let buzzClients = this._.cloneDeep(this.buzrData.buzzClients)
+                if (this.students.length) {
+                    buzzClients.forEach(item => {
+                        let s = this.students.find(stu => stu.seatID == item && stu.id === this.$store.state.userInfo.sub)
+                        if(s) {
+                            data.push({
+                                seatNo: item,
+                                name: s ? s.name : item
+                            })
+                        }
+                    })
+                }/*  else {
+                    data = buzzClients.map(item => {
+                        return {
+                            seatNo: item,
+                            name: item
+                        }
+                    })
+                } */
+                return data
+            }
+            return data
+        }
+    }
+    // watch: {
+    //     buzrData: {
+    //         immediate: true,
+    //         deep: true,
+    //         handler(n, o) {
+    //             if (n.buzzClients) {
+    //                 this.buzzClients = n.buzzClients
+    //             }
+    //         }
+    //     }
+    // }
+}
+</script>
+<style lang="less" scoped>
+.buzz-wrap {
+    display: flex;
+}
+.buzz-box {
+    margin-right: 20px;
+    width: 70px;
+    height: 70px;
+    line-height: 70px;
+    background: #17233d;
+    text-align: center;
+    color: white;
+    border-radius: 50%;
+    background-image: radial-gradient(#5cadff 50%, #2b85e4 30%);
+    span {
+        font-size: 20px;
+        font-weight: 600;
+    }
+}
+.event-type {
+    margin-right: 20px;
+    font-size: 15px;
+    font-weight: 600;
+}
+</style>

+ 7 - 6
TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/ClassRecord.less

@@ -109,7 +109,7 @@
 
     .message-area {
         position: relative;
-        height: 815px;
+        // height: 815px;
         padding: 10px;
         margin-bottom: 20px;
 
@@ -389,10 +389,11 @@
                     border: 1px dashed transparent;
                     margin-bottom: 15px;
                     padding: 5px 5px;
-                }
-
-                .event-item:hover {
-                    border: 1px dashed #e0e0e0;
+                    overflow-x: scroll;
+                    
+                    &:hover {
+                        border: 1px dashed #e0e0e0;
+                    }
                 }
 
                 .student-event {
@@ -405,7 +406,7 @@
 
     .dec {
         padding: 0px;
-        height: 800px;
+        // height: 800px;
         overflow: hidden;
         position: relative;
 

+ 160 - 105
TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/ClassRecord.vue

@@ -7,6 +7,7 @@
             </span>
             <span class="testTitleText">{{$t("studentWeb.courseContent.classRecord")}}</span>
         </div>
+        <div class="class-record">
         <vuescroll ref="pagewrap">
             <div style="padding: 1% 3% 3%; margin-top: 44px;">
                 <div class="record-head" v-if="recordInfo">
@@ -120,7 +121,7 @@
                                     <Icon :class="{'select-filter': currentfilterType === 'ShowAnsLoad' || currentfilterType === 'QaWrong' }" custom="iconfont icon-cuowu" :title="$t('studentWeb.courseContent.type.wrong')" size="30" />
                                 </span>
                             </div> -->
-                            <vuescroll ref="datawrap">
+                            <!-- <vuescroll ref="datawrap"> -->
                                 <div style="margin-bottom: 50px;">
                                     <div v-for="(items, index) in pageList" :key="index" :id="'page' + (items.page)">
                                         <div v-if="items.pageData.length" class="message-box">
@@ -153,19 +154,21 @@
                                                         <!-- 推送 -->
                                                         <Push class="event-item" v-if="event.Event === 'FastPgPush' && (currentfilterType === '' || currentfilterType === 'doc')" :pushData="event.data"></Push>
                                                         <!-- 作品收集 -->
-                                                        <StuReceive :nowStuInfo="nowStuInfo" :recordInfo="recordInfo" class="student-event event-item" v-if="event.Event === 'WrkSpaceEnd' && baseData" :rcvData="event.data" :students="baseData.student"></StuReceive>
-                                                        <Receive :recordInfo="recordInfo" class="student-event event-item" v-if="event.Event === 'WrkSpaceEnd' && baseData" :rcvData="event.data" :students="baseData.student"></Receive>
+                                                        <StuReceive class="student-event event-item" v-if="event.Event === 'WrkSpaceEnd' && baseData" :nowStuInfo="nowStuInfo" :rcvData="event.data" :recordInfo="recordInfo" :students="baseData.student"></StuReceive>
+                                                        <!-- 老师收集的所有作品目前不展示,只展示老师回贴的学生作品 -->
+                                                        <!-- <Receive :recordInfo="recordInfo" class="student-event event-item" v-if="event.Event === 'WrkCmp' && baseData" :rcvData="event.data" :students="baseData.student"></Receive> -->
+                                                        <ReceiveBack class="event-item" v-if="event.Event === 'WrkCmp' && event.data" :recordInfo="recordInfo" :rcvData="event.data" :students="baseData.student"></ReceiveBack>
                                                         <!-- 随机挑人 -->
-                                                        <Pick class="event-item student-event" :pickData="event.data" v-if="event.Event === 'PickupResult' && baseData" :students="baseData.student"></Pick>
+                                                        <Pick class="event-item student-event" v-if="event.Event === 'PickupResult' && baseData" :pickData="event.data" :students="baseData.student"></Pick>
                                                         <!-- 课中评测 -->
-                                                        <Exam class="event-item" :pickData="event.data" v-if="event.Event === 'SPQStrt'"></Exam>
+                                                        <Exam class="student-event event-item" :examInfo="event.data" :recordInfo="recordInfo" v-if="event.Event === 'SPQStrt'"></Exam>
                                                     </div>
                                                 </template>
                                             </div>
                                         </div>
                                     </div>
                                 </div>
-                            </vuescroll>
+                            <!-- </vuescroll> -->
                         </div>
                         <div v-else class="no-interaction">
                             {{ $t("studentWeb.hiteachNote.noContent") }}
@@ -174,6 +177,10 @@
                 </div>
             </div>
         </vuescroll>
+        </div>
+        
+        <!--返回顶部-->
+        <BackToTop @on-to-top="handleToTop"></BackToTop>
         <Modal v-model="showNote" :title="$t('studentWeb.courseContent.mynotes')" width="800">
             <!-- <div> -->
                 <img :src="item" alt="" v-for="(item, index) in recordInfo.myNote" :key="index" style="width: 100%">
@@ -194,11 +201,12 @@ import ShowQues from './ShowQues.vue';
 import StuReceive from './StuReceive.vue';
 import PopQues from '@/view/classrecord/eventchart/PopQues.vue';
 import Buzr from '@/view/classrecord/eventchart/Buzr.vue';
-import Pick from '@/view/classrecord/eventchart/Pick.vue';
+import Pick from './Pick.vue';
 import Push from '@/view/classrecord/eventchart/Push.vue';
-import Exam from '@/view/classrecord/eventchart/Exam.vue';
+import Exam from './Exam.vue';
 import Receive from './Receive.vue';
 import DataCount from './DataCount.vue';
+import ReceiveBack from './ReceiveBack.vue';
 
 export default {
     name: "ClassRecord",
@@ -215,6 +223,7 @@ export default {
         Exam,
         Receive,
         DataCount,
+        ReceiveBack,
     },
     data() {
         return {
@@ -380,110 +389,142 @@ export default {
             this.recordInfo.eNote = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/Note.pdf?${sas.sas}`
             // 如果只会存在一个视频,文件名是否可以固定?
             this.playerOptions.sources[0].src = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/Record/CourseRecord.mp4?${sas.sas}`
-            let url = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/Sokrates/SokratesRecords.json?${sas.sas}` //后面会根据TimeLine.json处理
-            this.$tools.getFile(url).then(
-                async res => {
-                    this.sokratesRecords = JSON.parse(res)
-                    //获取Push.json、IRS.json、Task.json、Base.json数据
-                    try {
-                        let pushUrl = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/IES/Push.json?${sas.sas}`
-                        this.pushData = JSON.parse(await this.$tools.getFile(pushUrl) || '[]')
-                        this.pushData.forEach(item => {
-                            item.pageUrl = `${sas.url}/${sas.name}/records/${this.recordInfo.id}${item.pageMeta}?${sas.sas}`
-                        })
-                    } catch (e) {
-                        this.pushData = []
-                    }
-                    try {
-                        let irsUrl = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/IES/IRS.json?${sas.sas}`
-                        this.irsData = JSON.parse(await this.$tools.getFile(irsUrl) || '[]')
-                    } catch (e) {
-                        this.irsData = []
-                    }
-                    try {
-                        let taskUrl = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/IES/Task.json?${sas.sas}`
-                        this.taskData = JSON.parse(await this.$tools.getFile(taskUrl) || '[]')
-                    } catch (e) {
-                        this.taskData = []
+            // let url = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/Sokrates/SokratesRecords.json?${sas.sas}` //后面会根据TimeLine.json处理
+            // 这里需要兼容原来没有TimeLine.json的课例(优先度读timeLine.json,没有则读SokratesRecords.json)
+            let url = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/IES/TimeLine.json?${sas.sas}`
+            let hasTimeLine = true
+            let dataErr = false
+            let pgids = []
+            let pageEvents = []
+            try {
+                let res = await this.$tools.getFile(url)
+                this.sokratesRecords = JSON.parse(res)
+                pgids = this.sokratesRecords.PgIdList || []
+                pageEvents = this.sokratesRecords.events || []
+            } catch (e) {
+                hasTimeLine = false
+            }
+            //读取 timeLine.json 失败,则读取 SokratesRecords.json
+            if (!hasTimeLine) {
+                url = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/Sokrates/SokratesRecords.json?${sas.sas}`
+                try {
+                    let res = await this.$tools.getFile(url)
+                    let resJson = JSON.parse(res)
+                    // 处理成timeLine数据格式
+                    let pageidEvent = resJson.find(item => item.Event == 'PgidList')
+                    pgids = pageidEvent && pageidEvent.PgIdList ? pageidEvent.PgIdList : []
+                    pageEvents = resJson.filter(item => this.events.includes(item.Event))
+                    this.sokratesRecords = {
+                        events: pageEvents,
+                        PgIdList: pgids
                     }
-                    try {
-                        let baseUrl = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/IES/base.json?${sas.sas}`
-                        this.baseData = JSON.parse(await this.$tools.getFile(baseUrl) || '{}')
-                        this.baseData.student.forEach((item, index) => {
-                            if (item.name === this.$store.state.userInfo.name) {
-                                this.nowStuInfo = item
-                                this.nowStuInfo.index = index
-                            }
-                        })
-                    } catch (e) {
-                        this.baseData = undefined
+                } catch (e) {
+                    //timeLine 和 SokratesRecords都没有找到
+                    dataErr = true
+                }
+            }
+            
+            // 数据异常
+            if (dataErr) {
+                this.$Message.error(this.$t('cusMgt.rcd.dataErr'))
+                return
+            }
+            //获取Push.json、IRS.json、Task.json、Base.json数据
+            try {
+                let pushUrl = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/IES/Push.json?${sas.sas}`
+                this.pushData = JSON.parse(await this.$tools.getFile(pushUrl) || '[]')
+                this.pushData.forEach(item => {
+                    item.pageUrl = `${sas.url}/${sas.name}/records/${this.recordInfo.id}${item.pageMeta}?${sas.sas}`
+                })
+            } catch (e) {
+                this.pushData = []
+            }
+            try {
+                let irsUrl = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/IES/IRS.json?${sas.sas}`
+                this.irsData = JSON.parse(await this.$tools.getFile(irsUrl) || '[]')
+            } catch (e) {
+                this.irsData = []
+            }
+            try {
+                let taskUrl = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/IES/Task.json?${sas.sas}`
+                this.taskData = JSON.parse(await this.$tools.getFile(taskUrl) || '[]')
+            } catch (e) {
+                this.taskData = []
+            }
+            try {
+                let baseUrl = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/IES/base.json?${sas.sas}`
+                this.baseData = JSON.parse(await this.$tools.getFile(baseUrl) || '{}')
+                this.baseData.student.forEach((item, index) => {
+                    if (item.id === this.$store.state.userInfo.sub) {
+                        this.nowStuInfo = item
+                        this.nowStuInfo.index = index
                     }
-                    let r = this.sokratesRecords.find(item => item.Event === 'PgidList')
-                    let pgids = r ? r.PgIdList : []
+                })
+            } catch (e) {
+                this.baseData = undefined
+            }
 
-                    //这里需要判断录制开始的pageid
-                    let startInfo = this.sokratesRecords.find(item => item.Event === 'EzsStartRecord')
-                    let startId = startInfo ? startInfo.Pgid : ''
-                    let startIndex = 0
-                    if (startId) {
-                        startIndex = pgids.findIndex(item => item === startId)
-                    }
-                    pgids = pgids.slice(startIndex)
+            //这里需要判断录制开始的pageid
+            let startInfo = pageEvents.find(item => item.Event === 'EzsStartRecord')
+            let startId = startInfo ? startInfo.Pgid : ''
+            let startIndex = 0
+            if (startId) {
+                startIndex = pgids.findIndex(item => item === startId)
+            }
+            pgids = pgids.slice(startIndex)
 
-                    let havePage = 0
-                    pgids.forEach((item, index) => {
-                        let page = {}
-                        page.id = item
-                        page.img = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/Memo/${item}.jpg?${sas.sas}`
-                        page.page = index + 1
-                        //当前页面对应的sokrates
-                        page.pageData = this.sokratesRecords.filter(record => record.Pgid === item && this.fnEvents.includes(record.Event))
-                        havePage += (page.pageData.length ? 1 : 0)
-                        page.pageData.forEach(e => {
-                            e.pageIndex = index
-                            e.eventName = this.hiTeachEvent[e.Event]?.text
-                            let rlt = this.hiTeachEvent[e.Event]?.relation
-                            e.relation = rlt
-                            switch (rlt) {
-                                case 'irs':
-                                    e.data = this.irsData.find(i => i.pageID == e.Pgid)
-                                    break
-                                case 'push':
-                                    e.data = this.pushData.find(p => p.pageId == e.Pgid)
-                                    break
-                                case 'task':
-                                    e.data = this.taskData.find(t => t.pageID == e.Pgid)
-                                    break
-                                case 'timeline':
-                                    e.data = this._.cloneDeep(e)
-                                    break
-                                default:
-                                    break
-                            }
-                        })
-                        this.pageList.push(page)
-                    })
-                    this.haveInteraction = havePage != 0
-                    let pageEvent = this.sokratesRecords.filter(item => item.Event === 'PopQuesLoad' || item.Event === 'ReAtmpAnsStrt' || item.Event === 'FastPgPush' || item.Event === 'WrkSpaceEnd' || item.Event === 'SPQStrt')
-                    this.markers = pageEvent.map((item, index) => {
-                        return {
-                            time: item.Time,
-                            text: `${this.$t('cusMgt.rcd.di')}${index + 1}${this.$t('cusMgt.rcd.page')}`,
-                            page: index + 1
-                        }
-                    })
-                    this.getVideo()
-                },
-                err => {
-                    this.$Message.error('获取数据失败')
+            let havePage = 0
+            pgids.forEach((item, index) => {
+                let page = {}
+                page.id = item
+                page.img = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/Memo/${item}.jpg?${sas.sas}`
+                page.page = index + 1
+                //当前页面对应的sokrates
+                page.pageData = pageEvents.filter(record => record.Pgid === item && this.fnEvents.includes(record.Event))
+                havePage += (page.pageData.length ? 1 : 0)
+                page.pageData.forEach(e => {
+                    e.pageIndex = index
+                    e.eventName = this.hiTeachEvent[e.Event]?.text
+                    let rlt = this.hiTeachEvent[e.Event]?.relation
+                    e.relation = rlt
+                    switch (rlt) {
+                        case 'irs':
+                            e.data = this.irsData.find(i => i.pageID == e.Pgid)
+                            break
+                        case 'push':
+                            e.data = this.pushData.find(p => p.pageId == e.Pgid || p.pageID == e.Pgid)
+                            break
+                        case 'task':
+                            e.data = this.taskData.find(t => t.pageID == e.Pgid)
+                            break
+                        case 'timeline':
+                            e.data = this._.cloneDeep(e)
+                            break
+                        case 'exam':
+                            e.data = this._.cloneDeep(e)
+                            break
+                        default:
+                            break
+                    }
+                })
+                this.pageList.push(page)
+            })
+            this.haveInteraction = havePage != 0
+            let pageEvent = pageEvents.filter(item => item.Event === 'PopQuesLoad' || item.Event === 'ReAtmpAnsStrt' || item.Event === 'FastPgPush' || item.Event === 'WrkSpaceEnd' || item.Event === 'SPQStrt')
+            this.markers = pageEvent.map((item, index) => {
+                return {
+                    time: item.Time,
+                    text: `${this.$t('cusMgt.rcd.di')}${index + 1}${this.$t('cusMgt.rcd.page')}`,
+                    page: index + 1
                 }
-            )
+            })
+            this.getVideo()
         },
         // 点击互动记录页面tag
         toVideo(page, e) {
             this.curPage = page
             //页面滚动
-            let dataLoacation = this.$refs["datawrap"].getPosition()
+            /* let dataLoacation = this.$refs["datawrap"].getPosition()
             let pageLocaltion = this.$refs["pagewrap"].getPosition()
             let y = e.pageY - 770 + pageLocaltion.scrollTop + dataLoacation.scrollTop
             this.$nextTick(() => {
@@ -499,7 +540,7 @@ export default {
                         y: y
                     }
                 )
-            })
+            }) */
             //视频时间定位
             let pageInfo = this.markers.find(item => {
                 return item.page === page
@@ -524,7 +565,7 @@ export default {
                     this.player.pause()
                 }
                 //互动记录滚动
-                this.$refs["datawrap"].scrollIntoView('#page' + page, 500)
+                // this.$refs["datawrap"].scrollIntoView('#page' + page, 500)
             } else {
             }
         },
@@ -532,7 +573,7 @@ export default {
         getCurPage(page) {
             this.curPage = page
             // this.mapJson = require('./data/' + page + '.json')
-            this.$refs["datawrap"].scrollIntoView('#page' + page, 500)
+            // this.$refs["datawrap"].scrollIntoView('#page' + page, 500)
             this.player.play()
         },
         //查看电子笔记
@@ -603,6 +644,16 @@ export default {
         quitRec() {
             this.$router.go(-1)
         },
+        //返回顶部
+        handleToTop() {
+            document.getElementsByClassName("class-record")[0].scrollIntoView()
+            /* this.$refs['pagewrap'].scrollTo(
+                {
+                    y: '0'
+                },
+                300
+            ) */
+        },
     },
     computed: {
         curImg() {
@@ -663,4 +714,8 @@ export default {
     border-radius: 50%;
     background: #de7320;
 }
+
+.record-overflow {
+    overflow-x: scroll;
+}
 </style>

+ 249 - 0
TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/Exam.vue

@@ -0,0 +1,249 @@
+<template>
+    <div class="exam-wrap">
+        <div class="clt-type" v-if="!examDetaiInfo">
+            <!-- <Icon type="md-podium" style="margin-right:5px" />
+            查看评测数据 -->
+            {{ $t("studentWeb.courseContent.noExam") }}
+        </div>
+        <div v-else class="exam-chart-wrap">
+            <ScoreBarChart :total="classTotal" :subjectNames="subjectName"></ScoreBarChart>
+            <ExamQu :quData="correctData[0] ? correctData[0].data : []"></ExamQu>
+            <ExamTable :examDetaiInfo="examDetaiInfo" :examInfo="examInfo" :recordInfo="recordInfo"></ExamTable>
+        </div>
+        <div>
+            <Icon type="ios-person" class="owner-student-client-icon"/>
+        </div>
+    </div>
+</template>
+<script>
+import TeacherClient from '@/view/classrecord/eventchart/TeacherClient.vue'
+import ExamGrade from '@/view/classrecord/eventchart/ExamGrade.vue'
+import ExamQu from './ExamQu.vue'
+import ScoreBarChart from './ScoreBarChart.vue'
+import ExamTable from './ExamTable.vue'
+
+export default {
+    components: {
+        TeacherClient, ExamGrade, ExamQu, ExamTable, ScoreBarChart
+    },
+    props: {
+        examInfo: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        },
+        recordInfo: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        },
+    },
+    data() {
+        return {
+            simpleData: {},
+            examDetaiInfo: undefined,
+            paperQuInfo: [],
+            correctData: [],
+            subjectName: []
+        }
+    },
+    methods: {
+        getExamInfo() {
+            return new Promise((r, j) => {
+                let code = this.recordInfo.scope == 'school' ? this.recordInfo.school : this.recordInfo.tmdid
+                let req = {
+                    id: this.examInfo.ExamId,
+                    studentId: this.$store.state.userInfo.sub,
+                    code,
+                    scode: `Exam-${this.recordInfo.tmdid}`,
+                    cIds: this.recordInfo.groupIds
+                }
+                this.$api.studentWeb.FindStudentPaper(req).then(res => {
+                    this.examDetaiInfo = res
+                    res.subjects.forEach(item => {
+                        this.subjectName.push(item.name)
+                    })
+                    let d = []
+                    let totalNum = 0 //总人数
+                    // 不同科目,多个班级,以科目的数量为准
+                    res.total.forEach((item, index) => {
+                        if((index + 1) % res.subjects.length === 0) {
+                            item.forEach(num => {
+                                totalNum += num
+                            })
+                        }
+                    })
+                    res.wno.forEach((item, index) => {
+                        let cData = item.map((w, i) => {
+                            let correct = totalNum - w
+                            return {
+                                correct: correct, //正确人数
+                                wrong: w,//错误人数
+                                rate: totalNum ? parseInt(correct * 100 / totalNum) : 0,//正确率
+                                quLabel: i + 1,
+                                type: false,
+                            }
+                        })
+                        d.push({
+                            subjectId: res.subjects[index],
+                            data: cData,
+                        })
+                    })
+                    this.correctData = d
+                    r(true)
+                })
+            })
+            
+        },
+        // 设置各科题号信息
+        async setPaperQuInfo() {
+            this.paperQuInfo = []
+            let code = this.recordInfo.scope == 'school' ? this.recordInfo.school : this.recordInfo.tmdid
+            let papers = await this.getStuPaper(code)
+            if (!papers) return
+            let objectiveQu = ['single', 'multiple', 'judge']
+            papers.forEach(paper => {
+                let data = []
+                let realIndex = 0
+                paper.item?.forEach((item, index) => {
+                    if (item.children.length) {
+                        item.children.forEach((childItem, childIndex) => {
+                            let i = realIndex++
+                            data.push({
+                                label: (index + 1) + '-' + (childIndex + 1),
+                                value: i,
+                                disabled: objectiveQu.includes(childItem.type),
+                                quIndex: index,
+                                childIndex: childIndex
+                            })
+                        })
+                    } else {
+                        let i = realIndex++
+                        data.push({
+                            label: (index + 1) + '',
+                            value: i,
+                            disabled: objectiveQu.includes(item.type),
+                            quIndex: index,
+                            childIndex: -1
+                        })
+                    }
+                })
+                this.paperQuInfo.push({
+                    subjectId: paper.subjectId,
+                    subjectName: paper.subjectName,
+                    quNo: data
+                })
+            })
+        },
+        getStuPaper(code) {
+            return new Promise(async(r, j) => {
+                try {
+                    let a = await this.$evTools.getStuPaper(code)
+                    r(a)
+                } catch(e) {
+                    j(undefined)
+                }
+            })
+        },
+    },
+    computed: {
+        examScore() {
+            if (this.examDetaiInfo && this.examDetaiInfo.score) {
+                return this.examDetaiInfo.score
+            } else {
+                return 0
+            }
+        },
+        classTotal() {
+            if (this.examDetaiInfo && this.examDetaiInfo.total) {
+                let total = []
+                this.examDetaiInfo.total.forEach((item, index) => {
+                    total.push({
+                        value: this.examDetaiInfo.subjects[index].name,
+                        data: item,
+                        type: "bar",
+                    })
+                })
+                return total
+            }
+        },
+        // 所有学生总分统计(多科)
+        stuTotalScores() {
+            if (this.simpleData && this.simpleData.averageTotal) {
+                let total = []
+                this.simpleData.averageTotal.forEach((subItem, i) => {
+                    // 累积各科分数
+                    subItem.total.forEach((sItem, j) => {
+                        if (!total[j]) total[j] = 0
+                        total[j] += sItem
+                    })
+                })
+                return total
+            } else {
+                return []
+            }
+        },
+        //总分成绩分布
+        scoreSegment() {
+            if (this.stuTotalScores.length) {
+                let segment = []
+                let unit = this.examScore / 10
+                let startScore = 0
+                for (let i = 0; i < 10; i++) {
+                    let endScore = Math.ceil(unit * (i + 1))
+                    let s = this.stuTotalScores.filter(item => {
+                        return item >= startScore && item <= endScore
+                    })
+                    segment.push({
+                        name: `${startScore}-${endScore}`,
+                        value: s.length
+                    })
+                    startScore = endScore + 1
+                }
+                return segment
+            } else {
+                return []
+            }
+        },
+    },
+    watch:{
+        examInfo: {
+            deep: true,
+            immediate: true,
+            async handler(n, o) {
+                if(n && n.ExamId){
+                    await this.getExamInfo()
+                }
+            }
+        }
+    }
+}
+</script>
+<style lang="less" scoped>
+.exam-wrap {
+    display: flex;
+}
+.exam-info {
+    width: fit-content;
+    padding: 5px 20px;
+    background: #5cadff;
+    color: white;
+    border: 2px solid #2b85e4;
+    border-radius: 6px;
+    font-size: 14px;
+    user-select: none;
+    cursor: pointer;
+}
+.exam-chart-wrap{
+    display: flex;
+    overflow-x: scroll;
+}
+
+.clt-type {
+    margin-right: 10px;
+    font-size: 15px;
+    font-weight: 600;
+}
+</style>

+ 283 - 0
TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/ExamQu.vue

@@ -0,0 +1,283 @@
+<template>
+    <div class="qu-score">
+        <div class="qu-score-count" :id="`class-score-count-${id}`"></div>
+        <p class="legend-info">
+            <!-- <span>{{ $t('studentWeb.exam.chart.legendSimple.legend1') }}</span> -->
+            <span>{{ $t('studentWeb.exam.chart.legendSimple.legend2') }}
+                <span style="color: #00AD6C;">√</span>
+            </span>
+            <span>{{ $t('studentWeb.exam.chart.legendSimple.legend3') }}
+                <span style="color: #FF5508;">×</span>
+            </span>
+        </p>
+    </div>
+</template>
+<script>
+import elementResizeDetectorMaker from "element-resize-detector"
+export default {
+    props: {
+        quData: {
+            type: Array,
+            default: () => {
+                return []
+            }
+        }
+    },
+    data() {
+        let _this = this
+        return {
+            isShowRate: true,
+            progressPie: undefined,
+            stuCount: 0,
+            option: {},
+            id: "",
+        }
+    },
+    created() {
+        this.id = this.$jsFn.getBtwRandom(0, 100000000)
+    },
+    mounted() {
+        this.progressPie = this.$echarts.init(document.getElementById(`class-score-count-${this.id}`), 'macarons')
+        this.progressPie.setOption(this.option)
+        let erd11 = elementResizeDetectorMaker()
+        erd11.listenTo(document.getElementById(`class-score-count-${this.id}`), () => {
+            this.$nextTick(() => {
+                //监听到事件后执行的业务逻辑
+                this.progressPie.resize()
+            })
+        })
+    },
+    watch: {
+        quData: {
+            handler(n, o) {
+                console.log(n);
+                let _this = this
+                this.$nextTick(() => {
+                    if (this.quData.length) _this.stuCount = this.quData[0].correct + this.quData[0].wrong
+                    this.option = {
+                        tooltip: {
+                            trigger: 'item',
+                            formatter: '{b} : {c}' + this.$t('unit.text7')
+                        },
+                        title: {
+                            "text": this.$t('learnActivity.simple.quCorrectRate'),
+                            "left": "center",
+                            "top": 0,
+                            "textStyle": {
+                                fontSize: 15,
+                                color: "#333"
+                            }
+                        },
+                        legend: {
+                            /* itemStyle: {
+                                opacity: 0
+                            }, */
+                            /* formatter: (params) => {
+                                return `√表示答对`
+                            }, */
+                            bottom: 0,
+                        },
+                        tooltip: {
+                            formatter: (params) => {
+                                return `${_this.$t('learnActivity.simple.rac')} ${params.data.value.toFixed(0)}%(${_this.quData[params.dataIndex].correct}${_this.$t('unit.text7')})`
+                            }
+                        },
+                        grid: {
+                            // width: "100%",
+                            left: 50,
+                            right: 50
+                        },
+                        xAxis: {
+                            show: true,
+                            // name:'题号',
+                            nameLocation: 'end',
+                            type: 'category',
+                            data: [],
+                            axisLine: {
+                                lineStyle: {
+                                    color: "#AAA"
+                                }
+                            },
+                            axisLabel: {
+                                rotate: 0
+                            }
+                        },
+                        yAxis: {
+                            // name:'正确率',
+                            type: 'value',
+                            axisLine: {
+                                lineStyle: {
+                                    color: "#AAA"
+                                }
+                            },
+                        },
+                        series: [
+                            {
+                                data: [],
+                                type: 'bar',
+                                barMinWidth: 30,
+                                barMaxWidth: 30,
+                                label: {
+                                    show: true,
+                                    position: 'top',
+                                    formatter: (params) => {
+                                        //统计人数的算法
+                                        // if (_this.stuCount > 0) {
+                                        //     return `${(params.data * 100 / _this.stuCount).toFixed(1)}%`
+                                        // } else {
+                                        //     return params.data
+                                        // }
+                                        // console.log(params);
+                                        // 统计正确率的算法
+                                        return _this.isShowRate ? `${params.data.type ? '{typea|√}' : '{typeb|×}'}\n{count|${params.data.value.toFixed(0)}%}` : ''
+                                    },
+                                    rich: {
+                                        typea: {
+                                            color: '#00AD6C',
+                                            fontSize: 20,
+                                            // height: 24,
+                                            // padding: [0, 5, 0, 5],
+                                            align: 'center'
+                                        },
+                                        typeb: {
+                                            color: '#FF5508',
+                                            fontSize: 20,
+                                            // height: 24,
+                                            // padding: [0, 5, 0, 5],
+                                            align: 'center'
+                                        },
+                                        count: {
+                                            color: '#333',
+                                            height: 24,
+                                            // padding: [0, 5, 0, 5],
+                                            // align: 'right'
+                                        },
+                                    },
+                                },
+                                itemStyle: {
+                                    normal: {
+                                        color: function (params) {
+                                            const colorList = ['#ed4014', '#ff9900', '#19be6b']
+                                            //统计人数的算法
+                                            // if (_this.stuCount >= 0) {
+                                            //     let rate = params.data * 100 / _this.stuCount
+                                            //     console.log(params, rate)
+                                            //     if (rate >= 70) {
+                                            //         return colorList[2]
+                                            //     } else if (rate > 50) {
+                                            //         return colorList[1]
+                                            //     } else {
+                                            //         return colorList[0]
+                                            //     }
+                                            // } else {
+                                            //     return colorList[2]
+                                            // }
+                                            // 统计正确率的算法
+                                            if (params.data.value >= 70) {
+                                                return colorList[2]
+                                            } else if (params.data.value > 50) {
+                                                return colorList[1]
+                                            } else {
+                                                return colorList[0]
+                                            }
+
+                                        }
+                                    }
+                                },
+                                stillShowZeroSum: true,
+                            },
+                        ]
+                    }
+                    if (!this.progressPie) {
+                        this.progressPie = this.$echarts.init(document.getElementById(`class-score-count-${this.id}`), 'macarons')
+                    }
+                    this.option.series[0].data = this.quData.map(item => {
+                        return {
+                            value: item.correct * 100 / this.stuCount,
+                            type: item.type
+                        }
+                    })
+                    /* this.option.series[0].data = [
+                        {
+                            value: this.quData.map(item => item.correct * 100 / this.stuCount)
+                        },
+                        {
+                            value: this.quData.map(item => {return item.type})
+                        }
+                    ] */
+                    // this.option.series[0].data = this.quData.map(item => item.correct * 100 / this.stuCount) //计算正确率
+                    this.option.xAxis.data = this.quData.map(item => item.quLabel)
+                    // if (this.quData.length) this.option.yAxis.max = this.quData[0].correct + this.quData[0].wrong
+                    if (this.quData.length) this.option.yAxis.max = 100 //计算比例不算人数
+                    if (this.quData.length > 8) {
+                        this.option.dataZoom = [
+                            {
+                                show: true,
+                                height: 8,
+                                xAxisIndex: [
+                                    0
+                                ],
+                                bottom: 10,
+                                endValue: 30,
+                                startValue: 0,
+                                handleIcon: 'M512 497.821538m-418.264615 0a418.264615 418.264615 0 1 0 836.52923 0 418.264615 418.264615 0 1 0-836.52923 0Z',
+                                handleSize: '100%',
+                                handleStyle: {
+                                    color: '#d3dee5'
+
+                                },
+                                textStyle: {
+                                    color: '#fff'
+                                },
+                                borderRadius: '5px',
+                                maxValue: 30,
+                            },
+                            {
+                                type: 'inside',
+                                show: true,
+                                height: 15,
+                                endValue: 30,
+                                startValue: 0,
+                                maxValue: 30,
+                            }
+                        ]
+                    } else {
+                        this.option.dataZoom = []
+                    }
+                    this.progressPie.setOption(this.option, true)
+                    /* this.progressPie.on('datazoom', (params) => {
+                        let start = params.batch[0]?.start
+                        let end = params.batch[0]?.end
+                        if (end - start > 30) {
+                            this.isShowRate = false
+                        } else {
+                            this.isShowRate = true
+                        }
+                    }) */
+                })
+            },
+            deep: true,
+            immediate: true
+        },
+    }
+}
+</script>
+<style scoped lang="less">
+.qu-score {
+    padding: 15px 40px 0 40px;
+    width: 400px;
+    height: 250px;
+}
+.qu-score-count {
+    width: 100%;
+    height: 100%;
+}
+.legend-info {
+    text-align: center;
+    font-weight: bold;
+
+    & > span:not(:last-child) {
+        margin-right: 10px;
+    }
+}
+</style>

+ 359 - 0
TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/ExamTable.vue

@@ -0,0 +1,359 @@
+<template>
+    <div class="exam-table-wrap">
+        <!-- 班级人数 -->
+        <div class="data-count-item">
+            <p class="data-value">
+                {{ overviewInfo.total }}
+            </p>
+            <p class="data-text">
+                {{ $t('learnActivity.simple.classStuCount') }}
+            </p>
+        </div>
+        <!-- 得分题目数 -->
+        <div class="data-count-item">
+            <p class="data-value">
+                {{ overviewInfo.examNum }}
+            </p>
+            <p class="data-text">
+                {{ $t("studentWeb.exam.report.getScore") }}
+            </p>
+        </div>
+        <!-- 得分 -->
+        <div class="data-count-item">
+            <p class="data-value" style="color:#ed4014">
+                {{ overviewInfo.score }}
+            </p>
+            <p class="data-text">
+                {{ $t("studentWeb.exam.score") }}
+            </p>
+        </div>
+        <!-- 平均分 -->
+        <div class="data-count-item">
+            <p class="data-value">
+                {{ overviewInfo.average }}
+            </p>
+            <p class="data-text">
+                {{ $t("studentWeb.myAchievement.average") }}
+            </p>
+        </div>
+        <!-- 查看更多 -->
+        <div class="data-count-item view-more" @click="toEvDetail" style="width: 100%;">
+            <p class="data-value" style="color:#2d8cf0">
+                <Icon type="ios-more" />
+            </p>
+            <p class="data-text" style="color:#2d8cf0">
+                {{ $t("totalAnalysis.more") }}
+            </p>
+        </div>
+    </div>
+</template>
+<script>
+export default {
+    props: {
+        examInfo: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        },
+        examDetaiInfo: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        },
+        recordInfo: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        },
+    },
+    data() {
+        return {
+            currentPage: 1,
+            pageSize: 10,
+            pageSizeOpts: [5, 10, 20, 30, 40],
+            viewTableStatus: false,
+            dataLoading: true,
+            overviewInfo: {
+                total: 0,
+                examNum: 0,
+                score: 0,
+                average: 0,
+                noScore: 0
+            },
+            scoreList: [
+                {
+                    title: this.$t('learnActivity.score.column1'),
+                    slot: "name",
+                    fixed: "left",
+                    align: "center",
+                    width: 150,
+                },
+                {
+                    title: this.$t('learnActivity.score.column2'),
+                    slot: "total",
+                    align: "center",
+                    sortable: true,
+                    fixed: "right",
+                    width: 100
+                },
+                // {
+                //     title: this.$t('learnActivity.score.column3'),
+                //     slot: "status",
+                //     align: "center",
+                //     fixed: "right",
+                //     width: 130,
+                // }
+            ],
+            tableColumn: [],
+            studentScore: [],
+            originData: [],
+            students: [],
+            tableData: [],
+            quCount: 0
+        }
+    },
+    methods: {
+        toEvDetail() {
+            this.$router.push({
+                path: "/studentWeb/examView",
+                query: {aId: this.examInfo.ExamId}
+            })
+            /* this.$router.push({
+                path: '/home/evDetail',
+                query: {
+                    examId: this.examInfo.id,
+                    code: `Exam-${this.$store.state.userInfo.TEAMModelId}`
+                }
+            })  */ 
+        },
+        getStatusInfo(answer, score, status) {
+            //评测设置学生可以补考
+            if (status == 2 || status == 3) {
+                return {
+                    status: 3,
+                    statusText: this.$t('learnActivity.score.status5'),
+                    statusColor: '#2db7f5'
+                }
+            }
+
+            //评测结束,学生缺考
+            if (status == 1 && this.examDetaiInfo.progress == 'finish') {
+                return {
+                    status: 1,
+                    statusText: this.$t('learnActivity.score.status4'),
+                    statusColor: '#ed4014'
+                }
+            }
+
+            //评测进行中,未作答
+            if (status == 1 && this.examDetaiInfo.progress == 'going') {
+                return {
+                    status: 0,
+                    statusText: this.$t('learnActivity.score.status1'),
+                    statusColor: '#808695'
+                }
+            }
+            // status:0
+            //已作答,未评分
+            if (score.includes(-1)) {
+                return {
+                    status: 2,
+                    statusText: this.$t('learnActivity.score.status2'),
+                    statusColor: '#ff9900'
+                }
+            }
+            //已作答已评分
+            return {
+                status: 4,
+                statusText: this.$t('learnActivity.score.status3'),
+                statusColor: '#19be6b'
+            }
+        },
+        //分数求和
+        getcount(arr) {
+            return arr.reduce((total, item) => {
+                if (item !== -1) {
+                    return total + item;
+                } else {
+                    return total;
+                }
+            }, 0);
+        },
+        //初始化表单数据
+        setTableData(studentData, studentAns) {
+            console.log(arguments)
+            this.studentScore = []
+            this.tableColumn = [...this.scoreList]
+            this.quCount = studentAns.studentScores[0] ? studentAns.studentScores[0].length : 0
+            for (let i = 0; i < this.quCount; i++) {
+                let data = {
+                    title: "Q" + (i + 1),
+                    slot: "qu" + i,
+                    align: "center",
+                    minWidth: 70
+                }
+                this.tableColumn.push(data);
+            }
+            let ans = []
+            for (let i = 0; i < studentAns.studentIds.length; i++) {
+                for (let k = 0; k < studentData.length; k++) {
+                    let score = {}
+                    if (studentAns.studentIds[i] == studentData[k].id) {
+                        score.name = studentData[k].name
+                        score.type = studentData[k].type
+                        score.id = studentAns.studentIds[i]
+                        score.data = studentAns.studentScores[i]
+                        score.total = this.getcount(score.data)
+                        score.ansBlob = studentAns.studentAnswers[i]
+                        score.mark = studentAns.mark[i]
+                        let { status, statusText, statusColor } = this.getStatusInfo(studentAns.studentAnswers[i], studentAns.studentScores[i], studentAns.status[i])
+                        score.status = status
+                        score.statusText = statusText
+                        score.statusColor = statusColor
+                        this.studentScore.push(score)
+                    }
+                }
+            }
+            this.originData = this._.cloneDeep(this.studentScore)
+            this.students = this._.cloneDeep(this.studentScore)
+            this.pageChange(1)
+            if (ans.length) {
+                for (let k = 0; k < this.paperInfo.papers.item.length; k++) {
+                    this.$set(
+                        this.paperInfo.papers.item[k],
+                        "answerData",
+                        ans[k]
+                    );
+                    this.$set(
+                        this.paperInfo.papers.item[k],
+                        "stuScore",
+                        score[k]
+                    );
+                }
+            }
+        },
+        // 页面size变化
+        pageSizeChange(val) {
+            this.pageSize = val
+            this.pageChange(1)
+        },
+        // 分页页面变化
+        pageChange(page) {
+            let start = this.pageSize * (page - 1)
+            let end = this.pageSize * page
+            this.currentPage = page
+            this.tableData = this.studentScore.slice(start, end)
+        },
+        //获取学生作答详情数据
+        getStudentAnswer() {
+            this.dataLoading = true
+            let requestData = {
+                id: this.examDetaiInfo.id,
+                code: this.recordInfo.scope == 'school' ? this.$store.state.userInfo.schoolCode : this.$store.state.userInfo.TEAMModelId,
+                subjectId: this.recordInfo.subjectId,
+                classId: this.recordInfo.groupIds[0],
+            };
+            this.$api.learnActivity.FindAllStudent(requestData).then(
+                (res) => {
+                    if (res.examClassResults && res.examClassResults.length) {
+                        this.setTableData(res.ufos, res.examClassResults[0])
+                        this.calcOverView(res.examClassResults[0])
+                        this.dataLoading = false
+                        this.tableLoading = false
+                    }
+                    //如果首次没有获取到有效数据(数据暂未生成),则尝试重复活动5次
+                    else {
+                        this.$Message.error({
+                            content: this.$t('learnActivity.score.dataError'),
+                            duration: 3
+                        })
+                        this.dataLoading = false
+                        this.tableLoading = false
+                    }
+                },
+                (err) => {
+                    this.$t('learnActivity.score.dataError')
+                    this.dataLoading = false
+                    this.tableLoading = false
+                }
+            )
+        },
+        //计算总览数据
+        calcOverView(data) {
+            console.log(data);
+            // 班级总人数
+            data.total.forEach(item => {
+                item.forEach(num => {
+                    this.overviewInfo.total += num
+                })
+            })
+            // 得分
+            // 先取第一套试卷的分数
+            data.stuScore[0].forEach((item, index) => {
+                if(item != -1) {
+                    this.overviewInfo.score += item
+                }
+                // 得了满分才算做对
+                this.overviewInfo.examNum += ((item != -1 && item === data.papers[0].point[index]) ? 1 : 0)
+            })
+            this.overviewInfo.average = data.average[0]
+        },
+    },
+    watch: {
+        examDetaiInfo: {
+            deep: true,
+            immediate: true,
+            handler(n, o) {
+                console.log('评测数据', n)
+                if (n && n.status === 200) {
+                    this.calcOverView(n)
+                    // this.getStudentAnswer()
+                }
+            }
+        }
+    }
+}
+</script>
+<style lang="less" scoped>
+.exam-table-wrap {
+    margin-top: 5px;
+    padding: 10px;
+    width: 250px;
+    height: 270px;
+    box-shadow: 0px 2px 5px #e9e9e9;
+    display: flex;
+    flex-wrap: wrap;
+    align-items: center;
+}
+.data-count-item {
+    text-align: center;
+    width: 50%;
+    min-width: 115px;
+}
+.view-more {
+    cursor: pointer;
+    &:hover {
+        color: #2b85e4;
+    }
+}
+.stu-status-tag {
+    cursor: pointer;
+    font-size: 12px;
+    padding: 3px 8px;
+    font-weight: 800;
+    border-radius: 4px;
+    color: white;
+}
+.page-wrap {
+    float: right;
+    margin-top: 15px;
+}
+</style>
+<style lang="less">
+.exam-table-wrap .ivu-page-options-sizer {
+    margin-right: 0px;
+}
+</style>

+ 116 - 0
TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/Pick.vue

@@ -0,0 +1,116 @@
+<template>
+    <!-- 随机挑人 -->
+    <div class="pick-wrap" v-if="pickRes.length">
+        <p class="event-type">{{ $t("talMgmt.text40") }}:</p>
+        <div class="pick-item" v-for="(item,index) in pickRes" :key="index">
+            <p class="student-no">{{item.seatNo}}</p>
+            <p class="student-name">{{item.name}}</p>
+        </div>
+        <StudentClient></StudentClient>
+    </div>
+</template>
+<script>
+import StudentClient from '@/view/classrecord/eventchart/StudentClient.vue'
+export default {
+    components: {
+        StudentClient
+    },
+    props: {
+        pickData: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        },
+        students: {
+            type: Array,
+            default: () => {
+                return []
+            }
+        }
+    },
+    data() {
+        return {
+            // pickRes: []
+        }
+    },
+    methods: {
+        getChinese(strValue) {
+            if (!strValue) return ''
+            var reg = /[\u4e00-\u9fa5]/g;
+            var names = strValue.match(reg);
+            if (names) {
+                return names.join("").substring(names.length - 2)
+            } else {
+                return strValue.substr(0, 2)
+            }
+        }
+    },
+    computed: {
+        pickRes() {
+            let data = []
+            if (this.pickData.PickupMemberId) {
+                let r = JSON.parse(this.pickData.PickupMemberId)
+                if (r) {
+                    r.forEach(item => {
+                        let s = this.students.find(stu => stu.seatID == item && stu.id === this.$store.state.userInfo.sub)
+                        if(s) {
+                            data.push({
+                                seatNo: item,
+                                name: s ? s.name : item
+                            })
+                        }
+                    })
+                }
+            }
+            return data
+        }
+    }
+    // watch: {
+    //     pickData: {
+    //         deep: true,
+    //         immediate: true,
+    //         handler(n, o) {
+    //             console.log('挑人数据', n)
+    //             if (n.PickupMemberId) {
+    //                 this.pickRes = JSON.parse(n.PickupMemberId)
+    //             } else {
+    //                 this.pickRes = []
+    //             }
+    //         }
+    //     }
+    // }
+}
+</script>
+<style scoped lang="less">
+.pick-wrap {
+    display: flex;
+}
+.pick-item {
+    margin-right: 20px;
+    width: 80px;
+    text-align: center;
+    background: rgba(0, 0, 0, 0.3);
+    height: 80px;
+    border: 2px solid #ff9900;
+    border-radius: 8px;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    .student-no {
+        font-size: 30px;
+        color: black;
+        font-weight: 600;
+    }
+    .student-name {
+        color: #007cff;
+        font-weight: 800;
+        font-size: 14px;
+    }
+}
+.event-type {
+    margin-right: 20px;
+    font-size: 15px;
+    font-weight: 600;
+}
+</style>

+ 2 - 1
TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/Receive.vue

@@ -154,7 +154,8 @@ export default {
             if (this.rcvData.clientWorks) {
                 data = this._.cloneDeep(this.rcvData.clientWorks)
             }
-            let sas = await this.$tools.getBlobSas(this.recordInfo.tmdid)
+            let code = this.recordInfo.scope === "school" ? this.recordInfo.school : this.recordInfo.tmdid
+            let sas = await this.$tools.getBlobSas(code)
             data.forEach(stu => {
                 //处理完整文件路径
                 if (stu.blobFiles) {

+ 256 - 0
TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/ReceiveBack.vue

@@ -0,0 +1,256 @@
+<template>
+    <!-- 作品收集 -->
+    <div class="receive-wrap">
+        <TeacherClient></TeacherClient>
+        <div class="receive-wrap">
+            <p class="clt-type">
+                {{ rcvData.eventName }}:
+            </p>
+            <div class="receive-wrap">
+                <div v-for="(item, rIndex) in receiveData" :key="`r${rIndex}`" class="receive-item">
+                    <div v-if="collateType == 0">
+                        收集的作品
+                    </div>
+                    <!-- 图片类型作品 -->
+                    <div v-else-if="collateType == 1">
+                        <img class="receive-img" :src="item.url" alt="" @click="viewImage(item.url)">
+                    </div>
+                    <!-- HTEX作品类型 -->
+                    <div v-else-if="collateType == 2">
+                        收集的HTEX作品
+                    </div>
+                    <!-- 音频 -->
+                    <div v-else-if="collateType == 3">
+                        <div class="audio-box" @click="viewAudio(item.url)">
+                            <Icon class="collate-type-icon" custom="iconfont icon-audio-outline" />
+                        </div>
+                    </div>
+                    <!-- 视频 -->
+                    <div v-else-if="collateType == 4">
+                        <div class="audio-box" @click="viewAudio(item.url)">
+                            <Icon class="collate-type-icon" custom="iconfont icon-video-outline" />
+                        </div>
+                    </div>
+                    <!-- 文字 -->
+                    <div v-else-if="collateType == 5">
+                        收集的文字
+                    </div>
+                    <!-- 附件 -->
+                    <div v-else-if="collateType == 6">
+                        <div class="audio-box" @click="downloadFile(item.url, item.name)">
+                            <Icon class="collate-type-icon" custom="iconfont icon-file" />
+                        </div>
+                    </div>
+                    <p style="text-align: center;">{{ item.name }}</p>
+                </div>
+            </div>
+        </div>
+        <!-- <StudentClient></StudentClient> -->
+        <!--文件预览-->
+        <div v-if="previewStatus" class="image-viewer">
+            <div style="width:fit-content;position:relative;margin:auto;">
+                <Icon type="md-close" class="close-icon" @click="closePreview" />
+                <video v-if="collateType == 4" id="previewVideo" :src="prevUrl" width="870" controls="controls" style="max-height: 800px;">
+                    {{$t('teachContent.tips8')}}
+                </video>
+                <audio v-else-if="collateType == 3" controls>
+                    <source :src="prevUrl">
+                    {{$t('teachContent.notAudio')}}
+                </audio>
+            </div>
+        </div>
+    </div>
+</template>
+<script>
+import StudentClient from '@/view/classrecord/eventchart/StudentClient.vue'
+import TeacherClient from '@/view/classrecord/eventchart/TeacherClient.vue'
+export default {
+    props: {
+        recordInfo: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        },
+        rcvData: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        },
+        students: {
+            type: Array,
+            default: () => {
+                return []
+            }
+        }
+    },
+    components: {
+        StudentClient, TeacherClient
+    },
+    data() {
+        return {
+            prevUrl: '',
+            previewStatus: false,
+            collateType: 0,
+            cltTypeMap: {
+                0: {
+                    type: 'None',
+                    text: ''
+                },
+                1: {
+                    type: 'Image',
+                    text: '图片'
+                },
+                2: {
+                    type: 'Htex',
+                    text: 'HTEX'
+                },
+                3: {
+                    type: 'Audio',
+                    text: '音频'
+                },
+                4: {
+                    type: 'Video',
+                    text: '视频'
+                },
+                5: {
+                    type: 'Text',
+                    text: '文字'
+                },
+                6: {
+                    type: 'File',
+                    text: '附件'
+                },
+            },
+            receiveData: []
+        }
+    },
+    methods: {
+        viewAudio(url) {
+            this.prevUrl = url
+            this.previewStatus = true
+        },
+        closePreview() {
+            this.previewStatus = false
+        },
+        viewImage(url) {
+            this.$hevueImgPreview(url)
+        },
+        downloadFile(url, name) {
+            const downloadRes = async () => {
+                let response = await fetch(url); // 内容转变成blob地址
+                let blob = await response.blob(); // 创建隐藏的可下载链接
+                let objectUrl = window.URL.createObjectURL(blob);
+                let a = document.createElement('a');
+                a.href = objectUrl;
+                let fileName = url.substring(url.lastIndexOf('/') + 1, url.lastIndexOf('?'))
+                a.download = `${this.rcvData.eventName}-${name}-${fileName}`
+                a.click()
+                a.remove();
+            }
+            downloadRes();
+        },
+        async getSas(data) {
+            // let data = this.rcvData
+            let code = this.recordInfo.scope === "school" ? this.recordInfo.school : this.recordInfo.tmdid
+            let sas = await this.$tools.getBlobSas(code)
+            if(data.Works) {
+                data.Works.forEach(stu => {
+                    //处理完整文件路径
+                    let urls = `${sas.url}/${sas.name}/records/${this.recordInfo.id}${stu}?${sas.sas}`
+                    let stuId = stu.substring(stu.lastIndexOf('/Clients/') + 9, stu.lastIndexOf('/Task'))
+                    let stuInfo = this.students.find(item => {
+                        return item.id === stuId
+                    })
+                    stuInfo.url = urls
+                    this.receiveData.push(stuInfo)
+                })
+            }
+        },
+    },
+    created () {
+    },
+    computed: {
+    },
+    watch: {
+        rcvData: {
+            deep: true,
+            immediate: true,
+            handler(n, o) {
+                this.receiveData = []
+                this.collateType = n.WrkType
+                this.getSas(n)
+            }
+        }
+    }
+}
+</script>
+<style lang="less" scoped>
+.image-viewer {
+    background-color: rgba(0, 0, 0, 0.8);
+    z-index: 9999;
+    width: 100%;
+    height: 100%;
+    position: fixed;
+    top: 0;
+    left: 0;
+    overflow-y: scroll;
+    overflow-x: hidden;
+    text-align: center;
+    padding: 50px;
+    padding-top: 8%;
+    .close-icon {
+        position: absolute;
+        right: -16px;
+        top: -16px;
+        font-size: 24px;
+        color: black;
+        cursor: pointer;
+        border-radius: 50px;
+        background: white;
+        padding: 2px;
+        z-index: 9999;
+    }
+}
+.receive-wrap {
+    display: flex;
+}
+.receive-item {
+    margin-right: 20px;
+}
+.receive-img {
+    max-width: 150px;
+    max-height: 150px;
+    border: 1px solid #eeeeee;
+    cursor: pointer;
+}
+.audio-box {
+    width: 80px;
+    height: 80px;
+    border: 1px solid #eeeeee;
+    background: #f3f3f3;
+    text-align: center;
+    line-height: 80px;
+    border-radius: 5px;
+    cursor: pointer;
+    &:hover {
+        // background: #f9f9f9;
+        border-color: #2d8cf0;
+    }
+    &:hover .collate-type-icon {
+        color: #2d8cf0;
+    }
+}
+.collate-type-icon {
+    font-size: 30px;
+}
+.clt-type {
+    /* margin-right: 10px;
+    font-size: 15px;
+    font-weight: 600; */
+
+    display: inline-block;
+    vertical-align: top;
+}
+</style>

+ 204 - 0
TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/ScoreBarChart.vue

@@ -0,0 +1,204 @@
+<template>
+    <div class="score-bar-chart">
+        <!-- <h4>{{ $t("studentWeb.exam.chart.scoreDistribution") }}</h4> -->
+        
+        <!-- <div class="no-data">
+            <p class="no-data-title">{{ $t("studentWeb.exam.chart.scoreDistribution") }}</p>
+        </div> -->
+        <!-- <div class="attendance">
+            {{ $t("studentWeb.exam.chart.participant") }}
+            <span class="timeNum">{{ Attendance }}</span>
+            {{ $t("studentWeb.exam.chart.student") }}
+        </div> -->
+        <div class="score-stu" :id="`scoreStu-${id}`"></div>
+    </div>
+</template>
+
+<script>
+export default {
+    name: "ScoreBarChart",
+
+    computed: {
+        Attendance: function () {
+            let att = 0
+            this.total[0].data.map(item => {
+                att += item
+            })
+            return att
+        },
+    },
+    props: {
+        total: {
+            type: Array,
+            default: () => {
+                return []
+            }
+        },
+        subjectNames: {
+            type: Array,
+            default: () => {
+                return []
+            }
+        },
+    },
+    data() {
+        return {
+            id: "",
+        }
+    },
+    created () {
+        this.id = this.$jsFn.getBtwRandom(0, 100000000)
+    },
+    methods: {
+        setMap() {
+            // return new Promise((r, j) => {
+                let myChart = this.$echarts.init(document.getElementById(`scoreStu-${this.id}`), 'macarons')
+                let option = {
+                    title: {
+                        text: this.$t("studentWeb.exam.chart.scoreDistribution"),
+                        left: 'center',
+                        "top": 0,
+                        textStyle: {
+                            // fontWeight: "normal",
+                            fontSize: "14",
+                            color: "#484848",
+                        },
+                        // show: false,
+                    },
+                    tooltip: {
+                        trigger: "item",
+                        padding: [4, 12],
+                        // backgroundColor: "white",
+                        textStyle: {
+                            // color: "black",
+                            // fontFamily: "Ariel",
+                            // fontWeight: "bolder",
+                        },
+                        formatter: `{b0}${this.$t('studentWeb.exam.chart.score')}: <span>{c0}</span>${this.$t('studentWeb.exam.chart.student')}`,
+                    },
+                    /* grid: {
+                        top: "20%",
+                        left: "6%",
+                        right: "12%",
+                        bottom: "18%",
+                        containLabel: true,
+                    }, */
+                    xAxis: {
+                        type: "category",
+                        data: ['0-59', '59-70', '70-80', '80-90', '90-100'],
+                        splitLine: {
+                            lineStyle: {
+                                color: "transparent",
+                            },
+                        },
+                        //座標軸線的設置
+                        axisLine: {
+                            lineStyle: {
+                                color: "gray",
+                                width: 1,
+                            },
+                        },
+                        //座標軸的刻度顏色
+                        axisLabel: {
+                            color: "black",
+                        },
+                    },
+                    yAxis: {
+                        type: "value",
+                        show: true,
+                        //座標軸線的設置
+                        axisLine: {
+                            lineStyle: {
+                                color: "transparent",
+                            },
+                        },
+                        //座標軸的刻度顏色
+                        axisLabel: {
+                            color: "black",
+                        },
+                    },
+                    legend: {
+                        data: this.subjectNames,
+                        bottom: 0,
+                    },
+                    barCategoryGap: "1px",
+                    series: this.total/* [
+                        {
+                            data: this.total,
+                            type: "bar",
+                            // itemStyle: { color: "#73a373" }
+                        },
+                    ] */,
+                }
+                console.log(option.series);
+                myChart.setOption(option)
+            // })
+        },
+    },
+    watch: {
+        total: {
+            deep: true,
+            immediate: true,
+            handler(n, o) {
+                
+            },
+        },
+    },
+    mounted () {
+        this.setMap()
+    }
+}
+</script>
+
+<style scoped>
+.score-bar-chart {
+    width: 400px;
+    /* margin: auto; */
+    /* margin-bottom: 50px; */
+    padding: 15px 40px 0px 40px;
+    height: 270px;
+    color: rgba(0, 0, 0, 0.726);
+}
+
+.score-bar-chart .no-data .no-data-title {
+    color: #484848;
+    font-weight: bolder;
+}
+
+.attendance {
+    position: relative;
+    top: 10px;
+    margin-top: -20px;
+    text-align: right;
+    padding-left: 12%;
+}
+.avatar {
+    width: 32px;
+    height: 32px;
+    position: relative;
+    top: -60px;
+    left: 26.5%;
+}
+.attendance .timeNum {
+    color: #575757;
+    font-weight: 500;
+    font-size: 44px;
+}
+
+.score-stu {
+    width: 306px;
+    height: 250px;
+}
+
+@media screen and (max-width: 1365px) {
+    .score-stu {
+        width: 180px;
+    }
+}
+
+@media screen and (max-width: 995px) {
+    .score-stu {
+        width: 306px;
+    }
+}
+</style>

+ 1 - 0
TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/StuReceive.vue

@@ -254,5 +254,6 @@ export default {
     margin-right: 10px;
     font-size: 15px;
     font-weight: 600;
+    line-height: 40px;
 }
 </style>

+ 230 - 0
TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/myWorks.vue

@@ -0,0 +1,230 @@
+<template>
+    <!-- 作品收集 -->
+    <div class="receive-wrap">
+        <div v-for="(item, index) in receiveData" :key="index">
+            <p>
+                {{ $t("studentWeb.myAchievement.hwName") }}:{{ item.jobName }}
+            </p>
+            <div v-if="item.collateType === 0">
+                收集的作品
+            </div>
+            <!-- 图片类型作品 -->
+            <div v-else-if="item.collateType === 'Image'">
+                <img v-for="(blob, Bindex) in item.blobFiles" :key="Bindex" class="receive-img" :src="blob">
+            </div>
+            <!-- HTEX作品类型 -->
+            <div v-else-if="item.collateType === 'Htex'">
+                收集的HTEX作品
+            </div>
+            <!-- 音频 -->
+            <div v-else-if="item.collateType === 'Audio'">
+                <div v-for="(blob, Aindex) in item.blobFiles" :key="Aindex" class="audio-box">
+                    <audio controls>
+                        <source :src="blob">
+                        {{$t('teachContent.notAudio')}}
+                    </audio>
+                </div>
+            </div>
+            <!-- 视频 -->
+            <div v-else-if="item.collateType === 'Video'">
+                <div v-for="(blob, Vindex) in item.blobFiles" :key="Vindex" class="audio-box">
+                    <video :src="blob" width="870" controls="controls" style="max-height: 800px;">
+                        {{$t('teachContent.tips8')}}
+                    </video>
+                </div>
+            </div>
+            <!-- 文字 -->
+            <div v-else-if="item.collateType === 'Text'">
+                收集的文字
+            </div>
+            <!-- 附件 -->
+            <div v-else-if="item.collateType === 'File'">
+                <div v-for="(blob, Findex) in item.blobFiles" :key="Findex" class="audio-box" @click="downloadFile(blob,item)">
+                    <Icon class="collate-type-icon" custom="iconfont icon-file" />
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+<script>
+export default {
+    props: {
+        recordInfo: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        },
+        rcvData: {
+            type: Array,
+            default: () => {
+                return []
+            }
+        },
+        students: {
+            type: Array,
+            default: () => {
+                return []
+            }
+        }
+    },
+    data() {
+        return {
+            prevUrl: '',
+            previewStatus: false,
+            // collateType: 0,
+            cltTypeMap: {
+                0: {
+                    type: 'None',
+                    text: ''
+                },
+                1: {
+                    type: 'Image',
+                    text: '图片'
+                },
+                2: {
+                    type: 'Htex',
+                    text: 'HTEX'
+                },
+                3: {
+                    type: 'Audio',
+                    text: '音频'
+                },
+                4: {
+                    type: 'Video',
+                    text: '视频'
+                },
+                5: {
+                    type: 'Text',
+                    text: '文字'
+                },
+                6: {
+                    type: 'File',
+                    text: '附件'
+                },
+            },
+            receiveData: []
+        }
+    },
+    methods: {
+        viewAudio(url) {
+            this.prevUrl = url
+            this.previewStatus = true
+        },
+        closePreview() {
+            this.previewStatus = false
+        },
+        viewImage(url) {
+            this.$hevueImgPreview(url)
+        },
+        downloadFile(url, taskInfo) {
+            const downloadRes = async () => {
+                let response = await fetch(url); // 内容转变成blob地址
+                let blob = await response.blob(); // 创建隐藏的可下载链接
+                let objectUrl = window.URL.createObjectURL(blob);
+                let a = document.createElement('a');
+                a.href = objectUrl;
+                let fileName = url.substring(url.lastIndexOf('/' + 1), url.lastIndexOf('?'))
+                a.download = `${this.rcvData.jobName}-${taskInfo.seatID}-${fileName}`
+                a.click()
+                a.remove();
+            }
+            downloadRes();
+        },
+        async getSas() {
+            let data = this._.cloneDeep(this.rcvData)
+            let code = this.recordInfo.scope === "school" ? this.recordInfo.school : this.recordInfo.tmdid
+            let sas = await this.$tools.getBlobSas(code)
+            data.forEach(stu => {
+                //处理完整文件路径
+                if (stu.blobFiles) {
+                    stu.blobFiles = stu.blobFiles.map(f => {
+                        return `${sas.url}/${sas.name}/records/${this.recordInfo.id}${f}?${sas.sas}`
+                    })
+                }
+            })
+            this.receiveData = data
+        },
+    },
+    created () {
+        this.getSas()
+    },
+    computed: {
+    },
+    watch: {
+        rcvData: {
+            deep: true,
+            immediate: true,
+            handler(n, o) {
+                // this.collateType = n.collateType
+            }
+        }
+    }
+}
+</script>
+<style lang="less" scoped>
+.image-viewer {
+    background-color: rgba(0, 0, 0, 0.8);
+    z-index: 9999;
+    width: 100%;
+    height: 100%;
+    position: fixed;
+    top: 0;
+    left: 0;
+    overflow-y: scroll;
+    overflow-x: hidden;
+    text-align: center;
+    padding: 50px;
+    padding-top: 8%;
+    .close-icon {
+        position: absolute;
+        right: -16px;
+        top: -16px;
+        font-size: 24px;
+        color: black;
+        cursor: pointer;
+        border-radius: 50px;
+        background: white;
+        padding: 2px;
+        z-index: 9999;
+    }
+}
+.receive-wrap {
+    display: flex;
+}
+.receive-item {
+    margin-right: 20px;
+}
+.receive-img {
+    max-width: 150px;
+    max-height: 150px;
+    border: 1px solid #eeeeee;
+    cursor: pointer;
+}
+.audio-box {
+    width: 80px;
+    height: 80px;
+    border: 1px solid #eeeeee;
+    background: #f3f3f3;
+    text-align: center;
+    line-height: 80px;
+    border-radius: 5px;
+    cursor: pointer;
+    &:hover {
+        // background: #f9f9f9;
+        border-color: #2d8cf0;
+    }
+    &:hover .collate-type-icon {
+        color: #2d8cf0;
+    }
+}
+.collate-type-icon {
+    font-size: 30px;
+}
+.clt-type {
+    margin-right: 10px;
+    font-size: 15px;
+    font-weight: 600;
+    line-height: 40px;
+}
+</style>

+ 529 - 0
TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/newClassRecord.less

@@ -0,0 +1,529 @@
+
+.courseware-wrap {
+    // width: 50%;
+    position: relative;
+    height: 450px;
+    background-color: #808080;
+    // margin-right: 5px;
+
+    .page-wrap {
+        opacity: 1;
+    }
+
+    .cur-page-tag {
+        opacity: 0;
+    }
+
+    .course-cur-img {
+        height: 100%;
+    }
+}
+
+.cur-page-tag {
+    position: absolute;
+    left: 5px;
+    bottom: 5px;
+    width: 30px;
+    height: 30px;
+    font-size: 18px;
+    line-height: 30px;
+    display: block;
+    background: rgba(255, 153, 0, 0.9);
+    border-radius: 50%;
+    text-align: center;
+    opacity: 1;
+    transition: opacity 0.2s;
+    color: white;
+    box-shadow: 0px 0px 5px rgb(255, 153, 0);
+}
+
+.page-wrap {
+    left: 0px;
+    bottom: 0px;
+    position: absolute;
+    padding: 6px;
+    color: white;
+    width: 100%;
+    height: 36px;
+    background: rgba(43, 51, 63, 0.7);
+    text-align: center;
+    opacity: 0;
+    transition: opacity 1.2s;
+    border-right: 2px solid black;
+}
+
+.record-info {
+    background-color: #fff;
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    top: 0;
+    left: 0;
+    z-index: 10;
+
+    .record-head {
+        display: flex;
+        justify-content: space-between;
+
+        &>div>p {
+            margin-bottom: 5px;
+        }
+
+        .attend-type {
+            transform: rotate(338deg);
+            font-size: 17px;
+            font-weight: bold;
+        }
+    }
+
+    .testTitle {
+        background-color: rgb(255, 255, 255);
+        // color: #24b880;
+        font-size: 20px;
+        padding: 5px 15px;
+        position: fixed;
+        width: 100%;
+        height: 44px;
+        border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+        z-index: 10;
+
+        .logoutIcon {
+            cursor: pointer;
+            margin-right: 10px;
+        }
+    }
+
+    img {
+        border-radius: 4px;
+        margin-right: 20px;
+        border: 1px solid rgba(173, 173, 173, 0.233);
+        width: 100%;
+    }
+
+    .count-box {
+        justify-content: center;
+        display: flex;
+        position: relative;
+    }
+
+    .video-player-box {
+        // width: 50%;
+        justify-content: center;
+        height: 450px;
+        display: flex;
+        // margin-left: 5px;
+        position: relative;
+        // background-color: #fff;
+    }
+
+    .message-area {
+        position: relative;
+        height: 75vh;
+        padding: 10px;
+        margin-bottom: 20px;
+
+        .filter-type {
+            position: absolute;
+            right: 0;
+            top: 10px;
+            padding-right: 10px;
+            padding-left: 10px;
+            text-align: center;
+            z-index: 9;
+            display: flex;
+            flex-direction: column;
+            background-color: #F1F1F1;
+
+            span {
+                border: none;
+                padding: 0px;
+                font-size: 32px;
+
+                &:hover {
+                    cursor: pointer;
+                    color: #24b880;
+                }
+
+                .select-filter {
+                    color: #24b880;
+                }
+            }
+        }
+
+        .message-box {
+            display: flex;
+            border-bottom: 1px #ccc solid;
+
+            .message-page {
+                // width: 10%;
+                padding: 10px 15px;
+                padding-bottom: 0;
+                // border-right: 1px #ccc dashed;
+
+                img {
+                    margin-top: 5px;
+                    margin-right: 0;
+                    width: 130px;
+                    cursor: pointer;
+                }
+
+                .messagetoPPT-tag {
+                    background-color: rgba(0, 0, 0, 0.1);
+                    white-space: initial;
+                    display: block;
+                    font-size: 14px;
+                    height: auto;
+                    padding: 3px 10px;
+                    border-radius: 4px;
+
+                    &:hover {
+                        cursor: pointer;
+                        color: #ffffff;
+                        background-color: #24b880;
+                    }
+                }
+            }
+
+            .message-record {
+                padding: 10px 15px;
+                padding-bottom: 0;
+                width: calc(100% - 160px);
+                // min-width: 780px;
+
+                .message-item {
+                    white-space: nowrap;
+                    display: block;
+                    position: relative;
+                    padding-top: 20px;
+                    margin-bottom: 50px;
+
+                    .message-avatar {
+                        border-radius: 50%;
+                        float: left;
+                        width: 32px !important;
+                        height: 32px !important;
+                        margin: 0px 15px;
+                        margin-top: -10px;
+                    }
+
+                    .user-name {
+                        white-space: nowrap;
+                        font-weight: bolder;
+                        position: absolute;
+                        top: 0;
+                        left: 55px;
+                        // position: absolute;
+                        // top: -12px;
+                        // margin-left: -8px;
+                    }
+
+                    .message-content {
+                        background: #d4ede1;
+                        padding: 10px;
+                        display: inline-block;
+                        border-radius: 4px;
+                        position: relative;
+                        // width: 85%;
+
+                        &::before {
+                            content: "";
+                            position: absolute;
+                            top: 3px;
+                            // margin-left: -16px;
+                            left: -7px;
+                            width: 0;
+                            height: 0;
+                            border-style: solid;
+                            border-width: 5px 7.7px 5px 0;
+                            border-color: transparent #d4ede1 transparent transparent;
+                        }
+
+                        .message-text {
+                            img {
+                                max-width: 80%;
+                            }
+
+                            .pick-item {
+                                width: 100px;
+                                text-align: center;
+                                background: rgba(0, 0, 0, 0.3);
+                                height: 100px;
+                                border: 2px solid #ff9900;
+                                border-radius: 8px;
+                                display: flex;
+                                flex-direction: column;
+                                justify-content: center;
+
+                                .student-no {
+                                    font-size: 40px;
+                                    color: black;
+                                    font-weight: 600;
+                                }
+
+                                .student-name {
+                                    color: #007cff;
+                                    font-weight: 800;
+                                    font-size: 16px;
+                                }
+                            }
+                        }
+
+                    }
+
+                    .message-time {
+                        white-space: nowrap;
+                        color: gray;
+                        font-size: 10px;
+                        font-weight: 400;
+                        // position: absolute;
+                        // bottom: -20px;
+                        // margin-left: -8px;
+                        position: absolute;
+                        // bottom: 0;
+                        left: 55px;
+                    }
+                }
+
+                .teacher-item {
+                    display: block;
+                    // padding-right: 5%;
+                    text-align: right !important;
+                    position: relative;
+                    margin-bottom: 50px;
+
+                    .message-avatar {
+                        border-radius: 50%;
+                        float: left;
+                        width: 32px !important;
+                        height: 32px !important;
+                        margin: 0px 15px;
+                        margin-top: -10px;
+                    }
+
+                    .user-name {
+                        white-space: nowrap;
+                        font-weight: bolder;
+                        position: absolute;
+                        top: 0;
+                        left: 55px;
+                        // position: absolute;
+                        // top: -12px;
+                        // margin-left: -8px;
+                    }
+
+                    .message-content {
+                        background: #ffecc2;
+                        padding: 10px;
+                        display: inline-block;
+                        border-radius: 4px;
+                        position: relative;
+                        // width: 85%;
+
+                        &::before {
+                            content: "";
+                            position: absolute;
+                            top: 3px;
+                            // margin-left: -16px;
+                            right: -9px;
+                            width: 0;
+                            height: 0;
+                            border-style: solid;
+                            border-width: 5px 0 5px 8.7px;
+                            border-color: transparent transparent transparent #ffecc2;
+                        }
+
+                        .message-text {
+                            img {
+                                max-width: 80%;
+                            }
+
+                            .pick-item {
+                                width: 100px;
+                                text-align: center;
+                                background: rgba(0, 0, 0, 0.3);
+                                height: 100px;
+                                border: 2px solid #ff9900;
+                                border-radius: 8px;
+                                display: flex;
+                                flex-direction: column;
+                                justify-content: center;
+
+                                .student-no {
+                                    font-size: 40px;
+                                    color: black;
+                                    font-weight: 600;
+                                }
+
+                                .student-name {
+                                    color: #007cff;
+                                    font-weight: 800;
+                                    font-size: 16px;
+                                }
+                            }
+                        }
+
+                    }
+
+                    .message-time {
+                        white-space: nowrap;
+                        color: gray;
+                        font-size: 10px;
+                        font-weight: 400;
+                        // position: absolute;
+                        // bottom: -20px;
+                        // margin-left: -8px;
+                        position: absolute;
+                        // bottom: 0;
+                        right: 55px;
+                    }
+                }
+
+                .teacher-client-icon {
+                    font-size: 30px;
+                    padding: 5px;
+                    color: #ffffff;
+                    border-radius: 50%;
+                    background: #19be6b;
+                    margin-right: 15px;
+                    float: left;
+                }
+
+                .student-client-icon {
+                    font-size: 30px;
+                    padding: 5px;
+                    color: #ffffff;
+                    border-radius: 50%;
+                    background: #efbb49;
+                    float: right;
+                    margin-left: 15px;
+                }
+
+                .event-item {
+                    border: 1px dashed transparent;
+                    margin-bottom: 15px;
+                    padding: 5px 5px;
+                    overflow-x: scroll;
+                    
+                    &:hover {
+                        border: 1px dashed #e0e0e0;
+                    }
+                }
+
+                .student-event {
+                    display: flex;
+                    justify-content: end;
+                }
+            }
+        }
+    }
+
+    .dec {
+        padding: 0px;
+        // height: 800px;
+        overflow: hidden;
+        position: relative;
+
+        .no-interaction {
+            text-align: center;
+            font-size: 20px;
+            margin-top: 80px;
+            font-weight: bold;
+        }
+    }
+
+    .title-rect-name>span {
+        background-color: #24B880;
+        font-size: 12px;
+        padding: 3px;
+        border: 1px solid #ccc;
+        border-radius: 4px;
+        color: #fff;
+        cursor: pointer;
+    }
+
+    .e-note-tag {
+        font-size: 16px;
+        padding: 2px 5px;
+        margin-left: 30px;
+        vertical-align: top;
+        cursor: pointer;
+        user-select: none;
+
+        &:hover {
+            color: #24b880;
+        }
+    }
+
+    .record-content {
+        display: flex;
+        justify-content: space-between;
+        
+        .record-left {
+            width: 36%;
+            height: 85vh;
+
+            & > div{
+                margin-top: 10px;
+            }
+        }
+
+        .record-right {
+            width: 63%;
+        }
+    }
+
+}
+
+.image-viewer {
+    background-color: rgba(0, 0, 0, 0.8);
+    z-index: 9999;
+    width: 100%;
+    height: 100%;
+    position: fixed;
+    top: 0;
+    left: 0;
+    overflow-y: scroll;
+    overflow-x: hidden;
+    text-align: center;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    img {
+        max-width: 50%;
+        margin: 5% auto;
+        border: none;
+    }
+
+    .close-icon {
+        position: absolute;
+        right: 15px;
+        top: 10px;
+        font-size: 20px;
+        color: white;
+        cursor: pointer;
+    }
+}
+
+.no-video-tips {
+    position: absolute;
+    top: 0px;
+    left: 0px;
+    color: #ff9900;
+    width: 100%;
+}
+
+@media screen and (max-width: 768px) {
+    .record-info .record-content {
+        display: block;
+
+        .record-left,
+        .record-right {
+            width: 100%;
+        }
+
+        .record-left {
+            margin-bottom: 30px;
+        }
+    }
+}

+ 789 - 0
TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/newClassRecord.vue

@@ -0,0 +1,789 @@
+<template>
+    <div class="record-info">
+        <Loading v-show="isLoad" bgColor="rgba(0, 0, 0, 0.3)"></Loading>
+        <div class="testTitle">
+            <span class="logoutIcon" @click="quitRec">
+                <svg-icon icon-class="logout" />
+            </span>
+            <span class="testTitleText">{{$t("studentWeb.courseContent.classRecord")}}</span>
+        </div>
+        <div style="margin-top: 44px; padding: 10px 15px;">
+            <h2 class="event-title" v-if="recordInfo">{{ recordInfo.name }}</h2>
+            <div style="text-align: right; margin-bottom: 5px;">
+                <Button type="primary" @click="showWorks = true" :disabled="!myWorks.length">
+                    <Icon custom="iconfont icon-zuopin" size="16" />
+                    {{ $t('studentWeb.courseContent.myWorks') }}
+                </Button>
+                <Button type="primary" @click="showNote = true" v-if="recordInfo.myNote.length">
+                    <Icon custom="iconfont icon-myNote" size="17" />
+                    {{ $t('studentWeb.courseContent.mynotes') }}
+                </Button>
+                <Button type="primary" @click="viewENote">
+                    <Icon custom="iconfont icon-activityT" />
+                    {{ $t('studentWeb.courseContent.notes') }}
+                </Button>
+                <Button type="primary" @click="isShowVd = !isShowVd" v-show="hasVideo">
+                    <Icon :type="isShowVd ? 'md-podium' : 'logo-youtube'" />
+                    {{ isShowVd ? $t('cusMgt.rcd.dataCount') : $t('cusMgt.rcd.videoData') }}
+                </Button>
+            </div>
+            <div class="record-content">
+                <div class="record-left">
+                    <vuescroll>
+                        <div style="padding-right: 10px;">
+                            <div class="record-head" v-if="recordInfo">
+                                <div>
+                                    <p style="margin-left: 20px;">
+                                        <Icon type="ios-contact-outline" style="font-weight: bold;" class="base-info-icon" />{{ $t('studentWeb.baseInfo.teacher') }}
+                                        <span class="base-info-text">{{ recordInfo.tmdname }}</span>
+                                    </p>
+                                    <template v-if="courseNow">
+                                        <p style="margin-left: 20px;">
+                                            <svg-icon class="base-info-icon" icon-class="course" />{{ $t('studentWeb.baseInfo.subjectName') }}
+                                            <span class="base-info-text">{{ courseNow.name }}</span>
+                                        </p>
+                                        <p style="margin-left: 20px;">
+                                            <Icon custom="iconfont icon-mingdan" class="base-info-icon" />{{ $t('studentWeb.baseInfo.stuList') }}
+                                            <template v-if="courseNow.className.length">
+                                                <span class="base-info-text" v-for="(item, index) in courseNow.className" :key="index" style="margin-right: 10px;">{{ item.name }}</span>
+                                            </template>
+                                        </p>
+                                    </template>
+                                    <p style="margin-left: 20px;">
+                                        <Icon type="md-timer" class="base-info-icon" />{{ $t('studentWeb.baseInfo.duration') }}:
+                                        <span class="base-info-text">{{ recordInfo.time }}</span>
+                                    </p>
+                                    <p style="margin-left: 20px;">
+                                        <svg-icon icon-class="time" class="base-info-icon" />{{$t('studentWeb.baseInfo.classTime')}}:
+                                        <span class="base-info-text">{{ recordInfo.startTime }}</span>
+                                    </p>
+                                </div>
+                                <div>
+                                    <i-circle :percent="100" :size="100" :stroke-width="10" :stroke-color="attentColor[attendType]">
+                                        <p class="attend-type">{{ $t(`studentWeb.hiteachNote.dataCount.attendTypeList[${attendType}]`) }}</p>
+                                    </i-circle>
+                                </div>
+                            </div>
+                            <!-- <div class="count-box">
+                                <Alert v-show="!hasVideo" class="no-video-tips" type="warning" show-icon>
+                                    {{$t('cusMgt.rcd.noVideo')}}
+                                </Alert>
+                                <DataCount :nowStuInfo="nowStuInfo" :rcdInfo="baseData" v-if="baseData"></DataCount>
+                            </div> -->
+                            
+                            <div v-if="hasVideo" v-show="isShowVd" class="video-player-box">
+                                <video style="width: 100%;" id="recordVideo" class="video-js vjs-default-skin" type="video/mp4"></video>
+                            </div>
+                            <div v-show="!isShowVd" class="video-player-box" style="padding:25px 0px">
+                                <Alert v-show="!hasVideo" class="no-video-tips" type="warning" show-icon>
+                                    {{$t('cusMgt.rcd.noVideo')}}
+                                </Alert>
+                                <DataCount :nowStuInfo="nowStuInfo" :rcdInfo="baseData" v-if="baseData"></DataCount>
+                            </div>
+                            <div class="courseware-wrap">
+                                <!-- <DrawHTEX :mapJson="mapJson"></DrawHTEX> -->
+                                <img :src="curImg" alt="" class="course-cur-img">
+                                <div class="page-wrap">
+                                    <Page :total="pageList.length" :current="curPage" :page-size="1" size="small" @on-change="getCurHTEX" />
+                                    <!-- <Icon v-if="pageList.length" type="md-qr-scanner" class="full-screen-icon" @click="viewHtex" /> -->
+                                </div>
+                                <!-- <span class="cur-page-tag">{{ curPage }}</span> -->
+                            </div>
+                        </div>
+                    </vuescroll>
+                </div>
+                <div class="record-right">
+                    <div>
+                        <h2 class="title-rect-name">
+                            {{ $t("studentWeb.hiteachNote.classInteractionRecord") }}
+                        </h2>
+                        <div style="width: 100%; display: flex; justify-content: space-between;">
+                            <div>
+                                <!-- <Button type="primary">时间</Button>
+                                <Button type="primary">页次</Button> -->
+                            </div>
+                            <div>
+                                <Button type="warning" @click="filterFn('all')">{{ $t("cusMgt.rcd.filter1") }}</Button>
+                                <Button type="warning" :disabled="!filtertype.push" @click="filterFn('push')">{{ $t("cusMgt.rcd.filter2") }}({{ filtertype.push }})</Button>
+                                <Button type="warning" :disabled="!filtertype.task" @click="filterFn('task')">{{ $t("cusMgt.rcd.filter3") }}({{ filtertype.task }})</Button>
+                                <Button type="warning" :disabled="!filtertype.irs" @click="filterFn('irs')">{{ $t("cusMgt.rcd.filter4") }}({{ filtertype.irs }})</Button>
+                                <Button type="warning" :disabled="!filtertype.exam" @click="filterFn('exam')">{{ $t("cusMgt.rcd.filter5") }}({{ filtertype.exam }})</Button>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="dec">
+                        <div class="message-area" v-if="haveInteraction">
+                            <vuescroll ref="datawrap">
+                                <div style="margin-bottom: 50px;">
+                                    <div v-for="(items, index) in showPageList" :key="index" :id="'page' + (items.page)">
+                                        <div v-if="items.pageData.length" class="message-box">
+                                            <div class="message-page">
+                                                <div @click="toVideo(index + 1, $event)">
+                                                    <img :src="items.img" @click="openViewer(items.img)">
+                                                </div>
+                                            </div>
+                                            <div class="message-record">
+                                                <template v-if="items.pageData.length">
+                                                    <div v-for="event in items.pageData" :key="event.Time">
+                                                        <!-- 即问即答 -->
+                                                        <div v-if="currentfilterType === '' || currentfilterType === 'ShowAnsLoad'">
+                                                            <ShowQues class="event-item student-event" v-if="event.Event === 'PopQuesLoad' || event.Event === 'ReAtmpAnsStrt'" :nowStuInfo="nowStuInfo" :evtType="event.Event" :irsData="event.data"></ShowQues>
+                                                            <PopQues class="event-item" v-if="event.Event === 'PopQuesLoad' || event.Event === 'ReAtmpAnsStrt'" :evtType="event.Event" :irsData="event.data"></PopQues>
+                                                        </div>
+                                                        <!-- 抢权 -->
+                                                        <Buzr class="event-item student-event" v-if="event.Event === 'BuzrAns' && baseData" :buzrData="event.data" :students="baseData.student"></Buzr>
+                                                        <!-- 推送 -->
+                                                        <Push class="event-item" v-if="event.Event === 'FastPgPush' && (currentfilterType === '' || currentfilterType === 'doc')" :pushData="event.data"></Push>
+                                                        <!-- 作品收集 -->
+                                                        <StuReceive class="student-event event-item" v-if="event.Event === 'WrkSpaceEnd' && baseData" :nowStuInfo="nowStuInfo" :rcvData="event.data" :recordInfo="recordInfo" :students="baseData.student"></StuReceive>
+                                                        <!-- 老师收集的所有作品目前不展示,只展示老师回贴的学生作品 -->
+                                                        <!-- <Receive :recordInfo="recordInfo" class="student-event event-item" v-if="event.Event === 'WrkCmp' && baseData" :rcvData="event.data" :students="baseData.student"></Receive> -->
+                                                        <ReceiveBack class="event-item" v-if="event.Event === 'WrkCmp' && event.data && baseData" :recordInfo="recordInfo" :rcvData="event.data" :students="baseData.student"></ReceiveBack>
+                                                        <!-- 随机挑人 -->
+                                                        <Pick class="event-item student-event" v-if="event.Event === 'PickupResult' && baseData" :pickData="event.data" :students="baseData.student"></Pick>
+                                                        <!-- 课中评测 -->
+                                                        <Exam class="student-event event-item" :examInfo="event.data" :recordInfo="recordInfo" v-if="event.Event === 'SPQStrt'"></Exam>
+                                                    </div>
+                                                </template>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </vuescroll>
+                        </div>
+                        <div v-else class="no-interaction">
+                            {{ $t("studentWeb.hiteachNote.noContent") }}
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <Modal v-model="showNote" :title="$t('studentWeb.courseContent.mynotes')" width="800">
+            <img :src="item" alt="" v-for="(item, index) in recordInfo.myNote" :key="index" style="width: 100%">
+        </Modal>
+        <Modal v-model="showWorks" :title="$t('studentWeb.courseContent.mynotes')" width="800">
+            <myWorks :recordInfo="recordInfo" :rcvData="myWorks" />
+        </Modal>
+        
+    </div>
+</template>
+
+<script>
+import videojs from "video.js";
+import "videojs-markers";
+
+import Loading from '@/common/Loading.vue';
+import DataCount from './newDataCount.vue';
+import ShowQues from './ShowQues.vue';
+import PopQues from '@/view/classrecord/eventchart/PopQues.vue';
+import Buzr from './Buzr.vue';
+import Push from '@/view/classrecord/eventchart/Push.vue';
+import StuReceive from './StuReceive.vue';
+import ReceiveBack from './ReceiveBack.vue';
+import Pick from './Pick.vue';
+import Exam from './Exam.vue';
+import myWorks from './myWorks.vue';
+
+export default {
+    name: "ClassRecord",
+    components: {
+        Loading,
+        DataCount, ShowQues, PopQues, Buzr, Push,
+        StuReceive, ReceiveBack, Pick, Exam, myWorks,
+    },
+    data() {
+        return {
+            isLoad: false,
+            player: undefined,
+            pageList: [], //课件
+            markers: [], //打点
+            sokratesRecords: {}, //原始数据
+            curPage: 1, //当前课件页码
+            currentfilterType: "",
+            openHtexViewer: false,
+            playerOptions: {
+                height: "450px",
+                controlBar: {
+                    children: [// 写在这里,会在播放条上显示出来,并且是按照写的顺序显示位置。
+                        { name: "playToggle" }, //播放暂停按钮
+                        { name: "currentTimeDisplay" }, //当前播放时间
+                        { name: "progressControl" }, //播放进度条
+                        { name: "durationDisplay" }, //总时间
+                        {
+                            name: "playbackRateMenuButton",
+                            playbackRates: [0.5, 1, 1.5, 2, 2.5]
+                        }, //播放速率
+                        {
+                            name: "volumePanel", //音量控制
+                            inline: false, //不使用水平方式
+                        },
+                        { name: "FullscreenToggle" }, //全屏
+                        { name: "DashBoardEchart" }
+                    ],
+                },
+                /* html5: {
+                    nativeControlsForTouch: true,
+                }, */
+                // inactivityTimeout: 1,
+                // nativeVideoTracks: false,
+                // playbackRates: [0.5, 1.0, 1.25, 2.0], //播放速度
+                // autoplay: false, //如果true,浏览器准备好时开始回放。
+                controls: true, //控制条
+                preload: 'auto', //视频预加载
+                muted: false, //默认情况下将会消除任何音频。
+                // loop: false, //导致视频一结束就重新开始。
+                // language: 'zh-CN',
+                // aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
+                //fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
+                sources: [{
+                    src: ""
+                }],
+                // notSupportedMessage: '此视频暂无法播放,请稍后再试' //允许覆盖Video.js无法播放媒体源时显示的默认信息。
+            },
+            recordInfo: {},
+            baseData: undefined, //base.json
+            pushData: [], //push.json
+            irsData: [], //irs.json
+            taskData: [], //task.json
+            fnEvents: [], //功能事件
+            events: [], //事件ID
+            hiTeachEvent: [], //需要解析的事件信息
+            isShowVd: true,
+            hasVideo: true,
+            nowStuInfo: undefined, //当前学生的信息
+            haveInteraction: true,
+            courseNow: undefined,
+            showNote: false,
+            filtertype: {
+                push: 0, //推送
+                task: 0, //任务
+                irs: 0, //互动
+                exam: 0, //测验
+            },
+            showPageList: [],
+            myWorks: [],
+            showWorks: false,
+            attendType: 2,
+            attentColor: ['', '#19be6b', '#EB3941', '#EB3941', '#EB3941', '#EB3941'],
+        }
+    },
+    created() {
+        this.hiTeachEvent = this.$GLOBAL.HI_TEACH_EVENT()
+        this.events = Object.keys(this.hiTeachEvent)
+        this.fnEvents = this.events.filter(key => this.hiTeachEvent[key].type === 'fn')
+        this.recordInfo = this.$route.params.record
+        this.courseNow = this.$route.params.courseNow
+        if (!this.recordInfo) {
+            this.$router.go(-1)
+        } else {
+            this.recordInfo.startTime = this.dateFormat(this.recordInfo.startTime)
+            let sec = this.recordInfo.duration % 60
+            let min = parseInt(this.recordInfo.duration / 60)
+            let mins = min >= 60 ? min % 60 : min
+            let hour = parseInt(min / 60)
+            this.recordInfo.time = `${hour < 10 ? ('0' + hour) : hour}:${mins < 10 ? ('0' + mins) : mins}:${sec < 10 ? ('0' + sec) : sec}`
+            this.getPageList()
+        }
+    },
+    mounted() {
+        // this.getVideo()
+    },
+    methods: {
+        getVideo() {
+            var that = this
+            this.player = videojs(document.getElementById("recordVideo"), this.playerOptions, function () {
+                this.on('error', (e) => {
+                    that.hasVideo = false
+                    that.isShowVd = false
+                })
+            })
+            //时间切片
+            this.player.markers({
+                markerStyle: {
+                    width: "16px",
+                    height: "16px",
+                    top: "-22px",
+                    "display": "inline-block",
+                    "border-radius": "50%",
+                    "font-size": "12px",
+                    "line-height": "16px",
+                    "background-color": "orange"
+                },
+                breakOverlay: {
+                    display: false,
+                    displayTime: 4,
+                    style: {
+                        "z-index": "6",
+                        width: "100%",
+                        height: "10%",
+                        "background-color": "rgba(200,250,10,0.6)",
+                        color: "white",
+                        "font-size": "16px",
+                    },
+                    text: function (marker) {
+                        return that.$t('system.compt.cusWare') + marker.text;
+                    },
+                },
+                markerTip: {
+                    display: false,
+                    text: function (marker) {
+                        return marker.text;
+                    },
+                },
+                markers: that.markers,
+
+                //标记点击事件
+                onMarkerClick: function (marker) {
+
+                },
+                //视频播放到标记点触发的时间
+                onMarkerReached: function (marker) {
+                    let mkDoms = document.getElementsByClassName('vjs-marker ')
+                    for (let index in mkDoms) {
+                        if (mkDoms[index].dataset) {
+                            if (parseInt(mkDoms[index].dataset.markerTime) <= marker.time) {
+                                mkDoms[index].style.backgroundColor = '#1CC0F3'
+                                mkDoms[index].classList.add('vjs-marker-active')
+                            } else {
+                                mkDoms[index].style.backgroundColor = 'orange'
+                                mkDoms[index].classList.remove('vjs-marker-active')
+                            }
+                            // mkDoms[index].innerHTML = this.markers[index].page
+                        }
+                    }
+                    that.getCurPage(marker.page)
+                },
+            });
+
+            this.isLoad = false
+        },
+        // 根据SokratesRecords.json处理page数据
+        async getPageList() {
+            this.isLoad = true
+            this.pageList = []
+            this.showPageList = []
+            this.markers = []
+            let sas = await this.$tools.getBlobSas(this.recordInfo.scope === 'school' ? this.recordInfo.school : this.recordInfo.tmdid)
+            this.recordInfo.eNote = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/Note.pdf?${sas.sas}`
+            // 如果只会存在一个视频,文件名是否可以固定?
+            this.playerOptions.sources[0].src = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/Record/CourseRecord.mp4?${sas.sas}`
+            // let url = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/Sokrates/SokratesRecords.json?${sas.sas}` //后面会根据TimeLine.json处理
+            // 这里需要兼容原来没有TimeLine.json的课例(优先度读timeLine.json,没有则读SokratesRecords.json)
+            let url = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/IES/TimeLine.json?${sas.sas}`
+            let hasTimeLine = true
+            let dataErr = false
+            let pgids = []
+            let pageEvents = []
+            try {
+                let res = await this.$tools.getFile(url)
+                this.sokratesRecords = JSON.parse(res)
+                pgids = this.sokratesRecords.PgIdList || []
+                pageEvents = this.sokratesRecords.events || []
+            } catch (e) {
+                hasTimeLine = false
+            }
+            //读取 timeLine.json 失败,则读取 SokratesRecords.json
+            if (!hasTimeLine) {
+                url = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/Sokrates/SokratesRecords.json?${sas.sas}`
+                try {
+                    let res = await this.$tools.getFile(url)
+                    let resJson = JSON.parse(res)
+                    // 处理成timeLine数据格式
+                    let pageidEvent = resJson.find(item => item.Event == 'PgidList')
+                    pgids = pageidEvent && pageidEvent.PgIdList ? pageidEvent.PgIdList : []
+                    pageEvents = resJson.filter(item => this.events.includes(item.Event))
+                    this.sokratesRecords = {
+                        events: pageEvents,
+                        PgIdList: pgids
+                    }
+                } catch (e) {
+                    //timeLine 和 SokratesRecords都没有找到
+                    dataErr = true
+                }
+            }
+            
+            // 数据异常
+            if (dataErr) {
+                this.$Message.error(this.$t('cusMgt.rcd.dataErr'))
+                return
+            }
+            //获取Push.json、IRS.json、Task.json、Base.json数据
+            try {
+                let pushUrl = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/IES/Push.json?${sas.sas}`
+                this.pushData = JSON.parse(await this.$tools.getFile(pushUrl) || '[]')
+                this.pushData.forEach(item => {
+                    item.pageUrl = `${sas.url}/${sas.name}/records/${this.recordInfo.id}${item.pageMeta}?${sas.sas}`
+                })
+            } catch (e) {
+                this.pushData = []
+            }
+            try {
+                let irsUrl = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/IES/IRS.json?${sas.sas}`
+                this.irsData = JSON.parse(await this.$tools.getFile(irsUrl) || '[]')
+            } catch (e) {
+                this.irsData = []
+            }
+            try {
+                let taskUrl = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/IES/Task.json?${sas.sas}`
+                this.taskData = JSON.parse(await this.$tools.getFile(taskUrl) || '[]')
+            } catch (e) {
+                this.taskData = []
+            }
+            try {
+                let baseUrl = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/IES/base.json?${sas.sas}`
+                this.baseData = JSON.parse(await this.$tools.getFile(baseUrl) || '{}')
+                this.baseData.student.forEach((item, index) => {
+                    if (item.id === this.$store.state.userInfo.sub) {
+                        this.nowStuInfo = item
+                        this.nowStuInfo.index = index
+                    }
+                })
+                if(this.nowStuInfo) {
+                    let nowClient = this.baseData.report.clientSummaryList.find(item => {
+                        return this.nowStuInfo.seatID === item.seatID
+                    })
+                    this.attendType = nowClient ? nowClient.attendState : 2
+                }
+            } catch (e) {
+                this.baseData = undefined
+            }
+
+            //这里需要判断录制开始的pageid
+            let startInfo = pageEvents.find(item => item.Event === 'EzsStartRecord')
+            let startId = startInfo ? startInfo.Pgid : ''
+            let startIndex = 0
+            if (startId) {
+                startIndex = pgids.findIndex(item => item === startId)
+            }
+            pgids = pgids.slice(startIndex)
+
+            let havePage = 0
+            let myTask = []
+            pgids.forEach((item, index) => {
+                let page = {}
+                page.id = item
+                page.img = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/Memo/${item}.jpg?${sas.sas}`
+                page.page = index + 1
+                //当前页面对应的sokrates
+                page.pageData = pageEvents.filter(record => record.Pgid === item && this.fnEvents.includes(record.Event))
+                havePage += (page.pageData.length ? 1 : 0)
+                page.pageData.forEach(e => {
+                    e.pageIndex = index
+                    e.eventName = this.hiTeachEvent[e.Event]?.text
+                    let rlt = this.hiTeachEvent[e.Event]?.relation
+                    e.relation = rlt
+                    switch (rlt) {
+                        case 'irs':
+                            this.filtertype.irs += 1
+                            e.data = this.irsData.find(i => i.pageID == e.Pgid)
+                            break
+                        case 'push':
+                            this.filtertype.push += 1
+                            e.data = this.pushData.find(p => p.pageId == e.Pgid || p.pageID == e.Pgid)
+                            break
+                        case 'task':
+                            this.filtertype.task += 1
+                            e.data = this.taskData.find(t => t.pageID == e.Pgid)
+                            myTask.push(e.data)
+                            break
+                        case 'timeline':
+                            e.data = this._.cloneDeep(e)
+                            break
+                        case 'exam':
+                            this.filtertype.exam += 1
+                            e.data = this._.cloneDeep(e)
+                            break
+                        default:
+                            break
+                    }
+                })
+                this.pageList.push(page)
+            })
+            if(this.baseData) {
+                myTask.forEach(item => {
+                    if(item.clientWorks.length) {
+                        item.clientWorks.forEach(works => {
+                            let owner = this.baseData.student.find(stu => stu.seatID == works.seatID && stu.id === this.$store.state.userInfo.sub)
+                            if(owner) {
+                                this.myWorks.push({
+                                    jobName: item.jobName,
+                                    collateType: item.collateType,
+                                    blobFiles: works.blobFiles
+                                })
+                            }
+                        })
+                    }
+                })
+            }
+            this.showPageList = [...this.pageList]
+            this.haveInteraction = havePage != 0
+            let pageEvent = pageEvents.filter(item => item.Event === 'PopQuesLoad' || item.Event === 'ReAtmpAnsStrt' || item.Event === 'FastPgPush' || item.Event === 'WrkSpaceEnd' || item.Event === 'SPQStrt')
+            this.markers = pageEvent.map((item, index) => {
+                return {
+                    time: item.Time,
+                    text: `${this.$t('cusMgt.rcd.di')}${index + 1}${this.$t('cusMgt.rcd.page')}`,
+                    page: index + 1
+                }
+            })
+            this.getVideo()
+        },
+        // 点击互动记录页面tag
+        toVideo(page, e) {
+            this.curPage = page
+            //页面滚动
+            /* let dataLoacation = this.$refs["datawrap"].getPosition()
+            let pageLocaltion = this.$refs["pagewrap"].getPosition()
+            let y = e.pageY - 770 + pageLocaltion.scrollTop + dataLoacation.scrollTop
+            this.$nextTick(() => {
+                this.$refs["pagewrap"].scrollTo(
+                    {
+                        x: 0,
+                        y: 0
+                    }
+                )
+                this.$refs["datawrap"].scrollTo(
+                    {
+                        x: 0,
+                        y: y
+                    }
+                )
+            }) */
+            //视频时间定位
+            let pageInfo = this.markers.find(item => {
+                return item.page === page
+            })
+            if (pageInfo) {
+                this.player.currentTime(pageInfo.time)
+            }
+        },
+        // 点击课件page
+        getCurHTEX(page) {
+            this.curPage = page
+            // this.mapJson = require('./data/' + page + '.json')
+            //视频时间定位
+            let pageInfo = this.markers.find(item => {
+                return item.page === page
+            })
+            if (pageInfo) {
+                this.player.currentTime(pageInfo.time)
+                if (!this.openHtexViewer) {
+                    this.player.play()
+                } else {
+                    this.player.pause()
+                }
+                //互动记录滚动
+                // this.$refs["datawrap"].scrollIntoView('#page' + page, 500)
+            } else {
+            }
+        },
+        // 点击视频切片
+        getCurPage(page) {
+            this.curPage = page
+            // this.mapJson = require('./data/' + page + '.json')
+            // this.$refs["datawrap"].scrollIntoView('#page' + page, 500)
+            this.player.play()
+        },
+        //查看电子笔记
+        async viewENote() {
+            let eNote
+            if (this.recordInfo.eNote) {
+                eNote = this.recordInfo.eNote
+            } else {
+                // let sasInfo = {}
+                // let blobInfo = this.recordInfo.scope === 'school' ? this.$store.state.user.schoolProfile : this.$store.state.user.userProfile
+                // sasInfo.sas = '?' + blobInfo.blob_sas
+                // sasInfo.name = this.recordInfo.scope ? this.$store.state.userInfo.schoolCode : this.$store.state.userInfo.TEAMModelId
+                // sasInfo.url = blobInfo.blob_uri.slice(0, blobInfo.blob_uri.lastIndexOf(sasInfo.name) - 1)
+                let sasInfo = await this.$tools.getBlobSas(this.recordInfo.scope === 'school' ? this.recordInfo.school : this.recordInfo.tmdid)
+                eNote = `${sasInfo.url}/${sasInfo.name}/records/${this.recordInfo.id}/Note.pdf?${sasInfo.sas}`
+            }
+            window.open('/web/viewer.html?file=' + encodeURIComponent(eNote))
+            // if (this.recordInfo.eNote) {
+            //     window.open('/web/viewer.html?file=' + encodeURIComponent(this.recordInfo.eNote))
+            // } else {
+            //     this.$Message.warning(this.$t('cusMgt.rcd.noNote'))
+            // }
+        },
+        //下载电子笔记
+        async loadNote() {
+            if (this.recordInfo.eNote) {
+                // 已经有授权,在查看文件时不需要再次获取授权
+                let blobData = this.recordInfo.eNote
+                const downloadRes = async () => {
+                    let response = await fetch(blobData); // 内容转变成blob地址
+                    let blob = await response.blob();  // 创建隐藏的可下载链接
+                    let objectUrl = window.URL.createObjectURL(blob);
+                    let a = document.createElement('a');
+                    a.href = objectUrl;
+                    a.download = "电子笔记.pdf";
+                    a.click()
+                    a.remove();
+                }
+                downloadRes();
+            } else {
+                this.$Message.warning("暂无电子笔记")
+            }
+        },
+        openViewer(item) {
+            this.$hevueImgPreview(item)
+        },
+        // 筛选
+        showFile(type) {
+            this.currentfilterType = (type === "all" ? "" : type)
+        },
+        getTime(time) {
+            let sec = parseInt(time % 60)
+            let min = parseInt(time / 60)
+            let mins = min >= 60 ? min % 60 : min
+            let hour = parseInt(min / 60)
+            return `${hour < 10 ? ('0' + hour) : hour}:${mins < 10 ? ('0' + mins) : mins}:${sec < 10 ? ('0' + sec) : sec}`
+        },
+        dateFormat(timestamp) {
+            var date = new Date(timestamp)
+            var Y = date.getFullYear() + '-'
+            var M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-'
+            var D = (date.getDate() < 10 ? '0' + (date.getDate()) : date.getDate()) + ' '
+            var H = (date.getHours() < 10 ? '0' + date.getHours() : date.getHours()) + ":"
+            var Min = (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes())
+            var S = (date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds()) + " "
+            return Y + M + D + H + Min;
+        },
+        quitRec() {
+            this.$router.go(-1)
+        },
+        //返回顶部
+        handleToTop() {
+            document.getElementsByClassName("class-record")[0].scrollIntoView()
+            /* this.$refs['pagewrap'].scrollTo(
+                {
+                    y: '0'
+                },
+                300
+            ) */
+        },
+        filterFn(type) {
+            this.showPageList = []
+            if(type === 'all') {
+                this.showPageList = [...this.pageList]
+            } else {
+                this.isLoad = true
+                this.pageList.forEach(item => {
+                    if(item.pageData.length) {
+                        let filterArr = item.pageData.filter(data => {
+                            return data.relation === type
+                        })
+                        if(filterArr.length) {
+                            this.showPageList.push({
+                                id: item.id,
+                                img: item.img,
+                                page: item.page,
+                                pageData: filterArr
+                            })
+                        }
+                    }
+                })
+                this.isLoad = false
+            }
+        },
+        downloadFile(url, taskInfo) {
+            const downloadRes = async () => {
+                let response = await fetch(url); // 内容转变成blob地址
+                let blob = await response.blob(); // 创建隐藏的可下载链接
+                let objectUrl = window.URL.createObjectURL(blob);
+                let a = document.createElement('a');
+                a.href = objectUrl;
+                let fileName = url.substring(url.lastIndexOf('/' + 1), url.lastIndexOf('?'))
+                a.download = `${taskInfo.jobName}-${taskInfo.seatID}-${fileName}`
+                a.click()
+                a.remove();
+            }
+            downloadRes();
+        },
+    },
+    computed: {
+        curImg() {
+            if (this.pageList[this.curPage - 1]) {
+                return this.pageList[this.curPage - 1].img
+            } else {
+                return ""
+            }
+        }
+    },
+}
+</script>
+
+<style lang="less" scoped>
+@import "./newClassRecord.less";
+</style>
+<style lang="less">
+.record-left {
+    .video-js .vjs-big-play-button {
+        top: 50%;
+        left: 50%;
+        margin-left: -20px;
+        margin-top: -20px;
+        display: none;
+    }
+
+    .video-js .vjs-control-bar {
+        display: flex;
+    }
+
+    .vjs-marker::after {
+        content: "";
+        height: 0px;
+        width: 0px;
+        border: 3px transparent solid;
+        display: block;
+        position: absolute;
+        bottom: -10px;
+        z-index: -1;
+        border-right: 8px solid transparent;
+        border-top: 15px solid orange;
+        border-left: 8px solid transparent;
+    }
+    .vjs-marker-active::after {
+        border-right: 8px solid transparent !important;
+        border-top: 15px solid #1cc0f3 !important;
+        border-left: 8px solid transparent !important;
+    }
+
+    .vjs-marker:hover {
+        z-index: 101;
+    }
+}
+
+.record-info {
+    .ivu-btn-primary {
+        background-color: #24b880;
+        border-color: #24b880;
+
+        &:not(:last-child) {
+            margin-right: 15px;
+        }
+    }
+
+    .ivu-btn-warning {
+        color: #515a6e;
+        background-color: #FEE49E;
+        border-color: #FEE49E;
+
+        &:not(:last-child) {
+            margin-right: 15px;
+        }
+    }
+
+    .ivu-btn-primary[disabled],
+    .ivu-btn-primary[disabled]:hover {
+        background-color: #24b880;
+        border-color: #24b880;
+    }
+
+    .ivu-btn-warning[disabled],
+    .ivu-btn-warning[disabled]:hover {
+        color: #c5c8ce;
+        background-color: #FEE49E;
+        border-color: #FEE49E;
+    }
+}
+</style>

+ 184 - 0
TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/newDataCount.vue

@@ -0,0 +1,184 @@
+<template>
+    <div>
+        <!-- <p class="attend-type" :style="{'color': attentColor[attendType]}">
+            {{ $t(`studentWeb.hiteachNote.dataCount.attendTypeList[${attendType}]`) }}
+        </p> -->
+        <div class="data-count-container">
+            <div v-for="(item,index) in dataList" :key="'1' + index" class="data-item">
+                <p class="data-value" :style="{borderColor: attentColor[attendType]}">{{item.value}}</p>
+                <p class="data-label">{{item.label}}</p>
+                <p class="data-info">{{item.info}}</p>
+            </div>
+            <div v-for="(item,index) in dataList2" :key="'2' + index" class="data-item">
+                <p class="data-value" :style="{borderColor: attentColor[attendType]}">{{item.value}}</p>
+                <p class="data-label">{{item.label}}</p>
+                <p class="data-info">{{item.info}}</p>
+            </div>
+        </div>
+    </div>
+</template>
+<script>
+import CountTo from 'vue-count-to'
+export default {
+    props: {
+        rcdInfo: {
+            type: Object,
+            default: () => {
+                return undefined
+            }
+        },
+        nowStuInfo: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        }
+    },
+    components: {
+        CountTo
+    },
+    data() {
+        return {
+            attentColor: ['', '#19be6b', '#EB3941', '#EB3941', '#EB3941', '#EB3941'],
+            colorList: ['#19be6b', '#2d8cf0', '#2db7f5', '#2db7f5', '#2db7f5', '#19be6b', '#2db7f5', '#2db7f5', '#2db7f5', '#ed4014'],
+            attendType: 2, //新加入的会没有该学生的信息,即nowStuInfo = undefined
+            dataList: [
+                {
+                    label: this.$t("studentWeb.hiteachNote.dataCount.getCount"),
+                    value: 0,
+                    info: ''
+                },
+
+                {
+                    label: this.$t("studentWeb.hiteachNote.dataCount.intCount"),
+                    value: 0,
+                    info: ''
+                },
+                {
+                    label: this.$t("studentWeb.hiteachNote.dataCount.taskNum"),
+                    value: 0,
+                    info: ''
+                },
+                {
+                    label: this.$t("studentWeb.hiteachNote.dataCount.colctNum"),
+                    value: 0,
+                    info: ''
+                },
+                {
+                    label: this.$t("studentWeb.hiteachNote.dataCount.pushNum"),
+                    value: 0,
+                    info: ''
+                },
+            ],
+            dataList2: [
+                {
+                    label: this.$t("studentWeb.hiteachNote.dataCount.examNum"),
+                    value: 0,
+                    info: ''
+                },
+                {
+                    label: this.$t("studentWeb.hiteachNote.dataCount.intExamNum"),
+                    value: 0,
+                    info: ''
+                },
+                {
+                    label: this.$t("studentWeb.hiteachNote.dataCount.examCount"),
+                    value: 0,
+                    info: ''
+                },
+            ]
+        }
+    },
+    created() {
+        if (this.rcdInfo) {
+            console.log(this.rcdInfo);
+            const summary = this.rcdInfo
+            let nowClient = summary.report.clientSummaryList.find(item => {
+                return this.nowStuInfo.seatID === item.seatID
+            })
+            if(nowClient) {
+                /* 
+                    EAttendState
+                    {
+                        Uncall,0(未點名)
+                        Attended,1(出席)
+                        Absent,2(缺席)
+                        DayOff,3(請假)
+                        Absent_Sick,4(病假)
+                        Absent_Personal,5(事假)
+                        Absent_Official,6(公假)
+                    }
+                */
+                this.attendType = nowClient.attendState
+                // this.dataList[0].value = nowClient.attendState
+                // 记分
+                this.dataList[0].value = nowClient.score
+                // 互动分
+                this.dataList[1].value = nowClient.interactScore
+                // 任务数
+                this.dataList[4].value = nowClient.taskCompleteCount
+                nowClient.examScoreList.map(item => {
+                    this.dataList2[2].value += item
+                })
+            }
+            // 出席
+            // this.dataList[0].value = summary.attendCount
+            // 總計分
+            // this.dataList[1].value = summary.totalPoint
+            // 總互動分
+            // this.dataList[2].value = summary.totalInteractPoint
+            // 任务总数(作品收集任務數)
+            // this.dataList[3].value = summary.collateTaskCount
+            // 作品总数
+            this.dataList[3].value = summary.report.collateCount
+            // 推送總數(頁面+資源+訊息+差異化)
+            this.dataList[4].value = summary.report.pushCount
+            // 测验总题数
+            this.dataList2[0].value = summary.report.examQuizCount
+            // 互动题数
+            this.dataList2[1].value = summary.report.interactionCount
+            // 测验得分率
+            // this.dataList[8].value = summary.examPointRate
+            this.$forceUpdate()
+        }
+    }
+}
+</script>
+<style lang="less" scoped>
+.attend-type {
+    font-size: 20px;
+    font-weight: 600;
+    margin-right: 50px;
+    margin-top: 10px;
+    float: right;
+}
+.data-count-container {
+    width: 100%;
+    display: flex;
+    flex-wrap: wrap;
+    padding: 20px 0px;
+}
+.data-item {
+    width: 150px;
+    padding: 10px;
+    text-align: center;
+    margin-bottom: 30px;
+}
+.data-value {
+    font-size: 40px;
+    font-weight: 600;
+    color: #17233d;
+    width: fit-content;
+    min-width: 70px;
+    margin: auto;
+    margin-bottom: 5px;
+    border-bottom: 2px solid;
+}
+.data-label {
+    font-size: 16px;
+    // color: #2d8cf0;
+}
+.data-info {
+    font-size: 12px;
+}
+</style>

File diff suppressed because it is too large
+ 729 - 747
TEAMModelOS/ClientApp/src/components/vote/BaseVoteForm.vue


+ 2 - 2
TEAMModelOS/ClientApp/src/css/common-style.less

@@ -188,10 +188,10 @@
         padding: 5px 0;
         cursor: pointer;
         vertical-align: middle;
-        padding: 5px 0px;
+        padding: 9px 0px;
         &.active{
             font-weight: bold;
-            color: var(--tabs-text-color);
+            color: var(--tabs-bottom-color);
             border-bottom: 2px var(--tabs-bottom-color) solid;
         }
     }

+ 14 - 4
TEAMModelOS/ClientApp/src/css/site.css

@@ -120,10 +120,6 @@ html[white] {
 	background: #94998a;
 }
 
-.scrollstyle::-webkit-scrollbar-thumb:hover {
-	/* background: #555; */
-}
-
 .scrollstyle::-webkit-scrollbar-button {
 	display: none;
 }
@@ -340,6 +336,20 @@ audio::-internal-media-controls-overflow-button {
 	white-space: nowrap;
 	overflow: hidden;
 }
+
+.ac-share-tooltip .ivu-icon {
+	margin-right: 6px;
+}
+
+.ac-share-tooltip .link-item {
+	padding: 6px;
+	cursor: pointer;
+}
+
+.ac-share-tooltip .link-item:hover {
+	color: #70b1e7;
+}
+
 dot{
 	position: relative;
 }

+ 23 - 3
TEAMModelOS/ClientApp/src/router/routes.js

@@ -520,6 +520,16 @@ export const routes = [{
 			isKeep: true
 		}
 	},
+	//新版我的课程页面
+	{
+		path: 'course',
+		name: 'course',
+		component: resolve => require(['@/view/mycourse/MyCourse.vue'], resolve),
+		meta: {
+			activeName: 'course',
+			isKeep: true
+		}
+	},
 	{
 		path: 'taskList',
 		name: 'taskList',
@@ -612,7 +622,7 @@ export const routes = [{
 			isKeep: true
 		}
 	},
-	// 创建个人评测
+	// 创建个人评测(待废弃)
 	{
 		path: 'createPrivEva',
 		name: 'createPrivEva',
@@ -622,6 +632,16 @@ export const routes = [{
 			isKeep: true
 		}
 	},
+	// 创建个人评测
+	{
+		path: 'createPrivExam',
+		name: 'createPrivExam',
+		component: resolve => require(['@/view/mycourse/exam/CreatePrivExam.vue'], resolve),
+		meta: {
+			activeName: 'privateEvaluation',
+			isKeep: true
+		}
+	},
 	//创建学习单元
 	{
 		path: 'createLearnUnit',
@@ -1135,7 +1155,7 @@ export const routes = [{
 	{
 		path: 'classRecord',
 		name: 'classRecord',
-		component: resolve => require(['@/view/classrecord/ClassRecord.vue'], resolve),
+		component: resolve => require(['@/view/classrecord/ClassRecordNew.vue'], resolve),
 		meta: {
 			activeName: 'myCourse'
 		}
@@ -1311,7 +1331,7 @@ export const routes = [{
 		// 课堂记录
 		name: "stuClassRec",
 		path: "stuClassRec",
-		component: resolve => require(['@/components/student-web/ClassRecord/ClassRecord'], resolve),
+		component: resolve => require(['@/components/student-web/ClassRecord/newClassRecord'], resolve),
 	},
 	{
 		// 错题本

+ 4 - 4
TEAMModelOS/ClientApp/src/static/Global.js

@@ -364,10 +364,10 @@ const HI_TEACH_EVENT = () => {
 			type: 'fn',
 			relation: 'task'
 		},
-		WrkCmp: {
-			text: '作品贴上',
-			type: 'tag',
-			relation: ''
+		WrkCmp:{
+			text:'作品贴上',
+			type:'fn',
+			relation:'timeline'
 		},
 		BuzrAns: {
 			text: '抢权',

+ 0 - 3
TEAMModelOS/ClientApp/src/utils/blobTool.js

@@ -630,9 +630,6 @@ export default class BlobTool {
                 console.log(scope)
                 console.log(sizeRes)
                 if (sizeRes) {
-                    console.log(sizeRes.total)
-                    console.log(this.blobSpace * 1024 * 1024 * 1024)
-
                     r(sizeRes.total > this.blobSpace * 1024 * 1024 * 1024)
                 } else {
                     j('容器空间判断失败!')

+ 40 - 1
TEAMModelOS/ClientApp/src/utils/public.js

@@ -8,6 +8,7 @@ import JsPDF from 'jspdf'
 import 'jspdf-autotable'
 import domtoimage from '@/utils/dom_to_image';
 import excel from '@/utils/excel.js'
+import BlobTool from '@/utils/blobTool.js'
 import i18n from '@/locale'
 // 引入iView多语言包
 import zhLocale from 'view-design/src/locale/lang/zh-CN'
@@ -188,6 +189,44 @@ export default {
 		'justify', // 对齐方式
 		'table', // 表格
 	],
+	/* 查询指定容器是否空间已满 */
+	isBlobContainerFull(scope) {
+		return new Promise(async (resolve, reject) => {
+			try {
+				let sas = scope == 'school' ? store.state?.user?.schoolProfile?.blob_sas : store.state?.user?.userProfile?.blob_sas
+				let blobUrl = scope == 'school' ? JSON.parse(decodeURIComponent(localStorage.school_profile, "utf-8"))?.blob_uri : JSON.parse(decodeURIComponent(localStorage.user_profile, "utf-8"))?.blob_uri
+				let host = blobUrl?.substring(0, blobUrl?.lastIndexOf('/'))
+				let cont = blobUrl?.substring(blobUrl?.lastIndexOf('/') + 1)
+				let blobClient = new BlobTool(host, cont, '?' + sas, scope)
+				let isFull = await blobClient.isContainerFull(scope)
+				console.log(isFull);
+				resolve(isFull)
+			} catch (e) {
+				reject(e)
+			}
+		})
+
+	},
+	/* 复制活动链接 */
+	async doCopyAcLink(type, acInfo) {
+		let shareContent = ''
+		let shareUrl = `https://${window.location.host}/studentWeb/examView?aId=${acInfo.id}`
+		let shortUrl = await $api.getShortUrl(encodeURI(shareUrl))
+		let acName = acInfo.name
+		if (type === 'notice') {
+			shareContent = `${app.$t('learnActivity.mgtScEv.shareText1')}\n${app.$t('cusMgt.inviteInfo8')}${app.$store.state.userInfo.name}\n${app.$t('learnActivity.mgtScEv.shareText2')}${acName}\n\n${app.$t('learnActivity.mgtScEv.shareText6')}\n${shortUrl.result || encodeURI(shareUrl)}\n\n${app.$t('learnActivity.mgtScEv.shareText11')}`
+		} else {
+			shareContent = shortUrl.result || encodeURI(shareUrl)
+		}
+		app.$copyText(shareContent).then(
+			ok => {
+				Message.success(app.$t("settings.copyModal1"))
+			},
+			fail => {
+				Message.error(app.$t("settings.copyModal2"))
+			}
+		)
+	},
 	/* 切换语系 */
 	changeLang(lang) {
 		// 如果已经加载过语言包 则直接设置 否则加载语言包
@@ -902,7 +941,7 @@ export default {
 	batchQrcodes(arr, pdfName) {
 		return new Promise((resolve, reject) => {
 			if (!arr.length) {
-				app.$Message.warning('未获取到学生数据')
+				app.$Message.warning(app.$t('common.noStuTip'))
 				resolve(200)
 				return
 			}

+ 19 - 3
TEAMModelOS/ClientApp/src/view/Home.vue

@@ -17,7 +17,7 @@
                 <BaseUserPoptip @logout="basicMenu('quit')"></BaseUserPoptip>
                 <div class="overlay-qrcode" v-show="isShowQrCode">
                     <img src="../assets/image/qrcode.png" width="150px" v-if="!inGlobalSite">
-                    <img src="../assets/image/qrcode_tw.png" width="150px" v-if="inGlobalSite">
+                    <img :src="globalQrCodeImg" width="150px" v-if="inGlobalSite">
                     <p style="margin-top: 10px;">{{ inGlobalSite ? $t('system.wechatTipTw') : $t('system.wechatTip') }}</p>
                 </div>
             </div>
@@ -79,6 +79,7 @@ export default {
     },
     data() {
         return {
+            globalQrCodeImg:'',
             isShowQrCode: false,
             updStatus: false,
             isRouterAlive: true,
@@ -315,10 +316,19 @@ export default {
                     path: '/login'
                 })
             }
+        },
+        getQrImgByLang(){
+            let curLang = localStorage.getItem('local') || 'zh-cn'
+            let imgMap = {
+                'zh-cn':require('../assets/image/qrcode.png'),
+                'zh-tw':require('../assets/image/qrcode_tw.png'),
+                'en-us':require('../assets/image/qrcode_en.png'),
+            }
+            this.globalQrCodeImg = imgMap[curLang]
         }
     },
     mounted() {
-
+        this.getQrImgByLang()
         if (localStorage.getItem('noSave') && JSON.parse(localStorage.getItem('noSave')).length) {
             this.$tools.deleteNoSave(JSON.parse(localStorage.getItem('noSave')))
         } else {
@@ -357,6 +367,9 @@ export default {
         isMobile() {
             return navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i)
         },
+        curLang(){
+            return localStorage.getItem('local') || 'zh-cn'
+        }
     },
     watch: {
         $route: {
@@ -376,6 +389,9 @@ export default {
             deep: true,
             //立即执行
             immediate: true
+        },
+        '$i18n.locale'(n, o) {
+            this.getQrImgByLang()
         }
     },
 }
@@ -441,7 +457,7 @@ export default {
 .header-right-box .overlay-qrcode {
     position: absolute;
     right: 115px;
-    bottom: -215px;
+    top: 40px;
     background-color: #fff;
     padding: 10px;
     border-radius: 8px;

+ 172 - 11
TEAMModelOS/ClientApp/src/view/auth/Product.vue

@@ -17,13 +17,17 @@
                         </p>
                         <p class="product-content">
                             {{item.content}}
+                            <a :href="item.more" target="_black" style="text-decoration: underline;">
+                                {{$t('auth.learnMore')}}
+                            </a>
                         </p>
                         <span :class="['product-status-tag', item.isPay ? 'has-pay' : '']">
                             {{item.isPay ? $t('auth.hasBuy') : $t('auth.noBuy')}}
                         </span>
                         <div style="display: flex;align-items: center;margin-top: 20px;">
                             <Button :type="item.isPay ? 'success' : 'primary'" :icon="item.isPay ? 'md-checkmark' : 'md-cart'" class="buy-btn" @click="toContact(item.isPay)">
-                                {{item.isPay ? $t('auth.using') :  $t('auth.buy')}}
+                                {{item.isPay ? $t('auth.using') :  $t('auth.contactUs')}}
+                                <!-- {{item.isPay ? $t('auth.using') :  $t('auth.buy')}} -->
                             </Button>
                             <p class="product-status" style="margin-top:5px" v-show="item.isPay">
                                 <!-- 时间 -->
@@ -118,25 +122,106 @@
                 <EmptyData :top="80"></EmptyData> -->
             </vuescroll>
         </div>
+        <!-- 联系表单 -->
+        <Modal v-model="formStatus" className="ed-name-modal" :width="600" footer-hide>
+            <div slot="header" class="modal-header">
+                {{$t('auth.ctFrom')}}
+            </div>
+            <div class="edit-name-content">
+                <p>
+                    {{$store.state.userInfo.name}} {{$t('auth.ctFmText1')}}
+                </p>
+                <Form ref="contact" :model="contact" :label-width="80" :rules="ruleValidate" label-position="left" style="margin-top:30px">
+                    <FormItem :label="$t('auth.ctFmText2')" prop="name">
+                        <Input v-model="contact.name" :placeholder="$t('auth.ctFmText3')"></Input>
+                    </FormItem>
+                    <FormItem :label="$t('auth.ctFmText4')" prop="sex">
+                        <RadioGroup v-model="contact.sex">
+                            <Radio :label="$t('auth.ctFmText5')"></Radio>
+                            <Radio :label="$t('auth.ctFmText6')"></Radio>
+                        </RadioGroup>
+                    </FormItem>
+                    <FormItem :label="$t('auth.ctFmText7')" prop="mobile">
+                        <Input v-model="contact.mobile" type="number" :placeholder="$t('auth.ctFmText8')"></Input>
+                    </FormItem>
+                    <FormItem :label="$t('auth.ctFmText12')" prop="address">
+                        <BaseAreaPicker ref="areaPicker" v-if="isChinaSite"></BaseAreaPicker>
+                        <div class="country-select" v-if="!isChinaSite">
+                            <Select v-model="contact.address" filterable :placeholder="$t('settings.applyForm.place6')">
+                                <Option v-for="(country,index) in countryArr" :value="country.cn" :key="index">{{ country.cn }}</Option>
+                            </Select>
+                        </div>
+                    </FormItem>
+                    <FormItem :label="$t('auth.ctFmText13')" prop="content">
+                        <Select v-model="contact.content" multiple>
+                            <Option v-for="item in productList" :value="item.name" :key="item.prodCode">{{ item.name }}</Option>
+                            <Option :value="$t('auth.ctFmText14')">
+                                {{$t('auth.ctFmText14')}}
+                            </Option>
+                            <Option :value="$t('auth.ctFmText15')">
+                                {{$t('auth.ctFmText15')}}
+                            </Option>
+                        </Select>
+                    </FormItem>
+
+                </Form>
+                <Button :loading="btnLoading" @click="sendInfo" long type="primary" class="confirm-btn">{{ $t('syllabus.confirm') }}</Button>
+            </div>
+        </Modal>
     </div>
 </template>
 <script>
+import countries from '@/static/countries.js'
+import enCountries from '@/static/countryCodeData.js'
+import twCitys from '@/static/twJson.js'
+import twTCitys from '@/static/twJsonT.js'
 import { mapGetters } from 'vuex'
 import SpaceInfo from './SpaceInfo.vue'
 export default {
     components: {
         SpaceInfo
     },
-    props:{
-        service:{
-            type:Array,
-            default:()=>{
+    props: {
+        service: {
+            type: Array,
+            default: () => {
                 return []
             }
         }
     },
     data() {
         return {
+            isShowTw: false,
+            curCountry: '',
+            curTwCity: [],
+            countryArr: [],
+            twCityArr: [],
+            ruleValidate: {
+                name: [
+                    { required: true, message: this.$t('auth.ctFmText3'), trigger: 'blur' }
+                ],
+                sex: [
+                    { required: true, message: this.$t('auth.ctFmText9'), trigger: 'change' }
+                ],
+                address: [
+                    { required: true, message: this.$t('auth.ctFmText16'), trigger: 'change' }
+                ],
+                content: [
+                    { required: true, type: 'array', message: this.$t('auth.ctFmText17'), trigger: 'change' }
+                ],
+                mobile: [
+                    { required: true, message: this.$t('auth.ctFmText10'), trigger: 'change' }
+                ]
+            },
+            contact: {
+                name: this.$store.state.userInfo.name,
+                sex: '',
+                mobile: '',
+                content: [],
+                address: ''
+            },
+            btnLoading: false,
+            formStatus: false,
             isLoading: false,
             scaleCount: 0, //学校规模授权数
             spaceStatus: 1,
@@ -150,6 +235,7 @@ export default {
                     content: this.$t('auth.content1'),
                     isPay: 0,
                     prodCode: 'YMPCVCIM',
+                    more: 'https://www.habook.com.cn/product.php?act=view&id=29'
                 },
                 {
                     name: this.$t('auth.module2'),
@@ -159,6 +245,7 @@ export default {
                     content: this.$t('auth.content2'),
                     isPay: 0,
                     prodCode: 'VABAJ6NV',
+                    more: 'https://www.habook.com.cn/product.php?act=view&id=30'
                 },
                 {
                     name: this.$t('auth.module3'),
@@ -168,6 +255,7 @@ export default {
                     content: this.$t('auth.content3'),
                     isPay: 0,
                     prodCode: 'Sokrates',
+                    more: 'https://www.habook.com.cn/cloud.php?act=view&id=21'
                 }
             ]
         }
@@ -176,6 +264,9 @@ export default {
         ...mapGetters({
             schoolBase: 'user/getSchoolBase',
         }),
+        isChinaSite() {
+            return this.$store.state.config.srvAdr === 'China'
+        },
         periodCount() {
             if (this.schoolBase && this.schoolBase.period) {
                 return this.schoolBase.period.length
@@ -185,8 +276,52 @@ export default {
         }
     },
     methods: {
+        /* 国家选择 */
+        onCountrySelect(val) {
+            if (val === '中国台湾' || val === '臺灣') {
+                this.isShowTw = true
+                this.curTwCity = this.twCityArr[0]
+            } else {
+                this.isShowTw = false
+            }
+        },
+        sendInfo() {
+            if (this.isChinaSite) {
+                let pickResult = this.$refs.areaPicker.pickResult
+                let hasFullAddress = pickResult.province && pickResult.city && pickResult.area
+                this.contact.address = hasFullAddress ? (pickResult.province + pickResult.city + pickResult.area) : ''
+            }
+            this.$refs['contact'].validate((valid) => {
+                if (valid) {
+                    let params = {
+                        area: this.contact.address,
+                        name: this.$store.state.user.schoolProfile?.school_base?.name,
+                        tmdname: this.$store.state.userInfo.name,
+                        tmdid: this.$store.state.userInfo.TEAMModelId,
+                        gender: this.contact.sex,
+                        cellphone: this.contact.mobile,
+                        content: this.contact.content.join(',')
+                    }
+                    console.log(params)
+                    this.$api.serviceDriveAuth.sendContactInfo(params).then(
+                        res => {
+                            this.$Message.success(this.$t('auth.ctFmText18'))
+                            this.formStatus = false
+                        },
+                        err => {
+                            this.$Message.error(this.$t('auth.ctFmText19'))
+                        }
+                    )
+                } else {
+                    this.$Message.error(this.$t('auth.ctFmText11'));
+                }
+            })
+
+        },
         toContact(isPay) {
             if (!isPay) {
+                this.formStatus = true
+                return
                 let urls = {
                     en: 'https://www.habook.com/en/contact_us.php',
                     tw: 'https://www.habook.com/zh-tw/contact_us.php',
@@ -223,16 +358,42 @@ export default {
             if (scaleInfo) this.scaleCount = scaleInfo.avaliable || 0
         }
     },
-    watch:{
-        service:{
-            deep:true,
-            immediate:true,
-            handler(n,o){
-                if(n){
+    watch: {
+        service: {
+            deep: true,
+            immediate: true,
+            handler(n, o) {
+                if (n) {
                     this.formatData()
                 }
             }
         }
+    },
+    created() {
+        let curLocal = localStorage.getItem('local')
+        let attr = 'CountryEn'
+        if (curLocal.includes('cn') || curLocal.includes('CN')) {
+            this.countryArr = countries
+            this.twCityArr = twCitys
+        } else if (curLocal.includes('tw') || curLocal.includes('TW')) {
+            this.twCityArr = twTCitys
+            for (const key in enCountries) {
+                this.countryArr.push(
+                    {
+                        cn: enCountries[key].CountryTw,
+                    }
+                )
+            }
+        } else {
+            this.twCityArr = twTCitys
+            for (const key in enCountries) {
+                this.countryArr.push(
+                    {
+                        cn: enCountries[key].CountryEn,
+                    }
+                )
+            }
+        }
     }
 }
 </script>

+ 2 - 2
TEAMModelOS/ClientApp/src/view/auth/Serial.vue

@@ -162,7 +162,7 @@
                     </vuescroll>
                 </div>
             </Split>
-            <!-- 修改课堂记录名称 -->
+            <!-- 绑定教室 -->
             <Modal v-model="rltRoomStatus" className="ed-name-modal" footer-hide>
                 <div slot="header" class="modal-header">
                     {{$t('auth.rltClass')}}
@@ -454,7 +454,7 @@ export default {
             }
         },
         formatData() {
-            let data = this.serial
+            let data = this._.cloneDeep(this.serial)
             let timestamp = Date.now()
             console.log(timestamp)
             data.forEach(item => {

+ 1 - 0
TEAMModelOS/ClientApp/src/view/classmgt/ClassNotice.less

@@ -82,4 +82,5 @@
     margin-bottom: 20px;
     text-indent: 2em;
     color: #515a6e;
+    
 }

+ 3 - 2
TEAMModelOS/ClientApp/src/view/classmgt/ClassNotice.vue

@@ -109,7 +109,8 @@ export default {
                 admin: "1",
                 code: this.$store.state.userInfo.schoolCode,
                 type: "class",
-                classes: this.classList.map(item => { return item.id })
+                classes: this.classList.map(item => { return item.id }),
+                scope:'school'
             }
             this.$api.notice.FindNotice(params).then(
                 res => {
@@ -142,7 +143,7 @@ export default {
                 onOk: () => {
                     let params = {
                         id: this.notifyListShow[index].id,
-                        code: this.notifyListShow[index].code.replace('Notice-', '')
+                        code: this.notifyListShow[index].code
                     }
                     this.$api.notice.DelNotice(params).then(
                         res => {

+ 1 - 0
TEAMModelOS/ClientApp/src/view/classmgt/CreateNotice.vue

@@ -167,6 +167,7 @@ export default {
                 creatorId: this.$store.state.userInfo.TEAMModelId,
                 autoDelete: this.notifyInfo.autoDelete,
                 publish,
+                scope: 'school',
                 createTime: new Date().getTime()
             }
             this.$api.notice.UpsertNotice(params).then(

+ 417 - 0
TEAMModelOS/ClientApp/src/view/classrecord/ClassRecordNew.less

@@ -0,0 +1,417 @@
+@first-bgColor: #141414;
+@second-bgColor: #1b1b1b;
+@third-bgColor: #222222;
+@borderColor: #424242;
+@primary-textColor: #fff; //文本主颜色
+@second-textColor: #a5a5a5; //文本副级颜色
+@primary-fontSize: 14px;
+@second-fontSize: 15px;
+
+.class-record-container {
+    width: 100%;
+    background: #ffffff;
+    height: 100%;
+}
+
+.class-record-header {
+    width: 100%;
+    box-shadow: 0px 2px 5px #e9e9e9;
+    padding: 15px 15px;
+    background: white;
+    .course-name {
+        padding: 4px 10px;
+        margin-right: 10px;
+        border-radius: 4px;
+        font-size: 15px;
+        font-weight: bold;
+        border: 1px solid;
+    }
+
+    .record-name {
+        font-size: 18px;
+        font-weight: bold;
+        vertical-align: top;
+    }
+
+    .label-value {
+        color: #1CC0F3;
+        font-weight: bold;
+        margin-right: 30px;
+    }
+}
+
+.class-content {
+    width: 100%;
+    margin-left: 10px;
+    margin-top: 7px;
+    position: relative;
+    &:hover .cus-page-wrap {
+        opacity: 1;
+    }
+
+    &:hover .cur-page-tag {
+        opacity: 0;
+    }
+}
+.cus-data-wrap{
+    width: ~"calc(100% - 20px)";
+    margin-left: 10px;
+    margin-top: 5px;
+}
+
+.video-player-box {
+    width: ~"calc(100% - 15px)";
+    box-shadow: 0px 0px 10px 2px #d8d8d8;
+    max-height: 450px;
+    display: flex;
+    position: relative;
+    background: #fff;
+}
+.courseware-wrap {
+    width: ~"calc(100% - 15px)";
+    margin-right: 15px;
+    position: relative;
+    max-height: 450px;
+    margin-top: 10px;
+    background: #fff;
+    box-shadow: 0px 0px 10px 2px #d8d8d8;
+
+    .full-screen-icon {
+        float: right;
+        margin-right: 20px;
+        margin-top: -20px;
+        cursor: pointer;
+    }
+
+    .cur-page-tag {
+        position: absolute;
+        left: 20px;
+        bottom: 5px;
+        width: 30px;
+        height: 30px;
+        font-size: 18px;
+        line-height: 30px;
+        display: block;
+        background: rgba(255,153,0,0.9);
+        border-radius: 50%;
+        text-align: center;
+        opacity: 1;
+        transition: opacity 0.2s;
+        color: white;
+        box-shadow: 0px 0px 5px rgb(255, 153, 0);
+    }
+}
+.cus-page-wrap {
+    left: 0px;
+    bottom: 0px;
+    position: absolute;
+    padding: 6px;
+    width: 100%;
+    height: 36px;
+    background: rgba(43, 51, 63, 0.7);
+    text-align: center;
+    opacity: 1;
+    transition: opacity 1s;
+    border-right: 2px solid black;
+}
+
+.content-title {
+    border-left: 4px solid #1CC0F3;
+    padding-left: 4px;
+    line-height: 20px;
+    font-size: 20px;
+    margin-top: 10px;
+    margin-bottom: 6px;
+    margin-left: 10px;
+    width: fit-content;
+}
+.e-note-tag{
+    font-size: 14px;
+    padding: 2px 5px;
+    margin-left: 30px;
+    vertical-align: top;
+    cursor: pointer;
+    user-select: none;
+    &:hover{
+        color: #2db7f5;
+    }
+}
+
+.page-content {
+    width: 10%;
+    height: 500px;
+    border: 1px solid @borderColor;
+    background: #515558;
+    height: 450px;
+}
+
+.page-filter-icon {
+    position: absolute;
+    color: white;
+    left: -28px;
+    top: 5px;
+    cursor:pointer;
+    font-size:18px;
+}
+.page-info-item {
+    color: white;
+    border-bottom: 1px solid @borderColor;
+    padding: 5px;
+    cursor: pointer;
+    transition: background 0.2s;
+
+    &:hover {
+        background: white;
+        color: #1CC0F3;
+    }
+
+    .page-time {
+        float: right;
+    }
+}
+
+.interaction-record-wrap {
+    width: 100%;
+    box-shadow: 5px 5px 500px #f0f0f0 inset;
+    border-radius: 4px;
+    border: 1px solid #f0f0f0;
+    margin-bottom: 10px;
+    position: relative;
+    background: #f9f9f9;
+    min-height: 300px;
+}
+
+.class-file-wrap {
+    width: 100%;
+    box-shadow: 5px 5px 500px #404040 inset;
+    border-radius: 4px;
+    border: 1px solid #404040;
+    margin-bottom: 10px;
+    padding: 20px 30px 20px 30px;
+    color: white;
+    justify-content: space-between;
+    display: flex;
+    align-items: center;
+
+    li {
+        margin: 5px 0px;
+    }
+}
+
+.upload-class-file-wrap {
+    text-align: center;
+    color: #a5a5a5;
+    margin-right: 30px;
+    cursor: pointer;
+
+    &:hover {
+        color: #1cc0f3;
+    }
+
+    .upload-class-file-icon {
+        margin: auto;
+        font-size: 60px;
+    }
+
+    .upload-class-file-text {
+        display: block;
+        margin-top: 5px;
+    }
+}
+
+
+.interaction-type-wrap {
+    position: absolute;
+    padding: 10px 0px;
+    width:50px;
+    right: 0px;
+    top: 0px;
+    text-align: center;
+    background: white;
+    z-index:999;
+    box-shadow: 0px 0px 5px rgba(0,0,0,0.1);
+    .type-icon {
+        font-size: 30px;
+        // color: white;
+        display: block;
+        cursor: pointer;
+        margin-bottom: 15px;
+
+        &:hover {
+            color: #1CC0F3;
+        }
+    }
+
+    .type-icon-active {
+        color: #1cc0f3 !important;
+    }
+}
+
+.page-item {
+    display: flex;
+    position: relative;
+    // background: white;
+    padding: 10px 70px 10px 20px;
+    .record-data-wrap {
+        width: 100%;
+        // padding-left: 15px;
+        padding-top: 5px;
+        border-bottom: 1px dashed #e0e0e0;
+    }
+}
+.page-info-wrap{
+    margin-right: 10px;
+    max-width: 160px;
+    padding-right: 15px;
+    // border-right: 1px dashed #e0e0e0;
+}
+
+.page-tag {
+    border: 1px solid #1cc0f3;
+    color: #1CC0F3;
+    white-space: initial;
+    display: block;
+    font-size: 14px;
+    width: 110px;
+    padding: 3px 10px;
+    border-radius: 4px;
+    cursor: pointer;
+    height: fit-content;
+    transition: background 0.3s;
+    margin-right: 10px;
+    user-select: none;
+    &:hover{
+        color: white;
+        background: #1cc0f3;
+    }
+}
+.page-mini-img{
+    width:110px;
+    margin-top:10px;
+    cursor:pointer;
+    border: 1px solid transparent;
+    border-color:#cccccc;
+}
+
+
+.question-time {
+    position: absolute;
+    left: 0px;
+    bottom: -24px;
+}
+
+.image-viewer {
+    background-color: rgba(0, 0, 0, 0.8);
+    z-index: 9999;
+    width: 100%;
+    height: 100%;
+    position: fixed;
+    top: 0;
+    left: 0;
+    overflow-y: scroll;
+    overflow-x: hidden;
+    text-align: center;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+.htex-viewer {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+
+    .viewer-page-wrap {
+        margin-top: -36px;
+        z-index: 99;
+        padding: 6px;
+        color: white;
+        width: 100%;
+        background: rgba(43, 51, 63, 0.7);
+    }
+}
+
+.image-viewer img {
+    max-width: 50%;
+    margin: 5% auto;
+    border: none;
+}
+.close-icon {
+    position: absolute;
+    right: 15px;
+    top: 10px;
+    font-size: 20px;
+    color: white;
+    cursor: pointer;
+}
+.course-cur-img{
+    max-width: 100%;
+    max-height: 100%;
+}
+.sokrate-report{
+    max-width: 100%;
+    max-height: 100%;
+    margin: auto;
+    display: block;
+}
+.event-tag{
+    display: inline-block;
+    background: white;
+    padding: 1px 8px;
+    margin-bottom: 5px;
+    border: 1px solid #e0e0e0;
+    margin-left: -5px;
+    color: #555555;
+    cursor: pointer;
+    font-size: 12px;
+    &:hover{
+        background: #2db7f5;
+        color: white;
+    }
+}
+.page-event-list{
+
+}
+.event-item{
+    border: 1px dashed transparent;
+    margin-bottom: 15px;
+    padding: 5px 5px;
+}
+.event-item:hover{
+    border: 1px dashed #e0e0e0;
+}
+.student-event{
+    display: flex;
+    justify-content: end;
+}
+.back-page{
+    margin-right: 15px;
+    cursor: pointer;
+    user-select: none;
+}
+.action-wrap{
+    float: right;
+
+}
+.toggle-view{
+    position: absolute;
+    right: 5px;
+    bottom: 5px;
+    z-index: 9999;
+}
+.no-video-tips{
+    position: absolute;
+    top: 0px;
+    left: 0px;
+    color: #ff9900;
+    width: 100%;
+}
+.rcd-split-pane{
+    height: ~"calc(100% - 60px)";
+}
+.top-wrap{
+    width: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+}

+ 612 - 0
TEAMModelOS/ClientApp/src/view/classrecord/ClassRecordNew.vue

@@ -0,0 +1,612 @@
+<template>
+    <div class="class-record-container">
+        <!--头部信息-->
+        <div class="class-record-header">
+            <span class="back-page" @click="goBack">
+                <Icon type="md-arrow-round-back" />
+                {{$t('cusMgt.rcd.rtn')}}
+            </span>
+            <span class="course-name">
+                {{recordInfo.name}}
+            </span>
+            <div style="display:inline-block;margin-left:30px">
+                <span class="label-text">
+                    {{$t('cusMgt.rcd.ctime')}}
+                </span>
+                <span class="label-value">
+                    {{$jsFn.timeFormat(recordInfo.startTime)}}
+                </span>
+            </div>
+            <div class="action-wrap">
+                <!-- 电子笔记 -->
+                <span class="e-note-tag" @click="viewENote">
+                    <Icon type="ios-paper" />
+                    {{$t('cusMgt.rcd.enote')}}
+                </span>
+                <!-- 表格下载 -->
+                <span class="e-note-tag" @click="downloadData">
+                    <Icon type="md-download" />
+                    {{$t('cusMgt.rcd.exportData')}}
+                </span>
+                <!-- 数据统计 -->
+                <span class="e-note-tag" v-show="hasVideo" @click="isShowVd = !isShowVd">
+                    <Icon :type="isShowVd ? 'md-podium' : 'logo-youtube'" />
+                    {{isShowVd ? $t('cusMgt.rcd.dataCount') : $t('cusMgt.rcd.videoData')}}
+                </span>
+            </div>
+        </div>
+        <Split v-model="split1">
+            <div slot="left" class="rcd-split-pane">
+                <!--上课内容-->
+                <vuescroll>
+                    <!-- <div :class="isShowBar ? 'class-content mouse-over-status':'class-content mouse-over-status'" @mousemove="isShowBar = true" @mouseleave="isShowBar = false"> -->
+                    <div class="class-content mouse-over-status">
+                        <video-player2 v-if="hasVideo" v-show="isShowVd" @on-vd-error="videoError" class="video-player-box" :markers="markers" ref="videoPlayer" :options="playerOptions" :playsinline="true" @getCurPage="getCurPage">
+                        </video-player2>
+                        <div v-show="!isShowVd" class="video-player-box" style="padding:25px 0px">
+                            <Alert v-show="!hasVideo" class="no-video-tips" type="warning" show-icon>
+                                {{$t('cusMgt.rcd.noVideo')}}
+                            </Alert>
+                            <DataCount :rcdInfo="recordInfo"></DataCount>
+                        </div>
+                        <div class="courseware-wrap">
+                            <img :src="curImg" alt="" class="course-cur-img">
+                            <div class="cus-page-wrap">
+                                <Page :total="pageList.length" :current.sync="curPage" :page-size="1" size="small" @on-change="getCurHTEX" />
+                            </div>
+                        </div>
+                    </div>
+                </vuescroll>
+            </div>
+            <div slot="right" class="rcd-split-pane">
+                <!--课堂互动记录-->
+                <div class="top-wrap">
+                    <h2 class="content-title">
+                        {{$t('cusMgt.rcd.rcdLabel')}}
+                    </h2>
+                    <div style="margin-top:5px;margin-right:3px">
+                        <RadioGroup v-model="filterType" size="small" @on-change="handleFilter">
+                            <Radio v-for="item in filterInte" :key="item.value" :label="item.value" border>
+                                {{item.text}}
+                                {{item.value == 'all' ? '' : `(${item.count})`}}
+                            </Radio>
+                        </RadioGroup>
+                    </div>
+                </div>
+                <vuescroll ref="pagewrap">
+                    <div class="cus-data-wrap">
+                        <div class="interaction-record-wrap">
+                            <template v-for="(item,index) in pageListShow">
+                                <div v-if="item.pageData && item.pageData.length" class="page-item" :key="index" :id="'page'+(item.page)">
+                                    <div class="page-info-wrap">
+                                        <span class="page-tag" @click="toVideo(index+1,$event)" :ref="'page'+(index+1)">
+                                            {{`${$t('cusMgt.rcd.cw')}${item.page}${$t('cusMgt.rcd.page')}`}}
+                                        </span>
+                                        <!-- 课件缩略图 -->
+                                        <img class="page-mini-img" @click="openViewer(item.img)" :src="item.img" />
+                                        <!-- <Timeline style="margin-top:10px;margin-left:-5px">
+                                            <TimelineItem v-for="(rtItem, rtIndex) in item.pageData" :key="rtIndex +''+index">
+                                                <Icon type="md-arrow-dropright" slot="dot" />
+                                                <p class="event-tag" @click="toEvent(rtItem)">{{rtItem.eventName}}</p>
+                                            </TimelineItem>
+                                        </Timeline> -->
+                                    </div>
+                                    <div class="record-data-wrap">
+                                        <!-- 互动数据 -->
+                                        <div v-for="event in item.pageData" :key="event.Time">
+                                            <!-- 即问即答 -->
+                                            <PopQues class="event-item" v-if="event.Event === 'PopQuesLoad' || event.Event === 'ReAtmpAnsStrt'" :evtType="event.Event" :irsData="event.data"></PopQues>
+                                            <!-- 抢权 -->
+                                            <Buzr class="event-item student-event" v-else-if="event.Event === 'BuzrAns'" :buzrData="event.data" :students="baseData.student"></Buzr>
+                                            <!-- 推送 -->
+                                            <Push class="event-item" v-else-if="event.Event === 'FastPgPush'" :pushData="event.data"></Push>
+                                            <!-- 作品收集 -->
+                                            <Receive :recordInfo="recordInfo" class="student-event event-item" v-else-if="event.Event === 'WrkSpaceEnd'" :rcvData="event.data" :students="baseData.student"></Receive>
+                                            <!-- 随机挑人 -->
+                                            <Pick class="event-item student-event" :pickData="event.data" v-else-if="event.Event === 'PickupResult'" :students="baseData.student"></Pick>
+                                            <!-- 课中评测 -->
+                                            <Exam class="event-item" :examInfo="event.data" :recordInfo="recordInfo" v-else-if="event.Event === 'SPQStrt'"></Exam>
+                                        </div>
+                                    </div>
+                                </div>
+                            </template>
+                        </div>
+                    </div>
+                </vuescroll>
+            </div>
+        </Split>
+        <div v-if="openImageViewer" class="image-viewer" @click="closeViewer()">
+            <Icon type="md-close" class="close-icon" @click="closeViewer()" />
+            <img :src="viewUrl" class="animated fadeIn" @click.stop />
+        </div>
+        <!--返回顶部-->
+        <BackToTop @on-to-top="handleToTop"></BackToTop>
+    </div>
+</template>
+<script>
+import PopQues from './eventchart/PopQues.vue'
+import Buzr from './eventchart/Buzr.vue'
+import Pick from './eventchart/Pick.vue'
+import Push from './eventchart/Push.vue'
+import Exam from './eventchart/Exam.vue'
+import Receive from './eventchart/Receive.vue'
+import DataCount from './eventchart/DataCount.vue'
+import CountTo from 'vue-count-to'
+export default {
+    components: {
+        PopQues, Pick, Push, Receive, DataCount, CountTo, Buzr, Exam
+    },
+    data() {
+        return {
+            filterType: 'all',
+            filterInte: [
+                {
+                    value: 'all',
+                    text: this.$t('cusMgt.rcd.filter1'),
+                },
+                {
+                    value: 'FastPgPush',
+                    text: this.$t('cusMgt.rcd.filter2'),
+                    count: 0
+                },
+                {
+                    value: 'WrkSpaceEnd',
+                    text: this.$t('cusMgt.rcd.filter3'),
+                    count: 0
+                },
+                {
+                    value: 'Other',
+                    text: this.$t('cusMgt.rcd.filter4'),
+                    count: 0
+                },
+                {
+                    value: 'SPQStrt',
+                    text: this.$t('cusMgt.rcd.filter5'),
+                    count: 0
+                }
+            ],
+            split1: 0.4,
+            backPage: undefined,
+            baseData: {}, //base.json
+            pushData: [],//push.json
+            irsData: [],//irs.json
+            taskData: [],//task.json
+            fnEvents: [],//功能事件
+            events: [],//事件ID
+            hiTeachEvent: [],//需要解析的事件信息
+            isShowVd: true,
+            hasVideo: true,
+            eventClick: false,
+            sokratesRecords: [],
+            pageIds: [],
+            pageEvents: [],
+            pageList: [],
+            videoUrl: '',
+            videoPoster: '',
+            recordInfo: {},
+            isShowBar: false,
+            markers: [],
+            curPage: 1,
+            viewUrl: '',
+            openImageViewer: false,
+            typeIndex: 'all',
+            playerOptions: {
+                height: "450px",
+                controlBar: {
+                    durationDisplay: true, // 总时间
+                    currentTimeDisplay: true,
+
+                },
+                html5: {
+                    nativeControlsForTouch: true,
+                },
+                inactivityTimeout: 1,
+                nativeVideoTracks: false,
+                playbackRates: [0.7, 1.0, 1.5, 2.0], //播放速度
+                autoplay: true, //如果true,浏览器准备好时开始回放。
+                controls: true, //控制条
+                preload: 'auto', //视频预加载
+                muted: false, //默认情况下将会消除任何音频。
+                loop: false, //导致视频一结束就重新开始。
+                language: 'zh-CN',
+                //aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
+                //fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
+                sources: [],
+                poster: '',
+                // notSupportedMessage: '此视频暂无法播放,请稍后再试' //允许覆盖Video.js无法播放媒体源时显示的默认信息。
+            }
+        }
+    },
+    methods: {
+        //下载统计表格
+        downloadData() {
+            let blobInfo = this.recordInfo.scope === 'school' ? this.$store.state.user.schoolProfile : this.$store.state.user.userProfile
+            let url = `${blobInfo.blob_uri}/records/${this.recordInfo.id}/IES/summary.xlsx?${blobInfo.blob_sas}`
+            const downloadRes = async () => {
+                let response = await fetch(url); // 内容转变成blob地址
+                let blob = await response.blob(); // 创建隐藏的可下载链接
+                let objectUrl = window.URL.createObjectURL(blob);
+                let a = document.createElement('a');
+                a.href = objectUrl;
+                a.download = this.recordInfo.name + '.xlsx';
+                a.click()
+                a.remove();
+            }
+            downloadRes();
+        },
+        videoError() {
+            this.hasVideo = false
+            this.isShowVd = false
+        },
+        //返回顶部
+        handleToTop() {
+            this.$refs['pagewrap'].scrollTo(
+                {
+                    y: '0'
+                },
+                300
+            )
+            // this.$refs['datawrap'].scrollTo(
+            //     {
+            //         y: '0'
+            //     },
+            //     300
+            // )
+        },
+        //跳转到对应事件
+        toEvent(event) {
+            console.log(event)
+            this.eventClick = true
+            this.curPage = event.pageIndex + 1
+            this.$refs.videoPlayer.player.currentTime(event.Time)
+        },
+        //根据SokratesRecords.json处理page数据
+        async getPageList() {
+            this.pageList = []
+            this.markers = []
+            let blobInfo = this.recordInfo.scope == 'school' ? this.$store.state.user.schoolProfile : this.$store.state.user.userProfile
+            // 这里需要兼容原来没有TimeLine.json的课例(优先度读timeLine.json,没有则读SokratesRecords.json)
+            let url = `${blobInfo.blob_uri}/records/${this.recordInfo.id}/IES/TimeLine.json?${blobInfo.blob_sas}`
+            let hasTimeLine = true
+            let dataErr = false
+            try {
+                let res = await this.$tools.getFile(url)
+                this.sokratesRecords = JSON.parse(res)
+                this.pageIds = this.sokratesRecords.PgIdList || []
+                this.pageEvents = this.sokratesRecords.events || []
+            } catch (e) {
+                hasTimeLine = false
+            }
+            //读取 timeLine.json 失败,则读取 SokratesRecords.json
+            if (!hasTimeLine) {
+                url = `${blobInfo.blob_uri}/records/${this.recordInfo.id}/Sokrates/SokratesRecords.json?${blobInfo.blob_sas}`
+                try {
+                    let res = await this.$tools.getFile(url)
+                    let resJson = JSON.parse(res)
+                    // 处理成timeLine数据格式
+                    let pageidEvent = resJson.find(item => item.Event == 'PgidList')
+                    this.pageIds = pageidEvent && pageidEvent.PgIdList ? pageidEvent.PgIdList : []
+                    this.pageEvents = resJson.filter(item => this.events.includes(item.Event))
+                    this.sokratesRecords = {
+                        events: this.pageEvents,
+                        PgIdList: this.pageIds
+                    }
+                } catch (e) {
+                    //timeLine 和 SokratesRecords都没有找到
+                    dataErr = true
+                }
+            }
+            // 数据异常
+            if (dataErr) {
+                this.$Message.error(this.$t('cusMgt.rcd.dataErr'))
+                return
+            }
+            // 时间轴数据正常
+            //获取Push.json、IRS.json、Task.json、Base.json数据
+            try {
+                let pushUrl = `${blobInfo.blob_uri}/records/${this.recordInfo.id}/IES/Push.json?${blobInfo.blob_sas}`
+                this.pushData = JSON.parse(await this.$tools.getFile(pushUrl) || '[]')
+                this.pushData.forEach(item => {
+                    item.pageUrl = `${blobInfo.blob_uri}/records/${this.recordInfo.id}${item.pageMeta}?${blobInfo.blob_sas}`
+                })
+            } catch (e) {
+                this.pushData = []
+            }
+            try {
+                let irsUrl = `${blobInfo.blob_uri}/records/${this.recordInfo.id}/IES/IRS.json?${blobInfo.blob_sas}`
+                this.irsData = JSON.parse(await this.$tools.getFile(irsUrl) || '[]')
+            } catch (e) {
+                this.irsData = []
+            }
+            try {
+                let taskUrl = `${blobInfo.blob_uri}/records/${this.recordInfo.id}/IES/Task.json?${blobInfo.blob_sas}`
+                this.taskData = JSON.parse(await this.$tools.getFile(taskUrl) || '[]')
+            } catch (e) {
+                this.taskData = []
+            }
+            try {
+                let baseUrl = `${blobInfo.blob_uri}/records/${this.recordInfo.id}/IES/base.json?${blobInfo.blob_sas}`
+                this.baseData = JSON.parse(await this.$tools.getFile(baseUrl) || '{}')
+            } catch (e) {
+                this.baseUrl = {}
+            }
+            let pgids = this.pageIds
+
+            //这里需要判断录制开始的pageid
+            let startInfo = this.pageEvents.findLast(item => item.Event === 'EzsStartRecord')
+            let startId = startInfo ? startInfo.Pgid : ''
+            let startIndex = 0
+            if (startId) {
+                startIndex = pgids.findIndex(item => item === startId)
+                //第一页视频标记
+                this.markers.push({
+                    time: startInfo.Time,
+                    text: `${this.$t('cusMgt.rcd.di')}${1}${this.$t('cusMgt.rcd.page')}`,
+                    page: 1
+                })
+            }
+            if (startIndex > -1) {
+                pgids = pgids.slice(startIndex)
+            }
+            pgids.forEach((item, index) => {
+                let page = {}
+                page.id = item
+                page.img = `${blobInfo.blob_uri}/records/${this.recordInfo.id}/Memo/${item}.jpg?${blobInfo.blob_sas}`
+                page.page = index + 1
+                //当前页面对应的功能事件
+                page.pageData = this.pageEvents.filter(record => record.Pgid === item && this.fnEvents.includes(record.Event))
+                page.pageData.forEach(e => {
+                    e.pageIndex = index
+                    e.eventName = this.hiTeachEvent[e.Event]?.text
+                    let rlt = this.hiTeachEvent[e.Event]?.relation
+                    e.relation = rlt
+                    switch (rlt) {
+                        case 'irs':
+                            e.data = this.irsData.find(i => i.pageID == e.Pgid)
+                            this.filterInte[3].count++
+                            break
+                        case 'push':
+                            e.data = this.pushData.find(p => p.pageId == e.Pgid || p.pageID == e.Pgid) //数据兼容 pageId 有些大写有些小写
+                            this.filterInte[1].count++
+                            break
+                        case 'task':
+                            e.data = this.taskData.find(t => t.pageID == e.Pgid)
+                            this.filterInte[2].count++
+                            break
+                        case 'timeline':
+                            e.data = this._.cloneDeep(e)
+                            this.filterInte[3].count++
+                            break
+                        case 'exam':
+                            e.data = this._.cloneDeep(e)
+                            this.filterInte[4].count++
+                            break
+                        default:
+                            break
+                    }
+                })
+                this.pageList.push(page)
+            })
+            console.log(this.pageList)
+            this.pageListShow = this.pageList
+            let pageEvent = this.pageEvents.filter(item => item.Event === 'PgJump' || item.Event === 'PgAdd' || item.Event === 'DiscussStart')
+            let page = this.markers.length
+            pageEvent.forEach((item, index) => {
+                if (item.JumpTo != item.JumpFrom) {
+                    this.markers.push({
+                        time: item.Time,
+                        text: `${this.$t('cusMgt.rcd.di')}${item.JumpTo}${this.$t('cusMgt.rcd.page')}`,
+                        page: item.JumpTo
+                    })
+                }
+            })
+            console.log(this.markers)
+        },
+        //查看苏格拉底报告
+        viewReport() {
+            this.$router.push({
+                name: 'teachCenter',
+                query: { id: this.recordInfo.id, name: this.recordInfo.name }
+            })
+        },
+        //查看电子笔记
+        viewENote() {
+            let eNote
+            if (this.recordInfo.eNote) {
+                eNote = this.recordInfo.eNote
+            } else {
+                let sasInfo = {}
+                let blobInfo = this.recordInfo.scope === 'school' ? this.$store.state.user.schoolProfile : this.$store.state.user.userProfile
+                sasInfo.sas = '?' + blobInfo.blob_sas
+                sasInfo.name = this.recordInfo.scope ? this.$store.state.userInfo.schoolCode : this.$store.state.userInfo.TEAMModelId
+                sasInfo.url = blobInfo.blob_uri.slice(0, blobInfo.blob_uri.lastIndexOf(sasInfo.name) - 1)
+
+                eNote = `${sasInfo.url}/${sasInfo.name}/records/${this.recordInfo.id}/Note.pdf${sasInfo.sas}`
+            }
+            window.open('/web/viewer.html?file=' + encodeURIComponent(eNote))
+            // if (this.recordInfo.eNote) {
+            // } else {
+            //     this.$Message.warning(this.$t('cusMgt.rcd.noNote'))
+            // }
+        },
+        //点击视频切片
+        getCurPage(page) {
+            // this.$refs["datawrap"].scrollIntoView('#page' + page, 500)
+            this.$refs.videoPlayer.player.play()
+            this.curPage = page
+        },
+        //点击课件page
+        getCurHTEX(page) {
+            if (this.eventClick) {
+                this.eventClick = false
+                return
+            }
+            //视频时间定位
+            let pageInfo = this.markers.find(item => {
+                return item.page == page
+            })
+            if (pageInfo) {
+                this.$refs.videoPlayer.player.currentTime(pageInfo.time)
+                this.$refs.videoPlayer.player.play()
+            } else {
+                // this.$refs["datawrap"].scrollIntoView('#page' + page, 500)
+            }
+        },
+        //点击互动记录页面tag
+        toVideo(page, e) {
+            //页面滚动
+            // let dataLoacation = this.$refs["datawrap"].getPosition()
+            // let pageLocaltion = this.$refs["pagewrap"].getPosition()
+            // let y = e.pageY - 665 + pageLocaltion.scrollTop + dataLoacation.scrollTop
+            this.$nextTick(() => {
+                // this.$refs["pagewrap"].scrollTo(
+                //     {
+                //         x: 0,
+                //         y: 0
+                //     }
+                // )
+                // this.$refs["datawrap"].scrollTo(
+                //     {
+                //         x: 0,
+                //         y: y
+                //     }
+                // )
+            })
+            // 教材页面切换
+            this.curPage = page
+            //视频时间定位
+            let pageInfo = this.markers.find(item => {
+                return item.page == page
+            })
+            if (pageInfo) this.$refs.videoPlayer.player.currentTime(pageInfo.time)
+        },
+        //互动类型筛选
+        handleFilter() {
+            if (this.filterType == 'all') {
+                this.pageListShow = this.pageList
+            } else {
+                let data = this._.cloneDeep(this.pageList)
+                let events = ['FastPgPush','WrkSpaceEnd','SPQStrt',]
+                this.pageListShow = data.map(item => {
+                    if (item.pageData.length) {
+                        item.pageData = item.pageData.filter(event=>{
+                            if(this.filterType == 'Other'){
+                                return !events.includes(event.Event)
+                            }else{
+                                return event.Event == this.filterType
+                            }
+                        })
+
+                    }
+                    return item
+                })
+            }
+
+        },
+        //查看图片
+        openViewer(url) {
+            this.openImageViewer = true
+            this.viewUrl = url
+        },
+        closeViewer() {
+            this.openImageViewer = false
+        },
+        goBack() {
+            if (this.backPage) {
+                this.$router.push({
+                    name: this.backPage,
+                    record: this.recordInfo
+                })
+            } else {
+                this.$router.go(-1)
+            }
+        },
+    },
+    computed: {
+        curImg() {
+            console.log(this.curPage)
+            if (this.pageList[this.curPage - 1]) {
+                return this.pageList[this.curPage - 1].img
+            } else {
+                return ""
+            }
+        }
+    },
+    created() {
+        this.hiTeachEvent = this.$GLOBAL.HI_TEACH_EVENT()
+        //正式站先暂时不放出课中评测
+        if (this.$store.state.config.srvAdrType == 'product') {
+            this.$delete(this.hiTeachEvent, 'SPQStrt')
+        }
+        this.events = Object.keys(this.hiTeachEvent)
+        this.fnEvents = this.events.filter(key => this.hiTeachEvent[key].type === 'fn')
+        console.log(this.events, this.fnEvents)
+        this.recordInfo = this.$route.params.record || (sessionStorage.getItem('record') ? JSON.parse(sessionStorage.getItem('record')) : undefined)
+        console.log(this.recordInfo)
+        if (!this.recordInfo) {
+            this.$router.go(-1)
+        } else {
+            sessionStorage.setItem('record', JSON.stringify(this.recordInfo))
+            //对接Blob数据
+            let blobInfo = this.recordInfo.scope == 'school' ? this.$store.state.user.schoolProfile : this.$store.state.user.userProfile
+            this.videoUrl = `${blobInfo.blob_uri}/records/${this.recordInfo.id}/Record/CourseRecord.mp4?${blobInfo.blob_sas}`
+            this.videoPoster = `${blobInfo.blob_uri}/records/${this.recordInfo.id}/Record/CoverImage.jpg?${blobInfo.blob_sas}`
+            this.playerOptions.poster = this.videoPoster
+            this.playerOptions.sources.push({
+                src: this.videoUrl
+            })
+            this.getPageList()
+        }
+    },
+    beforeRouteEnter(to, from, next) {
+        next(vm => {
+            if (from.name == 'homePage') {
+                vm.backPage = 'myCourse'
+            }
+            console.log(arguments)
+        })
+    }
+}
+</script>
+<style lang="less" scoped>
+@import "./ClassRecordNew.less";
+</style>
+<style>
+.top-wrap .ivu-radio-wrapper-checked {
+    color: white;
+    background: #2d8cf0;
+}
+.top-wrap .ivu-radio {
+    display: none;
+}
+.class-content .vjs-progress-holder {
+    font-size: 10px !important;
+}
+.mouse-over-status .vjs-control-bar {
+    opacity: 1 !important;
+}
+
+.class-content .video-js .vjs-big-play-button {
+    top: 50%;
+    left: 50%;
+    margin-left: -40px;
+    margin-top: -20px;
+}
+
+.class-content .vjs_video_3-dimensions.vjs-fluid {
+    padding-top: 0;
+}
+
+.class-content .video-js.vjs-fluid,
+.class-content .video-js.vjs-16-9,
+.class-content .video-js.vjs-4-3 {
+    height: 450px;
+}
+
+.video-player-box .vjs-volume-panel {
+    display: none;
+}
+.video-player-box .ivu-alert {
+    border-radius: 0px;
+}
+</style>

+ 1 - 0
TEAMModelOS/ClientApp/src/view/classrecord/eventchart/Exam.vue

@@ -310,5 +310,6 @@ export default {
 }
 .exam-chart-wrap{
     display: flex;
+    flex-wrap: wrap;
 }
 </style>

+ 1 - 0
TEAMModelOS/ClientApp/src/view/dashboard/Art.vue

@@ -128,6 +128,7 @@ export default {
   },
   created() {
     this.curPeriod = this.$store.state.user.curPeriod
+    this.$store.state.dashboard.classType = 'all'
     this.getDashboardData()
     this.onChangeSubject('music')
   },

+ 5 - 1
TEAMModelOS/ClientApp/src/view/dashboard/Research.vue

@@ -120,7 +120,11 @@
 					"code": this.$store.state.user.schoolCode,
 					"periodId":this.$store.state.user.curPeriod.id
 				}).then(res => {
-					this.$store.commit('setRearchDashboardData',res)
+					if(res.code && res.code == 404){
+						this.$Message.warning(this.$t('lessonRecord.noRecordTip'))
+					}else{
+						this.$store.commit('setRearchDashboardData',res)
+					}
 					this.cancelLoading()
 				})
 			},

+ 1 - 1
TEAMModelOS/ClientApp/src/view/homepage/HomePage.less

@@ -427,7 +427,7 @@
     display: flex;
     align-items: center;
     border-bottom: 1px solid #f3f3f3;
-    padding: 10px 5px 10px 0px;
+    padding: 10px 5px 10px 5px;
     cursor: pointer;
     &:hover{
         background: #f3f3f3;

+ 12 - 7
TEAMModelOS/ClientApp/src/view/homepage/HomePage.vue

@@ -320,13 +320,17 @@
                 </div>
             </div>
         </vuescroll>
-        <Modal v-model="viewNoticeStatus" :title="noticeList[noticeIndex] ? noticeList[noticeIndex].title : ''" width="600">
-            <template v-if="noticeList[noticeIndex]">
-                <p class="notice-content" v-html="noticeList[noticeIndex].content"></p>
-            </template>
+        <Modal v-model="viewNoticeStatus" className="ed-name-modal" footer-hide width="600">
+            <div slot="header" class="modal-header">
+                {{noticeList[noticeIndex] ? noticeList[noticeIndex].title : ''}}
+            </div>
+            <div class="edit-name-content">
+                <p class="notice-content">
+                    {{noticeList[noticeIndex] ? noticeList[noticeIndex].content : ''}}
+                </p>
+            </div>
         </Modal>
     </div>
-
 </template>
 <script>
 import { mapGetters } from 'vuex'
@@ -641,7 +645,8 @@ export default {
             let params = {
                 code: this.$store.state.userInfo.schoolCode,
                 type: "school",
-                publish: 1
+                publish: 1,
+                scope: 'school'
             }
             this.$api.notice.FindNotice(params).then(
                 res => {
@@ -784,7 +789,7 @@ export default {
                             Vote: 3,
                             Survey: 4,
                         }
-                        this.acCount.sort((a,b)=>{
+                        this.acCount.sort((a, b) => {
                             return sortMap[a.key] - sortMap[b.key]
                         })
                     }

File diff suppressed because it is too large
+ 975 - 971
TEAMModelOS/ClientApp/src/view/homework/ManageHomeWork.vue


+ 10 - 9
TEAMModelOS/ClientApp/src/view/learnactivity/CreatePrivEva.vue

@@ -299,15 +299,16 @@ export default {
          * 返回上一级
          */
         confirmToManage() {
-            if (this.mode == 'private') {
-                this.$router.push({
-                    name: 'privExam'
-                })
-            } else {
-                this.$router.push({
-                    name: 'schoolExam'
-                })
-            }
+            this.$router.go(-1)
+            // if (this.mode == 'private') {
+            //     this.$router.push({
+            //         name: 'privExam'
+            //     })
+            // } else {
+            //     this.$router.push({
+            //         name: 'schoolExam'
+            //     })
+            // }
         },
         /**
          * 处理挑选试卷事件

+ 8 - 0
TEAMModelOS/ClientApp/src/view/learnactivity/ExamMgt.less

@@ -185,4 +185,12 @@
     float: right;
     position: relative;
     z-index: 99;
+}
+.copy-text-info{
+    padding: 5px 10px;
+    cursor: pointer;
+    user-select: none;
+    &:hover{
+        color: #2d8cf0;
+    }
 }

+ 66 - 4
TEAMModelOS/ClientApp/src/view/learnactivity/ExamMgt.vue

@@ -81,9 +81,24 @@
                             <!-- 修改评测结束时间 -->
                             <Icon type="md-create" class="edit-end-time" v-if="item.progress == 'going'" @click="editEvEndtime(index)" :title="$t('learnActivity.mgtScEv.editEndTime')" />
                         </p>
-                        <!-- 二维码分享 -->
-                        <span class="ev-qr-tag" style="top:50%" @click="openQrcode(index)" v-show="item.source == '0' && item.progress == 'going'">
+                        <!-- 二维码分享 (暂时去掉二维码分享,仅提供链接,因为尚未兼容手机端)-->
+                        <!-- <span class="ev-qr-tag" style="top:50%" @click="openQrcode(index)" v-show="item.source == '0' && item.progress == 'going'">
                             <Icon size="20" custom="iconfont icon-qr-code" class="qr-code-icon" />
+                        </span> -->
+                        <span class="ev-qr-tag" style="top:50%" v-if="item.source == '0' && item.progress == 'going'">
+                            <Tooltip content="Here is the prompt text" transfer theme="light">
+                                <Icon type="ios-send" size="20" />
+                                <div slot="content" style="padding:5px">
+                                    <p class="copy-text-info" @click="copyNotice(index)">
+                                        <Icon type="ios-notifications" style="margin-right:5px" />
+                                        {{$t('learnActivity.mgtScEv.shareText9')}}
+                                    </p>
+                                    <p class="copy-text-info" @click="copyLink(index)">
+                                        <Icon type="ios-link" style="margin-right:5px" />
+                                        {{$t('learnActivity.mgtScEv.shareText10')}}
+                                    </p>
+                                </div>
+                            </Tooltip>
                         </span>
                         <!-- 立即结束 图标-->
                         <!-- <span class="ev-qr-tag" style="top:65%" @click="handleEnd(index)" :title="$t('learnActivity.mgtScEv.stop')">
@@ -417,8 +432,45 @@ export default {
                 if (this.$refs['score-box']) this.$refs['score-box'].showTest = false
             })
         },
+        async copyNotice(index) {
+            this.shareUrl = `https://${window.location.host}/studentWeb/examView?aId=${this.evaListShow[index].id}`
+            this.qrConfig.url = this.shareUrl
+            let evName = this.evaListShow[this.curEvaIndex].name
+            let evType = this.getModeLabel(this.evaListShow[this.curEvaIndex].source)
+            let soc = this.evaListShow[this.curEvaIndex].owner === 'school' ? this.$t('learnActivity.mgtScEv.shareText4') : this.$t('learnActivity.mgtScEv.shareText5')
+            let socName = this.evaListShow[this.curEvaIndex].subjects.map(item => item.name).join('、')
+            try {
+                let shortUrl = await this.$api.getShortUrl(encodeURI(this.shareUrl))
+                let shareContent = `${this.$t('learnActivity.mgtScEv.shareText1')}\n\n${soc}${socName}\n${this.$t('learnActivity.mgtScEv.shareText3')}${evType}\n${this.$t('cusMgt.inviteInfo8')}${this.$store.state.userInfo.name}\n${this.$t('learnActivity.mgtScEv.shareText2')}${evName}\n\n${this.$t('learnActivity.mgtScEv.shareText6')}\n${shortUrl.result || encodeURI(this.shareUrl)}\n\n${this.$t('learnActivity.mgtScEv.shareText11')}`
+                this.handleCopy(shareContent)
+            } catch (e) {
+                let shareContent = `${this.$t('learnActivity.mgtScEv.shareText1')}\n\n${soc}${socName}\n${this.$t('learnActivity.mgtScEv.shareText3')}${evType}\n${this.$t('cusMgt.inviteInfo8')}${this.$store.state.userInfo.name}\n${this.$t('learnActivity.mgtScEv.shareText2')}${evName}\n\n${this.$t('learnActivity.mgtScEv.shareText6')}\n${encodeURI(this.shareUrl)}\n\n${this.$t('learnActivity.mgtScEv.shareText11')}`
+                this.handleCopy(shareContent)
+            }
+
+        },
+        async copyLink(index) {
+            try {
+                this.shareUrl = `https://${window.location.host}/studentWeb/examView?aId=${this.evaListShow[index].id}`
+                let shortUrl = await this.$api.getShortUrl(encodeURI(this.shareUrl))
+                this.handleCopy(shortUrl.result)
+            } catch (e) {
+                this.handleCopy(ncodeURI(this.shareUrl))
+            }
+        },
+        handleCopy(content) {
+            //直接复制链接
+            this.$copyText(content).then(
+                ok => {
+                    this.$Message.success(this.$t("settings.copyModal1"))
+                },
+                fail => {
+                    this.$Message.error(this.$t("settings.copyModal2"))
+                }
+            )
+        },
         async openQrcode() {
-            this.shareUrl = `https://${window.location.host}/studentWeb/examView?aId=${this.evaListShow[this.curEvaIndex].id}`
+            this.shareUrl = `https://${window.location.host}/studentWeb/examView?aId=${this.evaListShow[index].id}`
             let shortUrl = await this.$api.getShortUrl(encodeURI(this.shareUrl))
             this.qrConfig.url = this.shareUrl
 
@@ -427,7 +479,17 @@ export default {
             let soc = this.evaListShow[this.curEvaIndex].owner === 'school' ? this.$t('learnActivity.mgtScEv.shareText4') : this.$t('learnActivity.mgtScEv.shareText5')
             let socName = this.evaListShow[this.curEvaIndex].subjects.map(item => item.name).join('、')
             this.qrConfig.shareContent = `${this.$t('learnActivity.mgtScEv.shareText1')}\n\n${soc}${socName}\n${this.$t('learnActivity.mgtScEv.shareText3')}${evType}\n${this.$t('cusMgt.inviteInfo8')}${this.$store.state.userInfo.name}\n${this.$t('learnActivity.mgtScEv.shareText2')}${evName}\n\n${this.$t('learnActivity.mgtScEv.shareText6')}\n${shortUrl.result || encodeURI(this.shareUrl)}\n\n${this.$t('learnActivity.mgtScEv.shareText7')}\nURL:https://${window.location.host}/login/student`
-            this.showQrStatus = true
+            //直接复制链接
+            // this.$copyText(this.qrConfig.shareContent).then(
+            //     ok => {
+            //         this.$Message.success(this.$t("settings.copyModal1"))
+            //     },
+            //     fail => {
+            //         this.$Message.error(this.$t("settings.copyModal2"))
+            //     }
+            // )
+            //暂时隐藏 现在不显示二维码,仅提供复制链接
+            // this.showQrStatus = true
         },
         /**
          * @param isFresh 是否清空当前列表 如果是筛选条件变化需要清空,如果是只是下拉刷新则不需要

+ 1 - 1
TEAMModelOS/ClientApp/src/view/learnactivity/ManualPaper.vue

@@ -228,7 +228,7 @@ export default {
         },
     },
     created() {
-        this.routeScope = this.$route.name == 'createPrivEva' ? 'private' : 'school'
+        this.routeScope = this.$route.name == 'createSchoolEva' ? 'school' : 'private'
         this.$store.dispatch('user/getSchoolProfile').then(
             res => {
                 if (res.school_base) {

+ 166 - 0
TEAMModelOS/ClientApp/src/view/mycourse/MyCourse.less

@@ -0,0 +1,166 @@
+@first-bgColor: #141414;
+@second-bgColor: #1b1b1b;
+@third-bgColor: #222222;
+@primary-textColor: var(--primary-text-color); //文本主颜色
+@second-textColor: var(--second-text-color); //文本副级颜色
+@primary-fontSize: 14px;
+@second-fontSize: 16px;
+
+.my-course-container {
+    width: 100%;
+    height: 100%;
+}
+
+.my-course-header {
+    height: 50px;
+    line-height: 50px;
+    width: 100%;
+    padding: 0px 15px;
+    box-shadow: 0 2px 5px #e9e9e9;
+    display: flex;
+}
+
+.course-action-wrap {
+    min-width: 180px;
+    text-align: right;
+}
+
+.course-list-wrap {
+    padding: 0px 0px 10px 0px;
+    flex: 1;
+    overflow: hidden;
+    transition: height 0.5s;
+    border-radius: 5px;
+    position: relative;
+    z-index: 9;
+}
+
+.course-type-fitcontent {
+    &:hover {
+        height: fit-content;
+        box-shadow: 0 2px 5px #e9e9e9;
+        background: #fafafa;
+    }
+}
+
+.filter-label {
+    font-weight: 800;
+    color: #000000;
+    margin-right: 10px;
+    vertical-align: top;
+}
+
+.course-type {
+    margin-right: 30px;
+}
+
+.my-course-main {
+    width: 100%;
+    height: ~"calc(100% - 50px)";
+}
+
+.course-classroom-list-header {
+    padding-left: 15px;
+    width: 100%;
+    height: 45px;
+    line-height: 45px;
+
+    .add-classroom-icon {
+        float: right;
+        margin-right: 20px;
+        cursor: pointer;
+    }
+
+    .course-classroom-label {
+        color: @second-textColor;
+    }
+}
+
+.course-classroom-list-content {
+    width: 100%;
+    height: ~"calc(100% - 50px)";
+}
+
+.tea-class-item {
+    padding: 10px;
+    padding-left: 15px;
+    cursor: pointer;
+
+    &:hover {
+        background-color: var(--hover-text-color);
+    }
+
+    .class-attr-item {
+        margin: 5px 0px;
+    }
+
+    .attr-label {
+        color: @second-textColor;
+        margin-right: 5px;
+    }
+
+    .class-name {
+        font-size: 14px;
+        color: var(--primary-text-color);
+    }
+
+    .def-class-name {
+        color: #a5a5a5;
+    }
+
+    .class-label {
+        font-size: 12px;
+        color: #19be6b;
+        border: 1px solid #19be6b;
+        padding: 0px 0px;
+        margin-left: 5px;
+        vertical-align: text-top;
+    }
+}
+
+.cus-class-icon {
+    float: right;
+    margin-right: 20px;
+    margin-top: 8px;
+    cursor: pointer;
+    color: var(--normal-icon-color);
+    font-size: 18px;
+    margin-top: 12px;
+}
+
+.qr-code-icon {
+    position: absolute;
+    right: 15px;
+    top: 50%;
+    margin-top: -15px;
+}
+
+.qr-code-icon-color {
+    color: var(--normal-icon-color);
+}
+
+.course-content-header {
+    width: 100%;
+    height: 45px;
+    padding-left: 15px;
+    // background: #f9f9f9;
+    border-bottom: 1px solid #e2e2e2;
+}
+
+.course-classroom-label {
+    color: @second-textColor;
+}
+.cus-action-icon-wrap{
+    display: none;
+}
+.cus-action-icon{
+    margin-left: 5px;
+}
+.course-content-wrap{
+    width: 100%;
+    height: 100%;
+}
+.course-content-wrap .pane{
+    margin-right: 25px;
+    padding: 10px 0px;
+}

+ 978 - 0
TEAMModelOS/ClientApp/src/view/mycourse/MyCourse.vue

@@ -0,0 +1,978 @@
+<template>
+    <div class="my-course-container">
+        <!-- 顶部课程筛选区域 -->
+        <div class="my-course-header">
+            <div class="course-type" v-show="isShowTypeFilter">
+                <span class="filter-label">来源:</span>
+                <RadioGroup v-model="listType" type="button">
+                    <Radio v-for="item in cusType" :label="item.value" :key="item.value">
+                        {{item.label}}
+                    </Radio>
+                </RadioGroup>
+            </div>
+            <div :class="courseTypeClass" @mouseover="checkOverflow" @mouseleave="removeOverflow">
+                <span class="filter-label">课程:</span>
+                <span v-show="!courseListShow.length">暂无课程</span>
+                <RadioGroup v-model="courseId" type="button" id="course-list-box" style="width: calc(100% - 70px);">
+                    <Radio v-for="item in courseListShow" :label="item.id" :key="item.id" style="margin-right:15px">
+                        {{item.name}}
+                        <span v-if="listType == 'private'" class="cus-action-icon-wrap">
+                            <Icon type="md-create" class="cus-action-icon" :title="$t('cusMgt.editCus')" @click="editCus()" />
+                            <Icon type="md-trash" class="cus-action-icon" :title="$t('cusMgt.delCus')" @click="delCourse()" />
+                        </span>
+                    </Radio>
+                </RadioGroup>
+
+            </div>
+            <div class="course-action-wrap">
+                <Button type="text" icon="md-add" @click="addCusStatus = true">新建个人课程</Button>
+            </div>
+        </div>
+        <div class="my-course-main custom-iview-split">
+            <Loading v-show="listLoading"></Loading>
+            <Split v-model="split1">
+                <!-- 课程名单列表 -->
+                <div slot="left" class="course-class-list">
+                    <div class="course-classroom-list-header">
+                        <span class="course-classroom-label">{{ listType === 'school' ? $t('cusMgt.classList') : $t('courseManage.classroom.classroomList')}}</span>
+                        <Tooltip :content="$t('tip.cusClass')" transfer theme="light" max-width="200" v-show="listType === 'private'">
+                            <Icon type="ios-information-circle-outline" color="#40A8F0" style="margin-left:5px; font-size: 16px" />
+                        </Tooltip>
+                        <Icon type="md-add" v-show="listType == 'private'" class="cus-class-icon" @click="newSlStatus = true" />
+                        <Icon type="md-trash" v-show="listType == 'private' && teaClassList.length" class="cus-class-icon" @click="removeListStatus = true" />
+                        <Icon type="md-create" v-show="listType == 'private' && teaClassList.length" class="cus-class-icon" @click="editListName" />
+                    </div>
+                    <div class="course-classroom-list-content">
+                        <vuescroll>
+                            <div v-for="(item,index) in teaClassList" :key="index" @click="selectClass(index)" :class="['block-bg','tea-class-item',curClassIndex == index ? 'block-bg-active':'']">
+                                <p class="class-attr-item">
+                                    <span class="attr-label" v-show="listType === 'school'">{{item.classId ? $t('cusMgt.listType1') : $t('cusMgt.listType2')}}: </span>
+                                    <span class="attr-label" v-show="listType === 'private'">{{$t('cusMgt.nameList')}}:</span>
+                                    <span class="class-name">{{item.classId ? item.classInfo.name: item.listName || '--'}}</span>
+                                </p>
+                                <p class="class-attr-item">
+                                    <span class="attr-label">{{$t('cusMgt.stuCount')}}</span>
+                                    <span class="class-name">{{item.allStu ? item.allStu.length : 0}}{{$t('unit.text7')}}</span>
+                                </p>
+                                <Icon size="25" custom="iconfont icon-qr-code" :class="['qr-code-icon', {'qr-code-icon-color': item.joinLock}]" @click="showQrCode(index)" v-if="listType == 'private'" :title="$t('cusMgt.qrCodeLabel')" />
+                            </div>
+                            <EmptyData v-if="teaClassList.length == 0" :top="160" :textContent="$t('cusMgt.noClassList')"></EmptyData>
+                        </vuescroll>
+                    </div>
+                </div>
+                <div slot="right" class="course-content-wrap">
+                    <div class="course-content-header tab-box">
+                        <span @click="selectTab('exam')" :class="tabName == 'exam' ? 'course-classroom-label pane active':'course-classroom-label pane'">
+                            {{$t('cusMgt.cusTab1')}}
+                        </span>
+                        <span @click="selectTab('hw')" :class="tabName == 'hw' ? 'course-classroom-label pane active':'course-classroom-label pane'">
+                            {{$t('cusMgt.cusTab2')}}
+                        </span>
+                        <span @click="selectTab('vote')" :class="tabName == 'vote' ? 'course-classroom-label pane active':'course-classroom-label pane'">
+                            {{$t('cusMgt.cusTab3')}}
+                        </span>
+                        <span @click="selectTab('survey')" :class="tabName == 'survey' ? 'course-classroom-label pane active':'course-classroom-label pane'">
+                            {{$t('cusMgt.cusTab4')}}
+                        </span>
+                        <span @click="selectTab('record')" :class="tabName == 'record' ? 'course-classroom-label pane active':'course-classroom-label pane'">
+                            {{$t('cusMgt.cusRecord')}}
+                        </span>
+                        <span @click="selectTab('notice')" :class="tabName == 'notice' ? 'course-classroom-label pane active':'course-classroom-label pane'">
+                            {{$t('cusMgt.cusTab5')}}
+                        </span>
+                        <span @click="selectTab('student')" :class="tabName == 'student' ? 'course-classroom-label pane active':'course-classroom-label pane'">
+                            {{$t('courseManage.classroom.studentList')}}
+                        </span>
+                    </div>
+                    <!-- 评测活动 -->
+                    <Exam v-if="tabName == 'exam'" :gradeParams="gradeParams" :students="students" :classInfo="teaClassList[curClassIndex]" :courseInfo="courseInfo" :teaClassList="teaClassList"></Exam>
+                    <Homework v-else-if="tabName == 'hw'" :classInfo="teaClassList[curClassIndex]"></Homework>
+                    <Survey v-else-if="tabName == 'survey'" :classInfo="teaClassList[curClassIndex]"></Survey>
+                    <Vote v-else-if="tabName == 'vote'" :classInfo="teaClassList[curClassIndex]"></Vote>
+                    <Record v-else-if="tabName == 'record'" :rcdParams="rcdParams"></Record>
+                    <Student v-else-if="tabName == 'student'" :classInfo="teaClassList[curClassIndex]" :stuList="curStuList" @on-del-student="onDelStudent" @on-add-student="onAddStudent" @on-set-irs="onSetIrs"></Student>
+                    <Notice v-else-if="tabName == 'notice'" :classInfo="teaClassList[curClassIndex]" :classList="teaClassList" :courseInfo="courseInfo"></Notice>
+                </div>
+            </Split>
+        </div>
+        <QrcodeModal v-model="showQrStatus" :config="qrConfig">
+            <span style="margin-left: 10px;" slot="switch">
+                <!-- <span>{{ agreeJoin ? '已同意扫码加入' : '禁止扫码加入' }}</span> -->
+                <i-switch v-model="agreeJoin" size="small" @on-change="agreeJoinClass" />
+            </span>
+            <!-- 支持自定义内容 -->
+            <p class="qr-code-text" slot="tips-content">
+                (<span style="font-size:17px">{{$t('cusMgt.qrCodeText')}}</span>
+                {{stuListNo}})
+            </p>
+        </QrcodeModal>
+        <!-- 添加或修改课程 -->
+        <Modal v-model="addCusStatus" footer-hide @on-cancel="initCusInfo" className="ed-name-modal">
+            <div slot="header" class="modal-header">
+                {{addCusInfo.id ? $t('cusMgt.editCus') : $t('cusMgt.addCus')}}
+            </div>
+            <div class="edit-name-content">
+                <Form ref="addCusInfo" :model="addCusInfo" :rules="ruleAddCus">
+                    <FormItem prop="name" :label="$t('cusMgt.cusName')">
+                        <Input v-special-char type="text" v-model="addCusInfo.name" :placeholder="$t('cusMgt.nameHolder')"></Input>
+                    </FormItem>
+                    <FormItem :label="$t('cusMgt.cusCode')" prop="no">
+                        <Input v-special-char type="text" v-model="addCusInfo.no" :placeholder="$t('cusMgt.codeHolder')"></Input>
+                    </FormItem>
+                    <FormItem :label="$t('cusMgt.cusDesc')" prop="desc">
+                        <Input v-special-char v-model="addCusInfo.desc" type="textarea" :maxlength="200" show-word-limit :autosize="{ minRows: 4, maxRows: 6 }" :placeholder="$t('cusMgt.descHolder')" />
+                    </FormItem>
+                </Form>
+                <Button :loading="btnLoading" @click="confirmAddCus" long type="primary" class="confirm-btn">{{ $t('syllabus.confirm') }}</Button>
+            </div>
+        </Modal>
+        <!-- 删除课程名单 -->
+        <Modal v-model="removeListStatus" footer-hide :title="$t('cusMgt.remvListTitle')" className="ed-name-modal">
+            <div slot="header" class="modal-header">
+                {{$t('cusMgt.remvListTitle')}}
+            </div>
+            <div v-if="removeListStatus" class="edit-name-content">
+                <RadioGroup v-model="isPermanent">
+                    <Radio label="remove">
+                        <span>{{$t('cusMgt.justRmList')}}</span>
+                    </Radio>
+                    <Radio label="delete" style="margin-top:20px;color:red">
+                        <span>{{$t('cusMgt.permDelList')}}</span>
+                    </Radio>
+                </RadioGroup>
+                <Button :loading="btnLoading" @click="removeStuList" long type="primary" class="confirm-btn">{{ $t('syllabus.confirm') }}</Button>
+            </div>
+        </Modal>
+        <!-- 修改名单名称 -->
+        <Modal v-model="editClassStatus" footer-hide :title="$t('cusMgt.renameListTitle')" className="ed-name-modal">
+            <div slot="header" class="modal-header">
+                {{$t('cusMgt.renameListTitle')}}
+            </div>
+            <div class="edit-name-content">
+                <p class="edit-name-label">
+                    {{$t('cusMgt.listName')}}
+                </p>
+                <Input v-model="edName" :placeholder="renameBefore" />
+                <Button :loading="btnLoading" @click="confirmRename" long type="primary" class="confirm-btn">{{ $t('syllabus.confirm') }}</Button>
+            </div>
+        </Modal>
+        <!-- 创建名单 -->
+        <Modal v-model="newSlStatus" width="600" footer-hide className="ed-name-modal">
+            <div slot="header" class="modal-header">
+                {{$t('cusMgt.addStuList')}}
+            </div>
+            <div class="edit-name-content">
+                <Form label-position="left" :label-width="70" label-colon @submit.native.prevent>
+                    <FormItem :label="$t('cusMgt.addListType')">
+                        <RadioGroup v-model="type">
+                            <Radio label="create">
+                                <span>
+                                    {{$t('cusMgt.addListType1')}}
+                                </span>
+                            </Radio>
+                            <Radio label="select">
+                                <span>
+                                    {{$t('cusMgt.addListType2')}}
+                                </span>
+                            </Radio>
+                        </RadioGroup>
+                    </FormItem>
+                    <FormItem :label="$t('cusMgt.name')" v-if="type == 'create'">
+                        <Input v-file-name v-model="listName" :placeholder="$t('cusMgt.nameHolder')" style="width: 300px" />
+                    </FormItem>
+                    <FormItem :label="$t('cusMgt.listLabel')" v-else>
+                        <Select v-model="listId" style="width:300px">
+                            <Option v-for="(item,index) in groupList" :value="item.id" :key="index">{{ item.name }}</Option>
+                        </Select>
+                    </FormItem>
+                </Form>
+                <span class="create-list-tips">{{$store.state.userInfo.hasSchool ? $t('cusMgt.createTips1') : $t('cusMgt.createTips2')}}</span>
+                <Button :loading="btnLoading" @click="confirmCreateList" long type="primary" class="confirm-btn">{{ $t('syllabus.confirm') }}</Button>
+            </div>
+        </Modal>
+    </div>
+</template>
+<script>
+import Exam from "./exam/Exam.vue"
+import Homework from "./homework/Homework.vue"
+import Survey from "./survey/Survey.vue"
+import Vote from "./vote/Vote.vue"
+import Record from "./record/Record.vue"
+import Student from "./student/Student.vue"
+import Notice from "./notice/Notice.vue"
+export default {
+    components: {
+        Exam, Homework, Survey, Vote, Record, Student, Notice
+    },
+    data() {
+        // 验证只能是字母和数字
+        const validateCode = (rule, value, callback) => {
+            let zg = /^[0-9a-zA-Z]+$/
+            if (value && !zg.test(value)) {
+                callback(new Error(this.$t('cusMgt.codeErr2')))
+            } else {
+                callback()
+            }
+        }
+        return {
+            stuListNo: '',
+            agreeJoin: true,
+            qrConfig: {
+                title: this.$t('cusMgt.qrCodeLabel'),
+                url: '',//二维码内容 不用转短网址,组件内部处理
+                shareContent: '',//复制链接内容,支持字符串模板,如果包含网址需要处理好短网址
+            },
+            showQrStatus: false,
+            listId: '',
+            type: 'create',
+            listName: '',
+            newSlStatus: false,
+            groupList: [],
+            edName: '',
+            renameBefore: '',
+            isPermanent: 'remove',
+            editClassStatus: false,
+            removeListStatus: false,
+            addCusStatus: false,
+            btnLoading: false,
+            tabName: 'exam',
+            curClassIndex: 0,
+            split1: 0.2,
+            listLoading: false,
+            listType: 'school',
+            courseId: '',
+            cusType: [],
+            courseList: [],
+            filterPeriod: '',
+            courseInfo: {},
+            courseTypeClass: ['course-list-wrap'],
+            addCusInfo: {
+                name: '',
+                no: '',
+                id: '',
+                desc: ''
+            },
+            ruleAddCus: {
+                name: [
+                    { required: true, message: this.$t('cusMgt.nameHolder'), trigger: 'change' }
+                ],
+                no: [
+                    { validator: validateCode, trigger: 'change' }
+                ]
+            },
+        }
+    },
+    computed: {
+        isShowTypeFilter() {
+            let res = this.$jsFn.groupBy(this.courseList, 'scope')
+            return res && res.length > 1
+        },
+        curStuList() {
+            let groupId = this.teaClassList[this.curClassIndex]?.classId || this.teaClassList[this.curClassIndex]?.stulist
+            return this.groupList.find(item => item.id == groupId)
+        },
+        rcdParams() {
+            return {
+                scope: this.listType,
+                courseId: this.courseId,
+                classId: this.teaClassList[this.curClassIndex].classId || this.teaClassList[this.curClassIndex].stulist
+            }
+        },
+        students() {
+            if (this.teaClassList && this.teaClassList[this.curClassIndex]) {
+                return this.teaClassList[this.curClassIndex].allStu
+            }
+            else {
+                return []
+            }
+        },
+        //查询成绩表所需参数(courseId, cId, code)
+        gradeParams() {
+            let data = {
+                courseId: this.courseInfo.id,
+                subjectId: this.courseInfo.subject?.id,
+                cId: this.teaClassList[this.curClassIndex]?.classId || this.teaClassList[this.curClassIndex]?.stulist
+            }
+            return data
+        },
+        courseListShow() {
+            let data = this.courseList.filter(item => item.scope == this.listType)
+            if (data.length) {
+                this.courseId = data[0].id
+            }
+            return data
+        },
+        //课程名单 —— 去重处理后的数据
+        teaClassList() {
+            if (this.courseInfo && this.courseInfo.schedule) {
+                //一个名单被安排到不同教室上课会出现重复数据,这里需要去重
+                let list = []
+                this.courseInfo.schedule.forEach(item => {
+                    if (item.classId || item.stulist) {
+                        // 一个名单被安排到不同教室上课会出现重复数据,这里做去重操作
+                        let has = list.find(listItem => {
+                            return listItem.classId == item.classId && item.stulist == listItem.stulist && listItem.room != item.room
+                        })
+                        if (!has) {
+                            list.push(item)
+                        }
+                    }
+                })
+                return list
+            } else {
+                return []
+            }
+        },
+    },
+    created() {
+        this.initCusType()
+        this.getAllStuList()
+    },
+    activated() {
+        let tn = this.tabName
+        this.tabName = ''
+        setTimeout(() => {
+            this.tabName = tn
+        }, 100)
+    },
+    methods: {
+        onSetIrs(data) {
+            let { students } = data
+            // 当前名单对应的课程安排用于更新UI
+            let schedule = this.courseInfo.schedule.find(item => {
+                return item.stulist == this.teaClassList[this.curClassIndex].stulist && !item.classId //只有自定名单能添加学生,所以classId == ‘’
+            })
+            schedule.allStu = students
+
+            let stulist = this.groupList.find(item => {
+                return item.id == this.teaClassList[this.curClassIndex].stulist
+            })
+            if (stulist) {
+                stulist.members = students
+            }
+        },
+        agreeJoinClass(status) {
+            let stulist = this.groupList.find(item => {
+                return item.id == this.teaClassList[this.curClassIndex].stulist
+            })
+            if (stulist) {
+                stulist.joinLock = status ? 1 : 0
+                let params = stulist
+                this.listLoading = true
+                this.$api.common.upsertGroupInfo(params).then(
+                    res => {
+                        this.$Message.info(`${status ? this.$t('cusMgt.joinMessage1') : this.$t('cusMgt.joinMessage2')}`)
+                        this.teaClassList[this.curClassIndex].joinLock = stulist.joinLock
+                    },
+                    err => {
+                        this.$Message.error(this.$t('cusMgt.updOk'))
+                    }
+                ).finally(() => {
+                    this.listLoading = false
+                })
+            }
+        },
+        async showQrCode(index) {
+            let loginUrl = window.location.host
+            let tName = this.$store.state.userInfo.name
+            let listId = this.teaClassList[index].stulist
+            let listName = this.teaClassList[index].listName
+            let cusName = this.courseInfo.name
+            let stulistInfo = this.groupList.find(item => {
+                return item.id == listId
+            })
+            if (stulistInfo) {
+                //二维码链接
+                this.stuListNo = stulistInfo.no
+                let host = window.location.origin
+                let callbackUrl = `${host}/joinclass?listName=${listName}&tName=${tName}&listNo=${this.stuListNo}&cusName=${cusName}`
+                this.inviteUrl = callbackUrl
+                this.qrConfig.url = callbackUrl
+
+                // 处理分享内容
+                try {
+                    let shortUrl = await this.$api.getShortUrl(encodeURI(this.inviteUrl))
+                    let shareText = `${this.$t('cusMgt.inviteInfo1')}\n\n${this.$t('cusMgt.inviteInfo2')}${cusName}\n${this.$t('cusMgt.inviteInfo3')}${listName}\n${this.$t('cusMgt.inviteInfo4')}${tName}\n\n${this.$t('cusMgt.inviteInfo5')}\n${shortUrl.result || encodeURI(this.inviteUrl)}\n\n${this.$t('cusMgt.inviteInfo6')}\nURL:https://${loginUrl}/login/student\n${this.$t('cusMgt.inviteInfo7')}${this.stuListNo}`
+                    this.qrConfig.shareContent = shareText
+                } catch (e) {
+                    this.qrConfig.shareContent = ''
+                }
+                this.showQrStatus = true
+            } else {
+                this.$Message.error(this.$t('cusMgt.listExp'))
+            }
+        },
+        //确认添加自定义名单
+        confirmCreateList() {
+            if (!this.listName && this.type == 'create') {
+                this.$Message.warning(this.$t('cusMgt.nameWarning'))
+            }
+            else if (!this.listId && this.type == 'select') {
+                this.$Message.warning(this.$t('cusMgt.selectListTips'))
+            }
+            else if (this.type == 'create') {
+                let stuList = {
+                    code: this.$store.state.userInfo.TEAMModelId,
+                    scope: 'private',
+                    name: this.listName,
+                    school: this.$store.state.userInfo.schoolId,
+                    type: 'teach',
+                    leader: this.$store.state.userInfo.TEAMModelId,
+                    members: [],
+                    joinLock: 1, //0:关闭,1:开启
+                }
+                this.btnLoading = true
+                this.saveStuList(stuList, true)
+            }
+            else if (this.type == 'select') {
+                let s = this.groupList.find(item => {
+                    return item.id == this.listId
+                })
+                if (s) {
+                    this.courseInfo.schedule = this.courseInfo.schedule || []
+                    //检查重复添加
+                    let res = this.courseInfo.schedule.find(item => {
+                        return item.stulist == this.listId && item.teacherId == this.$store.state.userInfo.TEAMModelId
+                    })
+                    if (res) {
+                        this.$Message.warning(`${res.listName}${this.$t('cusMgt.alreadyExist')}`)
+                    } else {
+                        this.courseInfo.schedule.push({
+                            classId: '',
+                            notice: '',
+                            listName: s.name,
+                            stulist: this.listId,
+                            teacherId: this.$store.state.userInfo.TEAMModelId,
+                            teacherName: this.$store.state.userInfo.name,
+                            students: s.students,
+                            time: []
+                        })
+                        this.btnLoading = true
+                        this.updCusInfo()
+                        this.listId = ''
+                    }
+                }
+            }
+        },
+        getAllStuList() {
+            let params = {
+                tmdid: this.$store.state.userInfo.TEAMModelId,
+                scope: 'private',
+                type: 'teach'
+            }
+            this.$api.common.getGroupListInfo(params).then(
+                res => {
+                    if (res.groups) {
+                        this.groupList = res.groups
+                    } else {
+                        this.$Message.error(this.$t('cusMgt.listAPIErr'))
+                    }
+                },
+                err => {
+                    this.$Message.error(this.$t('cusMgt.listAPIErr'))
+                }
+            )
+        },
+        // 更新个人课程
+        updCusInfo() {
+            this.courseInfo.code = this.courseInfo.code.replace('Course-', '')
+            this.$api.courseMgmt.saveOrUpdateCourse({
+                course: this.courseInfo,
+                option: 'update',
+                scope: this.courseInfo.scope || 'private'
+            }).then(
+                res => {
+                    this.$Message.success(this.$t('cusMgt.updateOk'))
+                    this.getCusInfo()
+                }
+            ).catch(() => {
+                this.$Message.error(this.$t('cusMgt.updateErr'))
+            }).finally(() => {
+                this.listLoading = false
+                this.newSlStatus = false
+                this.btnLoading = false
+                this.removeListStatus = false
+                this.listName = ''
+            })
+        },
+        // 修改课程名单
+        editListName() {
+            this.edName = this.teaClassList[this.curClassIndex].listName
+            this.renameBefore = this.teaClassList[this.curClassIndex].listName
+            this.editClassStatus = true
+        },
+        confirmRename() {
+            let stulistId = this.teaClassList[this.curClassIndex].stulist
+            let stulist = this.groupList.find(item => {
+                return item.id == this.teaClassList[this.curClassIndex].stulist
+            })
+            if (stulist) {
+                stulist.name = this.edName
+                stulist.type = 'teach'
+                stulist.scope = 'private'
+                let params = stulist
+                this.btnLoading = true
+                this.$api.common.upsertGroupInfo(params).then(
+                    res => {
+                        this.$Message.success(this.$t('cusMgt.updOk'))
+                        this.btnLoading = false
+                        this.editClassStatus = false
+                        this.teaClassList[this.curClassIndex].listName = this.edName
+                        //更新其他课程数据
+                        this.courseListShow.forEach(item => {
+                            if (item.schedule) {
+                                item.schedule.forEach(s => {
+                                    if (s.stulist == stulistId) {
+                                        s.listName = this.edName
+                                    }
+                                })
+                            }
+                        })
+                    },
+                    err => {
+                        this.$Message.error(this.$t('cusMgt.updOk'))
+                    }
+                ).finally(() => {
+                    this.listLoading = false
+                })
+            }
+        },
+        //移除课程名单
+        removeStuList() {
+            this.listLoading = true
+            let listId = this.teaClassList[this.curClassIndex].stulist
+            let index = this.curClassIndex
+            this.curClassIndex = 0
+            this.courseInfo.schedule.splice(index, 1)
+            this.btnLoading = true
+            this.updCusInfo()
+            if (this.isPermanent == 'delete') {
+                //删除名单
+                let params = {
+                    id: listId,
+                    code: this.$store.state.userInfo.TEAMModelId,
+                    scope: "private"
+                }
+                this.$api.common.deleteGroupList(params).then(
+                    res => {
+                        this.$Message.success(this.$t('cusMgt.delOk'))
+                        this.groupList.forEach((item, index) => {
+                            if (item.id == listId) {
+                                this.groupList.splice(index, 1)
+                            }
+                        })
+                    },
+                    err => {
+                        this.$Message.error(this.$t('teachermgmt.rmvErr'))
+                    }
+                ).finally(() => {
+                    this.btnLoading = false
+                    this.removeListStatus = false
+                })
+            }
+        },
+        //删除个人课程
+        delCourse() {
+            this.$Modal.confirm({
+                title: this.$t('cusMgt.delCus'),
+                content: `${this.$t('cusMgt.delContent')}${this.courseInfo.name}?`,
+                onOk: () => {
+                    this.$api.courseMgmt.deleteCourse({
+                        id: this.courseInfo.id,
+                        code: this.courseInfo.code.replace('Course-', ''),
+                        scope: 'private'
+                    }).then(
+                        res => {
+                            if (!res.error) {
+                                //删除原始数据
+                                let originIndex = this.courseList.findIndex(item => {
+                                    return item.id == this.courseInfo.id
+                                })
+                                if (originIndex > -1) {
+                                    this.courseList.splice(originIndex, 1)
+                                }
+
+                                // 删除列表数据
+                                let index = this.curCusIndex
+                                this.curCusIndex = 0
+                                this.$Message.success(this.$t('cusMgt.delOk'))
+                            } else {
+                                this.$Message.error(this.$t('cusMgt.delErr'))
+                            }
+                        },
+                        err => {
+                            this.$Message.error(this.$t('cusMgt.delErr'))
+                        }
+                    )
+                }
+            })
+        },
+        editCus() {
+            this.addCusInfo = this._.cloneDeep(this.courseInfo)
+            this.addCusStatus = true
+        },
+        initCusInfo() {
+            this.addCusInfo = {
+                name: '',
+                no: '',
+                id: '',
+                desc: ''
+            }
+        },
+        // 新增个人课程
+        confirmAddCus() {
+            this.$refs['addCusInfo'].validate((valid) => {
+                if (valid) {
+                    let requestData = {
+                        option: this.addCusInfo.id ? 'update' : 'insert',
+                        scope: 'private',
+                        course: {
+                            name: this.addCusInfo.name,
+                            no: this.addCusInfo.no,
+                            desc: this.addCusInfo.desc,
+                            id: this.addCusInfo.id || this.$tools.guid(),
+                            code: this.$store.state.userInfo.TEAMModelId,
+                            schedule: this.addCusInfo.schedule || [],
+                            scope: 'private',
+                            creatorId: this.$store.state.userInfo.schoolCode
+                        }
+                    }
+                    this.btnLoading = true
+                    this.$api.courseMgmt.saveOrUpdateCourse(requestData).then(
+                        (res) => {
+                            if (!res.error) {
+                                this.$Message.success(this.addCusInfo.id ? this.$t('cusMgt.editOk') : this.$t('cusMgt.addOk'))
+                                //新增课程
+                                if (!this.addCusInfo.id) {
+                                    this.courseList.push(requestData.course)
+                                    this.courseId = requestData.course.id
+                                }
+                                // 修改课程
+                                else {
+                                    let ci = this.courseList.find(item => item.id == this.courseId)
+                                    if (ci) {
+                                        ci.name = this.addCusInfo.name
+                                        ci.no = this.addCusInfo.no
+                                        ci.desc = this.addCusInfo.desc
+                                    }
+                                    this.getCusInfo()
+                                }
+                                this.initCusInfo()
+                                this.addCusStatus = false
+                                this.$refs['addCusInfo'].resetFields()
+                            } else {
+                                this.$Message.error(this.addCusInfo.id ? this.$t('cusMgt.editErr') : this.$t('cusMgt.addErr'))
+                            }
+                        },
+                        (err) => {
+                            this.btnLoading = false
+                        }
+                    ).finally(() => {
+                        this.btnLoading = false
+                    })
+                } else {
+                    this.$Message.error(this.$t('cusMgt.formTips'));
+                    this.btnLoading = false
+                }
+            })
+        },
+        selectTab(tab) {
+            this.tabName = tab
+        },
+        removeOverflow() {
+            if (this.courseTypeClass.length > 1) this.courseTypeClass.pop()
+        },
+        checkOverflow() {
+            const courseBox = document.getElementById('course-list-box')
+            let height = courseBox.clientHeight
+            console.log(height)
+            if (height > 50 && !this.courseTypeClass.includes('course-type-fitcontent')) {
+                this.courseTypeClass.push('course-type-fitcontent')
+            }
+        },
+        // 初始化课程来源
+        initCusType() {
+            this.cusType = [
+                {
+                    label: this.$t('cusMgt.privCus'),
+                    value: 'private'
+                }
+            ]
+            if (this.$store.state.userInfo.hasSchool) {
+                this.cusType.unshift({
+                    label: this.$t('cusMgt.scCus'),
+                    value: 'school'
+                })
+                this.listType = 'school'
+            }
+        },
+        // 获取课程列表
+        getCourseList() {
+            this.listLoading = true
+            let requestData = {
+                'code': this.$store.state.userInfo.TEAMModelId,
+                'schoolId': this.$store.state.userInfo.schoolCode,
+                'scope': 'private'
+            }
+            if (this.filterPeriod) requestData.period = this.filterPeriod
+            this.$api.courseMgmt.findCourse(requestData).then(
+                (res) => {
+                    if (!res.error) {
+                        this.courseList = res.courses
+                        // 如果从首页跳转返回
+                        if (this.routerRecord) {
+                            this.listType = this.routerRecord.scope || this.listType
+                        }
+                        if (!this.courseListShow.length) { //如果没有标准课程,则设置默认为个人课程
+                            this.listType = 'private'
+                        }
+                        this.listLoading = false
+                    }
+                    this.listLoading = false
+                },
+                (err) => {
+                    this.listLoading = false
+                }
+            )
+        },
+        //获取课程详细信息
+        getCusInfo() {
+            this.listLoading = true
+            this.courseInfo = {}
+            let requestData = {
+                'code': this.listType == 'school' ? this.$store.state.userInfo.schoolCode : this.$store.state.userInfo.TEAMModelId,
+                'scope': this.listType,
+                'id': this.courseId
+            }
+            this.$api.courseMgmt.findCusInfo(requestData).then(
+                async (res) => {
+                    if (res.courses && res.courses.length > 0) {
+                        res.courses[0].schedule = res.courses[0].schedule ? res.courses[0].schedule : []
+                        //过滤当前教师的schedule 
+                        res.courses[0].schedule = res.courses[0].schedule.filter(item => {
+                            return item.teacherId == this.$store.state.userInfo.TEAMModelId
+                        })
+                        let resSchedule = res.courses[0].schedule
+                        // 获取自定义名单信息
+                        let ids = resSchedule.map(item => {
+                            return item.stulist
+                        })
+                        ids = ids.filter(item => !!item)
+                        //获取班级名单信息
+                        let classIds = resSchedule.map(item => {
+                            return item.classId
+                        })
+                        classIds = classIds.filter(item => !!item)
+                        try {
+                            //新版名单API支持同时获取各种名单详细信息
+                            let allGroupIds = ids.concat(classIds)
+                            if (allGroupIds.length) {
+                                let allGroups = await this.getListInfo([...allGroupIds])
+                                if (allGroups.groups) {
+                                    resSchedule.forEach(item => {
+                                        //补充行政班信息
+                                        if (item.classId) {
+                                            let classInfo = allGroups.groups.find(classItem => {
+                                                return classItem.id == item.classId
+                                            })
+                                            item.classInfo = {
+                                                id: item.classId,
+                                                name: classInfo ? classInfo.name : this.$t('cusMgt.hasDelClass'),
+                                                year: classInfo ? classInfo.year : 0 //方便计算年级
+                                            }
+                                            item.allStu = classInfo ? classInfo.members : []
+                                            item.fullStu = true
+                                            item.joinLock = classInfo.joinLock
+                                        }
+                                        //补充教学班信息
+                                        if (item.stulist) {
+                                            let listInfo = allGroups.groups.find(listItem => {
+                                                return listItem.id == item.stulist
+                                            })
+                                            item.listName = listInfo ? listInfo.name : this.$t('cusMgt.hasDelClass')
+                                            item.allStu = listInfo ? listInfo.members : []
+                                            item.listSchool = listInfo ? listInfo.school : undefined
+                                            item.fullStu = true
+                                            item.joinLock = listInfo.joinLock
+                                        }
+                                        //统一数据格式
+                                        item.classId = item.classId || undefined
+                                        item.stulist = item.stulist || undefined
+                                    })
+                                } else {
+                                    this.$Message.error(this.$t('cusMgt.getListErr'))
+                                }
+                            }
+                        } catch (e) {
+                            this.$Message.error(this.$t('cusMgt.getListErr'))
+                        }
+                    }
+                    this.courseInfo = res.courses[0]
+                    this.selectClass(0)
+                }
+            ).finally(() => {
+                this.listLoading = false
+            })
+        },
+        onAddStudent(data) {
+            let { addStudents } = data
+            // 当前名单对应的课程安排用于更新UI
+            let schedule = this.courseInfo.schedule.find(item => {
+                return item.stulist == this.teaClassList[this.curClassIndex].stulist && !item.classId //只有自定名单能添加学生
+            })
+            if (schedule) {
+                schedule.allStu.unshift(...addStudents)
+            }
+            //当前名单完整信息 用于更新名单API
+            let stulist = this.groupList.find(item => {
+                return item.id == this.teaClassList[this.curClassIndex].stulist
+            })
+            if (stulist) {
+                stulist.members = stulist.members || []
+                stulist.members.push(...addStudents)
+            }
+        },
+        onDelStudent(data) {
+            let { delIds } = data
+            // 当前名单对应的课程安排用于更新UI
+            let schedule = this.courseInfo.schedule.find(item => {
+                return item.stulist == this.teaClassList[this.curClassIndex].stulist && !item.classId //只有自定名单能添加学生
+            })
+            if (schedule) {
+                for (let i = 0; i < schedule.allStu.length; i++) {
+                    if (delIds.includes(schedule.allStu[i].id)) {
+                        schedule.allStu.splice(i, 1)
+                        i--
+                    }
+                }
+            }
+            //当前名单完整信息 用于更新名单API
+            let stulist = this.groupList.find(item => {
+                return item.id == this.teaClassList[this.curClassIndex].stulist
+            })
+            if (stulist) {
+                for (let i = 0; i < stulist.members.length; i++) {
+                    if (delIds.includes(stulist.members[i].id)) {
+                        stulist.members.splice(i, 1)
+                        i--
+                    }
+                }
+            }
+        },
+        //根据id获取stulist信息
+        getListInfo(ids) {
+            let requestData = {
+                ids,
+                schoolId: this.$store.state.userInfo.schoolCode
+            }
+            return this.$api.common.getGroupListByIds(requestData)
+        },
+        selectClass(index) {
+            this.curClassIndex = index
+            this.agreeJoin = !this.teaClassList.length ? true : (this.teaClassList[index].joinLock ? true : false)
+        },
+        // 保存名单
+        saveStuList(stuList, isUpdCus) {
+            stuList.scope = 'private'
+            let params = stuList
+            this.$api.common.upsertGroupInfo(params).then(
+                res => {
+                    this.$Message.success(this.$t('cusMgt.listSaveOk'))
+                    this.newSlStatus = false
+                    //TODO 添加到当前课程schedule
+                    if (isUpdCus && res.list) {
+                        this.courseInfo.schedule = this.courseInfo.schedule || []
+                        this.courseInfo.schedule.push({
+                            classId: '',
+                            notice: '',
+                            listName: res.list.name,
+                            stulist: res.list.id,
+                            teacherId: this.$store.state.userInfo.TEAMModelId,
+                            teacherName: this.$store.state.userInfo.name,
+                            students: res.list.members,
+                            time: []
+                        })
+                        this.groupList.push(res.list)
+                        this.updCusInfo()
+                    }
+                },
+                err => {
+                    this.$Message.error(this.$t('cusMgt.listSaveErr'))
+                    this.modalLoading = false
+                    setTimeout(() => {
+                        this.modalLoading = true
+                    }, 0)
+                }
+            ).finally(() => {
+                this.listLoading = false
+                this.btnLoading = false
+                this.setIrsStatus = false
+            })
+        },
+    },
+    watch: {
+        courseId: {
+            immediate: true,
+            handler(n, o) {
+                if (n) this.getCusInfo()
+            }
+        },
+        '$store.state.userInfo.schoolCode': {
+            handler(n, o) {
+                this.getCourseList()
+            }
+        },
+        '$store.state.user.curPeriod': {
+            deep: true,
+            immediate: true,
+            handler(n, o) {
+                if (n) {
+                    this.filterPeriod = n.id
+                    this.getCourseList()
+                }
+            }
+        },
+    }
+
+
+}
+</script>
+<style scoped lang="less">
+@import "./MyCourse.less";
+</style>
+<style lang="less">
+.my-course-header {
+    .ivu-radio-group-button .ivu-radio-wrapper:first-child {
+        border: none;
+    }
+    .ivu-radio-group-button .ivu-radio-wrapper:after {
+        display: none;
+    }
+    .ivu-radio-wrapper {
+        margin-right: 5px;
+        border: none;
+        box-shadow: none;
+        .ivu-icon {
+            margin-bottom: 2px;
+        }
+    }
+
+    .ivu-radio-group-item {
+        border-radius: 5px !important;
+        border: 1px solid var(--border-color) !important;
+    }
+
+    .ivu-radio-wrapper-checked {
+        background: #2b85e4 !important;
+        box-shadow: none !important;
+        color: white !important;
+    }
+    .ivu-radio-group-button .ivu-radio-wrapper {
+        height: 28px;
+        line-height: 26px;
+    }
+}
+.ivu-radio-wrapper-checked .cus-action-icon-wrap {
+    display: inline-block;
+}
+</style>

+ 192 - 0
TEAMModelOS/ClientApp/src/view/mycourse/exam/CreatePrivExam.less

@@ -0,0 +1,192 @@
+@main-bgColor: rgb(40,40,40); //主背景颜色
+@borderColor: var(--border-color);
+@primary-textColor: var(--primary-text-color); //文本主颜色
+@second-textColor: var(--second-text-color); //文本副级颜色
+@primary-fontSize: 14px;
+@second-fontSize: 16px;
+
+.create-priv-container {
+    width: 100%;
+    height: 100%;
+    // background: #242328;
+
+    .create-header {
+        width: 100%;
+        height: 45px;
+        border-bottom: 1px solid @borderColor;
+
+        .create-header-title {
+            height: 45px;
+            line-height: 45px;
+            width: 100%;
+            color: @primary-textColor;
+            padding-left: 15px;
+            font-size: 16px;
+            width: fit-content;
+            display: inline-block;
+        }
+
+        .btn-save {
+            float: right;
+            cursor: pointer;
+            margin-top: 6px;
+            margin-right: 10px;
+            color: @second-textColor;
+
+            color: #40A8F0;
+        }
+    }
+
+    .create-body {
+        width: 100%;
+        height: ~"calc(100% - 45px)";
+        display: flex;
+        flex-direction: row;
+    }
+}
+
+.create-priv-container{
+    .create-body {
+        .evaluation-attr-wrap {
+            width: 400px;
+            border-right: 1px solid @borderColor;
+            height: 100%;
+            padding-left: 15px;
+    
+            .wrap-label {
+                // color: white;
+                font-size: @primary-fontSize;
+                height: 40px;
+                line-height: 40px;
+                // border-bottom: 1px solid @borderColor;
+            }
+        }
+    
+        .evaluation-question-wrap {
+            width: ~"calc(100% - 400px)";
+            height: 100%;
+            padding-left: 15px;
+    
+            .wrap-label {
+                color: white;
+                font-size: @primary-fontSize;
+                height: 40px;
+                line-height: 40px;
+                border-bottom: 1px solid @borderColor;
+    
+                p {
+                    display: inline-block;
+                    margin-right: 5px;
+                }
+    
+                .subject-item {
+                    display: inline-block;
+                    color: @second-textColor;
+                    cursor: pointer;
+                    line-height: 39px;
+                    padding:0px 20px;
+                    font-size: 16px;
+                    text-align: center;
+                    position: relative;
+    
+                    .delete-subject-btn {
+                        position: absolute;
+                        display: none;
+                        // color:white;
+                        font-size:18px;
+                        top:10px;
+                        right:0px;
+                    }
+    
+                    &:hover .delete-subject-btn {
+                        display: inline-block;
+                    }
+                }
+    
+                .subject-item-active {
+                    color: @primary-textColor;
+                    border-bottom: 2px solid white;
+                    font-weight: 600;
+                }
+            }
+        }
+        
+        .evaluation-attr-form {
+            margin-right: 10px;
+        }
+    
+        .evaluation-question-main {
+            width: 100%;
+            height: ~"calc(100% - 10px)";
+            padding-top: 15px;
+    
+            .create-type-wrap {
+                color: white;
+                margin-right:40px;
+            }
+        }
+    
+        .add-subject-icon {
+            margin-left:15px;
+            color:white;
+            font-size:16px;
+            margin-top: 8px;
+            cursor: pointer;
+        }
+    
+        .question-main-tabs {
+            height: 100%;
+        }
+    
+        .test-paper-analysis {
+            color: white;
+            cursor: pointer;
+            float: right;
+            margin-right: 50px;
+    
+            &:hover {
+                color: aqua;
+            }
+        }
+    }
+}
+
+.teacher-preview-container .back-to-top:hover {
+    background: rgb(128,128,128);
+}
+
+.teacher-preview-container .back-to-top .ivu-icon {
+    font-size: 26px;
+    color: white;
+}
+.teacher-preview-container .back-to-top {
+    position: fixed;
+    right: ~"calc(50px - 100%)";
+    bottom: 40px;
+    height: 48px;
+    width: 50px;
+    background: #595959;
+    z-index: 99999;
+    cursor: pointer;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+}
+.teacher-preview-container {
+    height: 100%;
+    // background: #404040;
+    border: 1px solid @borderColor;
+    padding: 10px;
+    padding-bottom: 30px;
+    position:relative;
+}
+.no-auth-tag{
+    margin-left: 20px;
+    background: #ff9900;
+    color: white;
+    padding: 2px 8px;
+    border-radius: 2px;
+    font-size: 12px;
+    cursor: pointer;
+}

+ 711 - 0
TEAMModelOS/ClientApp/src/view/mycourse/exam/CreatePrivExam.vue

@@ -0,0 +1,711 @@
+<template>
+    <div class="create-priv-container">
+        <div class="create-header">
+            <p class="create-header-title" @click="consData">{{$t('learnActivity.createEv.createLabel')}}</p>
+            <Button class="btn-save" type="text" :loading="isLoading" ghost icon="ios-albums-outline" @click="saveEvaluation" style="margin-right:30px;">
+                {{$t('learnActivity.createEv.publishEv')}}
+            </Button>
+            <Button class="btn-save" type="text" ghost icon="md-arrow-back" @click="confirmToManage">
+                {{$t('learnActivity.createEv.return')}}
+            </Button>
+        </div>
+        <div class="create-body">
+            <div class="evaluation-attr-wrap">
+                <p class="wrap-label">{{$t('learnActivity.createEv.baseInfo')}}</p>
+                <div style="width:100%; height:calc(100% - 45px);padding-top:15px;" class="ivu-select-nochoose light-iview-form light-el-input">
+                    <vuescroll>
+                        <Form ref="evaForm" :model="evaluationInfo" label-position="top" class="evaluation-attr-form " label-colon :rules="ruleValidate">
+                            <FormItem :label="$t('learnActivity.createEv.evName')" prop="name">
+                                <Input v-special-char v-model="evaluationInfo.name" :placeholder="$t('learnActivity.createEv.evName')"></Input>
+                            </FormItem>
+                            <FormItem prop="source">
+                                <label slot="label" style="width:200px">
+                                    <span>{{$t('learnActivity.createEv.evMode')}}</span>
+                                    <Tooltip :content="$t('tip.examMode')" transfer theme="light" max-width="300">
+                                        <Icon type="ios-information-circle-outline" color="#1cc0f3" style="margin-left:5px" />
+                                    </Tooltip>
+                                </label>
+                                <Select v-model="evaluationInfo.source">
+                                    <!-- 暂时取消创建课中评量 -->
+                                    <Option v-for="(item,index) in $GLOBAL.EV_MODE()" v-show="index != 1" :label="item.label" :value="item.value" :key="index" :disabled="index > 1 && (!$store.state.userInfo.hasSchool || !$store.state.user.schoolProfile.svcStatus.VABAJ6NV)">
+                                        <div>
+                                            <span>
+                                                {{ item.label }}
+                                            </span>
+                                            <span @click.stop="toProduct" v-show="index > 1 && scaleStatusText()" class="no-auth-tag">
+                                                {{ scaleStatusText() }}
+                                            </span>
+                                        </div>
+                                    </Option>
+                                </Select>
+                            </FormItem>
+                            <!-- 使用级联选择 课程--》班级 -->
+                            <FormItem :label="$t('learnActivity.createEv.evTarget')" prop="targets">
+                                <label slot="label">
+                                    <span>{{$t('learnActivity.createEv.evTarget')}}</span>
+                                    <Tooltip :content="$t('tip.targetTips')" transfer theme="light" max-width="300">
+                                        <Icon type="ios-information-circle-outline" color="#1cc0f3" style="margin-left:5px" />
+                                    </Tooltip>
+                                </label>
+                                <el-cascader ref="evtarget" size="small" :options="targetData" :show-all-levels="false" clearable filterable v-model="targets" @visible-change="visibleChange" :props="props" @change="treeChange" style="width:100%;" :popper-class="cascaderClass">
+                                </el-cascader>
+                            </FormItem>
+                            <FormItem :label="$t('learnActivity.createEv.startTime')+'(' + $t('learnActivity.noStartTimeTip') + ')'">
+                                <DatePicker :options="dateOpt" type="datetime" format="yyyy/MM/dd HH:mm" v-model="startTime" split-panels :placeholder="$t('learnActivity.createEv.sTimeHolder')" style="width:100%" @on-change="getDate($event,0)"></DatePicker>
+                            </FormItem>
+                            <FormItem :label="$t('learnActivity.createEv.endTime')" prop="endTime">
+                                <DatePicker :options="dateOpt1" type="datetime" format="yyyy/MM/dd HH:mm" v-model="endTime" split-panels :placeholder="$t('learnActivity.createEv.eTimeHolder')" style="width:100%" @on-change="getDate($event,1)"></DatePicker>
+                            </FormItem>
+                        </Form>
+                    </vuescroll>
+                </div>
+            </div>
+            <div class="evaluation-question-wrap">
+                <div class="evaluation-question-main">
+                    <Tabs v-model="activeTab" type="card" class="question-main-tabs" name="createTest">
+                        <TabPane :label="$t('learnActivity.createEv.papersLabel')" name="manualPaper" v-if="createType == 'manualPaper'" :index="1" tab="createTest">
+                            <ManualPaper @selectPaper="selectPaper" :selectedId="evaluationInfo.paperInfo[0] ? evaluationInfo.paperInfo[0].id : ''"></ManualPaper>
+                        </TabPane>
+                        <TabPane :label="$t('learnActivity.createEv.perviewLabel')" name="preview" :index="2" tab="createTest">
+                            <div class="teacher-preview-container">
+                                <!--返回顶部-->
+                                <BackToTop @on-to-top="handleBackToTop"></BackToTop>
+                                <vuescroll ref="paper-preview" @handle-scroll="checkBackTop">
+                                    <TestPaper v-if="activeTab == 'preview'" :paper="evaluationInfo.paperInfo.length ? evaluationInfo.paperInfo[0] : {item:[]}" isExamPaper hideSheet></TestPaper>
+                                </vuescroll>
+                            </div>
+                        </TabPane>
+                    </Tabs>
+                </div>
+            </div>
+        </div>
+
+    </div>
+</template>
+<script>
+import BlobTool from '@/utils/blobTool.js'
+import TestPaper from '@/view/evaluation/index/TestPaper.vue'
+import ManualPaper from '@/view/learnactivity/ManualPaper.vue'
+export default {
+    components: {
+        TestPaper,
+        ManualPaper
+    },
+    data() {
+        const _this = this
+        return {
+            props: {
+                multiple: true,
+                value: 'id',
+                label: 'name'
+            },
+            courseInfo: {},
+            classList: [],
+            dateOpt: {
+                disabledDate(date) {
+                    return date && date.valueOf() < Date.now() - 86400000
+                }
+            },
+            dateOpt1: {
+                disabledDate(date) {
+                    let d = _this.evaluationInfo.startTime ? _this.evaluationInfo.startTime : Date.now()
+                    return d && d > date.valueOf() + 86400000
+                }
+            },
+            defaultScope: '',
+            createType: 'manualPaper',
+            startTime: '',
+            endTime: '',
+            isLoading: false,
+            ruleValidate: {
+                name: [
+                    { required: true, message: this.$t('learnActivity.createEv.errTips1'), trigger: 'change' }
+                ],
+                'type.id': [
+                    { required: true, message: this.$t('learnActivity.createEv.errTips2'), trigger: 'change' }
+                ],
+                source: [
+                    { required: true, message: this.$t('learnActivity.createEv.errTips3'), trigger: 'change' }
+                ],
+                'type': [
+                    { required: true, message: this.$t('learnActivity.createEv.errTips4'), trigger: 'change' }
+                ],
+                'examType.id': [
+                    { required: true, message: this.$t('learnActivity.createEv.errTips5'), trigger: 'change' }
+                ],
+                targets: [
+                    { required: true, message: this.$t('learnActivity.createEv.errTips6'), type: 'array', trigger: 'change' }
+                ],
+                classes: [
+                    { required: true, message: this.$t('learnActivity.createEv.errTips6'), type: 'array', trigger: 'change' }
+                ],
+                publish: [
+                    { required: true, message: this.$t('learnActivity.createEv.errTips7'), trigger: 'change' }
+                ],
+                startTime: [
+                    { required: true, type: 'number', message: this.$t('learnActivity.createEv.errTips8'), trigger: 'change' }
+                ],
+                endTime: [
+                    { required: true, type: 'number', message: this.$t('learnActivity.createEv.errTips9'), trigger: 'change' }
+                ]
+            },
+            activeTab: 'manualPaper',
+            evaluationInfo: {
+                name: '',
+                targets: [],
+                classes: [],
+                scope: '',
+                type: '',  //测试类别
+                source: '',
+                publish: '0',
+                examType: {}, //测试类型
+                startTime: undefined,
+                endTime: undefined,
+                paperInfo: [],
+                papers: []
+            },
+            mode: '',
+            targets: [],
+            selectBefore: [],
+            stuList: [],
+            showBack: false
+        }
+    },
+    computed: {
+        cascaderClass() {
+            return ''
+            let groupRes = this.$jsFn.groupBy(this.classList, 'type')
+            return groupRes.length > 1 ? 'one-hidden-check-box' : ''
+        },
+        targetData() {
+            let data = []
+            let groupRes = this.$jsFn.groupBy(this.classList, 'type')
+            if (groupRes.length > 1) {
+                groupRes.forEach(item => {
+                    if (item[0].type == 'class') {
+                        data.push({
+                            id: 'class',
+                            name: this.$t('cusMgt.listType1'),
+                            children: item
+                        })
+                    } else {
+                        data.push({
+                            id: 'teach',
+                            name: this.$t('cusMgt.listType2'),
+                            children: item
+                        })
+                    }
+                })
+                return data
+            } else {
+                return this.classList
+            }
+        }
+    },
+    methods: {
+        visibleChange(status) {
+            if (status) {
+                this.selectBefore = this._.cloneDeep(this.targets)
+            }
+        },
+        treeChange(data) {
+            if (this.selectBefore.length) {
+                // 实现教学班和行政班二选一
+                let curType = new Set()
+                this.targets.forEach(item => {
+                    curType.add(item[0])
+                })
+                if (curType.size > 1) {
+                    let oldType = this.selectBefore[0][0]
+                    let newData = this.targets.filter(item => {
+                        return item[0] != oldType
+                    })
+                    this.targets = []
+                    this.$refs.evtarget.panel.clearCheckedNodes()
+                    this.$refs.evtarget.panel.activePath = []
+                    this.selectBefore = newData
+                    this.targets = newData
+                }
+            } else {
+                this.selectBefore = data
+            }
+            this.treeChangeToSetInfo()
+        },
+        treeChangeToSetInfo() {
+            console.log(this.targets)
+            this.evaluationInfo.targets = this.targets
+            this.$refs['evaForm']?.validateField('targets')
+            // 1、设置发布对象
+            //个人课程
+            if (this.evaluationInfo.scope == 'private') {
+                this.evaluationInfo.stuLists = this.evaluationInfo.targets.map(item => {
+                    return item[item.length - 1]
+                })
+                this.evaluationInfo.classes = []
+            }
+            //学校课程教学班
+            else if (this.evaluationInfo.scope == 'school' && this.evaluationInfo.targets.length && this.evaluationInfo.targets[0][0] == 'teach') {
+                this.evaluationInfo.stuLists = this.evaluationInfo.targets.map(item => {
+                    return item[item.length - 1]
+                })
+                this.evaluationInfo.classes = []
+                this.evaluationInfo.grades = []
+            }
+            // 学校课程行政班
+            else {
+                this.evaluationInfo.classes = this.evaluationInfo.targets.map(item => {
+                    return item[item.length - 1]
+                })
+                this.evaluationInfo.stuLists = []
+                //补充年级信息
+                this.evaluationInfo.grades = []
+
+                let sltClass = this.classList.filter(item => this.evaluationInfo.classes.includes(item.id))
+                let grades = Array.from(new Set(sltClass?.map(item => item.year)))
+
+                let gradesInfo = grades.map(item => {
+                    return this.$jsFn.getGradeInfoByYear(item, this.courseInfo.period?.id)
+                })
+                gradesInfo = gradesInfo.filter(item => !!item)
+                gradesInfo.forEach(item => {
+                    item.id = item.id.toString()
+                })
+                this.evaluationInfo.grades = gradesInfo
+            }
+
+            //2、设置评测“学科”
+            this.evaluationInfo.subjects = []
+            // 个人课程用课程名称代替学科
+            if (this.evaluationInfo.scope == 'private') {
+                if (data.targets.length && data.course) {
+                    this.evaluationInfo.subjects.push({
+                        id: this.courseInfo.id,
+                        name: this.courseInfo.name
+                    })
+                }
+                this.evaluationInfo.period = undefined
+            }
+            //校本课程用课程关联的学生,并补充学段信息
+            else {
+                this.evaluationInfo.subjects.push({
+                    id: this.courseInfo?.subject?.id,
+                    name: this.courseInfo?.subject?.name
+                })
+                this.evaluationInfo.period = {
+                    id: this.courseInfo?.period?.id,
+                    name: this.courseInfo?.period?.name
+                }
+            }
+        },
+        scaleStatusText() {
+            if (!this.$store.state.userInfo.hasSchool) {
+                return this.$t('learnActivity.createEv.noSchool')
+            }
+            if (this.$store.state.user.schoolProfile.svcStatus.VABAJ6NV) {
+                return ''
+            } else {
+                return this.$t('learnActivity.createEv.noAuth')
+            }
+        },
+        toProduct() {
+            if (this.$store.state.userInfo.hasSchool) {
+                this.$router.push({
+                    name: 'product'
+                })
+            }
+        },
+        getScope(data) {
+            this.evaluationInfo.scope = data
+        },
+        /**vuescroll回到顶部 */
+        handleBackToTop() {
+            this.$refs['paper-preview'].scrollTo(
+                {
+                    y: '0'
+                },
+                300
+            )
+        },
+        /**
+         * 判断是否显示回到顶部按钮
+         * @param vertical
+         * @param horizontal
+         * @param nativeEvent
+         */
+        checkBackTop(vertical, horizontal, nativeEvent) {
+            if (vertical.scrollTop > 100) {
+                this.showBack = true
+            } else {
+                this.showBack = false
+            }
+        },
+        /*
+         * 返回上一级
+         */
+        confirmToManage() {
+            this.$router.go(-1)
+        },
+        /**
+         * 处理挑选试卷事件
+         * @param data
+         */
+        selectPaper(data) {
+            let simplePaper = data
+            this.$Modal.confirm({
+                title: this.$t('learnActivity.createEv.stPaperTitle'),
+                content: `${this.$t('learnActivity.createEv.stPaperContent')}${data.name}?`,
+                onOk: async () => {
+                    let fullPaper = await this.$evTools.getFullPaper(simplePaper)
+                    this.comfirmSelectPaper(simplePaper, fullPaper)
+                }
+            })
+        },
+        comfirmSelectPaper(simplePaper, fullPaper) {
+            if (!this.evaluationInfo.name) {
+                this.evaluationInfo.name = simplePaper.name
+            }
+            fullPaper.examId = ''
+            fullPaper.examScope = this.mode
+            fullPaper.examCode = this.$store.state.userInfo.schoolCode
+            this.evaluationInfo.paperInfo[0] = fullPaper
+            this.evaluationInfo.papers[0] = simplePaper
+            this.goToPreview()
+        },
+
+        /**
+         * 将日期控件时间格式转成时间戳
+         * @param value
+         * @param date
+         */
+        getDate(value, flag) {
+            if (value.indexOf('00:00') > 0) {
+                value = value.replace('00:00', '23:59:59')
+            }
+            if (flag == 0) {
+                this.startTime = value
+                this.evaluationInfo.startTime = new Date(value).getTime()
+                if (this.evaluationInfo.startTime >= this.evaluationInfo.endTime) {
+                    this.endTime = undefined
+                    this.evaluationInfo.endTime = undefined
+                }
+            } else if (flag == 1) {
+                this.endTime = value
+                this.evaluationInfo.endTime = new Date(value).getTime()
+            }
+        },
+        goToPreview() {
+            this.activeTab = 'preview'
+        },
+        consData() {
+            console.log('evaluationInfo', this.evaluationInfo)
+        },
+        async saveEvaluation() {
+            //表单验证
+            let flag = true
+            this.$refs['evaForm'].validate((valid) => {
+                if (!valid) {
+                    this.$Message.warning(this.$t('learnActivity.createEv.formWarning'))
+                    flag = false
+                }
+            })
+            if (!flag) return
+            //检查试卷是否添加
+            if (this.evaluationInfo.papers.length < 1) {
+                this.$Message.warning({
+                    content: this.$t('learnActivity.createEv.paperWarning'),
+                    duration: 2
+                })
+                flag = false
+            }
+            if (!flag) return
+            //通过验证,保存评测
+            let apiPapers = this.getPaperInfo()
+            //线下阅卷需要处理答题卡数据 
+            let sheetIds = []
+            if (this.evaluationInfo.source == '2') {
+                sheetIds = apiPapers.map(item => {
+                    return {
+                        sheetId: item.sheet,
+                        paperId: item.id,
+                        code: item.code.replace('Paper-', ''),
+                        scope: item.scope
+                    }
+                })
+                console.log(JSON.stringify(sheetIds))
+                sheetIds = sheetIds.filter(item => {
+                    return item.sheetId
+                })
+            }
+            this.isLoading = true
+            let requestData = {
+                pk: 'Exam',
+                id: this.evaluationInfo.id || null,
+                code: this.$store.state.userInfo.TEAMModelId,
+                school: this.$store.state.userInfo.schoolCode,
+                name: this.evaluationInfo.name,
+                creatorId: this.$store.state.userInfo.TEAMModelId,
+                type: this.evaluationInfo.type,
+                period: this.evaluationInfo.period,
+                subjects: this.evaluationInfo.subjects,
+                grades: this.evaluationInfo.grades,
+                papers: apiPapers,
+                examType: this.evaluationInfo.examType,
+                year: new Date().getFullYear(),
+                source: this.evaluationInfo.source,
+                classes: this.evaluationInfo.classes,
+                stuLists: this.evaluationInfo.stuLists,
+                targets: this.evaluationInfo.targets,
+                startTime: this.evaluationInfo.startTime || -1, //立即发布由后端获取时间
+                endTime: this.evaluationInfo.endTime,
+                scope: this.evaluationInfo.scope,
+                createDate: Math.round(new Date()),
+                owner: 'teacher' //后面新增字段
+            }
+
+            this.$api.learnActivity.SaveExamInfo(requestData).then(
+                res => {
+                    if (res.error) {
+                        // this.$Message.error('API ERROR!')
+                        this.$Message.error(this.$t('learnActivity.mark.saveErr'))
+                        this.isLoading = false
+                        return
+                    }
+                    //发布成功需要备份试卷数据
+                    let examId = res.exam.id
+                    let privateSas = {}
+                    if (this.$store.state.user.userProfile) {
+                        let blobInfo = this.$store.state.user.userProfile
+                        privateSas.sas = '?' + blobInfo.blob_sas
+                        privateSas.url = blobInfo.blob_uri.slice(0, blobInfo.blob_uri.lastIndexOf(this.$store.state.userInfo.TEAMModelId) - 1)
+                        privateSas.name = this.$store.state.userInfo.TEAMModelId
+                    }
+
+                    let schoolSas = {}
+                    if (this.$store.state.userInfo.hasSchool) {
+                        schoolSas = {
+                            sas: '?' + this.$store.state.user.schoolProfile.blob_sas,
+                            url: this.$store.state.user.schoolProfile.blob_uri.slice(0, this.$store.state.user.schoolProfile.blob_uri.lastIndexOf(this.$store.state.userInfo.schoolCode) - 1),
+                            name: this.$store.state.userInfo.schoolCode
+                        }
+                    }
+                    //校本课程复制到学校容器 个人课程复制到个人容器
+                    let targetBlob = undefined
+                    if (this.evaluationInfo.scope == 'school') {
+                        targetBlob = new BlobTool(schoolSas.url, schoolSas.name, schoolSas.sas, 'school')
+                    } else {
+                        targetBlob = new BlobTool(privateSas.url, privateSas.name, privateSas.sas, 'private')
+                    }
+                    let privateBlob = new BlobTool(privateSas.url, privateSas.name, privateSas.sas, 'private')
+                    let schoolBlob = undefined
+                    let targetFolder = 'exam/' + examId + '/paper/'
+                    let count = 0
+                    requestData.papers.forEach(async (item, index) => {
+                        if (item.blob.indexOf('/exam/') == 0) {
+                            if (++count == (requestData.papers.length)) {
+                                this.$Message.success(this.$t('learnActivity.createEv.publishOk'))
+                                this.isLoading = false
+                                // let route = this.mode + 'Evaluation'
+                                this.$router.go(-1)
+                            }
+                            return
+                        }
+                        // 挑选的是校本试卷
+                        if (item.scope == 'school') {
+                            if (!schoolBlob) schoolBlob = new BlobTool(schoolSas.url, schoolSas.name, schoolSas.sas, 'school')
+                            targetBlob.copyFolder(targetFolder + this.evaluationInfo.subjects[index].id + '/', item.blob.substring(1), schoolBlob).then().finally(
+                                () => {
+                                    if (++count == requestData.papers.length) {
+                                        this.$Message.success(this.$t('learnActivity.createEv.publishOk'))
+                                        this.isLoading = false
+                                        // let route = this.mode + 'Evaluation'
+                                        this.$router.go(-1)
+                                    }
+                                }
+                            )
+                        }
+                        //挑选的是个人试卷
+                        else if (item.scope == 'private') {
+                            if (!privateBlob) privateBlob = new BlobTool(privateSas.url, privateSas.name, privateSas.sas, 'private')
+                            targetBlob.copyFolder(targetFolder + this.evaluationInfo.subjects[index].id + '/', item.blob.substring(1), privateBlob).then().finally(
+                                () => {
+                                    if (++count == requestData.papers.length) {
+                                        this.$Message.success(this.$t('learnActivity.createEv.publishOk'))
+                                        this.isLoading = false
+                                        // let route = this.mode + 'Evaluation'
+                                        this.$router.go(-1)
+                                    }
+                                }
+                            )
+                        }
+                    })
+                    //处理答题卡 
+                    if (sheetIds.length) {
+                        sheetIds.forEach(async sheetItem => {
+                            try {
+                                //这里校本评测只能选校本试卷,所以参数直接写学校
+                                let params = {
+                                    id: sheetItem.sheetId,
+                                    code: sheetItem.code,
+                                    scope: sheetItem.scope
+                                }
+                                //查询答题卡数据
+                                let sheetData = await this.$api.evaluation.findSheet(params)
+                                if (sheetData.config) {
+                                    sheetData.config.id = null //清空答题卡重新创建
+                                    sheetData.config.code = sheetData.config.code.replace('SheetConfig-', '')
+                                    // 检查答题卡scope
+                                    if (sheetData.config.scope != this.evaluationInfo.scope) {
+                                        sheetData.config.code = this.evaluationInfo.scope == 'school' ? this.$store.state.userInfo.schoolCode : this.$store.state.userInfo.TEAMModelId //清空答题卡重新创建
+                                        sheetData.config.school = this.$store.state.userInfo.schoolCode
+                                        sheetData.config.scope = this.evaluationInfo.scope
+                                    }
+                                    //更新答题卡数据
+                                    let updParams = {
+                                        sheet: sheetData.config,
+                                        examId: examId,
+                                        owner: 'teacher',
+                                        paperId: sheetItem.paperId
+                                    }
+                                    let updRes = await this.$api.evaluation.upsertSheet(updParams)
+                                }
+                            } catch (e) {
+                                this.$Message.error(this.$t('learnActivity.createEv.sheetErr'))
+                            }
+                        })
+                    }
+                },
+                err => {
+                    this.isLoading = false
+                }
+            )
+        },
+        //拼接API需要的paper数据
+        getPaperInfo() {
+            let data = this.evaluationInfo.papers
+            let rule = this.evaluationInfo.paperInfo
+            if (data) {
+                let paperDto = []
+                for (let i = 0; i < data.length; i++) {
+                    if (data[i].blob.indexOf('/exam/') == 0) {
+                        paperDto.push(data[i])
+                    } else {
+                        let paper = {}
+                        paper.id = data[i].id
+                        paper.code = data[i].code
+                        paper.name = data[i].name
+                        paper.blob = data[i].blob
+                        paper.scope = data[i].scope
+                        paper.sheet = data[i].sheet
+                        paper.multipleRule = rule[i].multipleRule
+                        paper.answers = []
+                        paper.point = []
+                        paper.knowledge = []
+                        paper.field = []
+                        paper.type = []//后面新增字段, 保存每个题目类型
+                        for (let k = 0; k < rule[i].slides.length; k++) {
+                            if (rule[i].slides[k].type !== 'compose') {
+                                paper.answers.push(rule[i].slides[k].scoring.ans ? rule[i].slides[k].scoring.ans : [])
+                                paper.point.push(rule[i].slides[k].scoring.score)
+                                paper.knowledge.push(rule[i].slides[k].scoring.knowledge ? rule[i].slides[k].scoring.knowledge : [])
+                                paper.field.push(rule[i].slides[k].scoring.field ? rule[i].slides[k].scoring.field : [])
+                                paper.type.push(rule[i].slides[k].type) //后面新增字段, 保存每个题目类型
+                            }
+                        }
+                        paper.subjectName = this.evaluationInfo.subjects && this.evaluationInfo.subjects.length ? this.evaluationInfo.subjects[0].name : ''
+                        paper.subjectId = this.evaluationInfo.subjects && this.evaluationInfo.subjects.length ? this.evaluationInfo.subjects[0].id : ''
+                        paperDto.push(paper)
+                    }
+                }
+                return paperDto
+            } else {
+                return []
+            }
+        }
+    },
+    created() {
+        // 处理默认时间
+        this.endTime = new Date(new Date(new Date().toLocaleDateString()).getTime() + 2 * 24 * 60 * 60 * 1000 - 1)
+        this.evaluationInfo.endTime = new Date(new Date().toLocaleDateString()).getTime() + 2 * 24 * 60 * 60 * 1000 - 1
+
+        this.mode = 'private'
+
+        let courseData = this.$route.params.courseData
+        if (!courseData) {
+            this.$router.go(-1)
+            return
+        }
+
+        let { course, classList, defaultClass, classType } = courseData
+        if (!course || !classList) {
+            this.$router.go(-1)
+            return
+        }
+        console.log(courseData)
+        this.courseInfo = course
+        this.evaluationInfo.scope = this.courseInfo.scope
+        this.classList = classList.map(item => {
+            return {
+                id: item.classId || item.stulist,
+                year: item.year,
+                name: item.classInfo?.name || item.listName,
+                type: item.classId ? 'class' : 'teach'
+            }
+        })
+        //默认选中班级
+        if (defaultClass && classType) {
+            let groupRes = this.$jsFn.groupBy(this.classList, 'type')
+            let classInfo = this.classList.find(item => item.id == defaultClass)
+            if (groupRes.length > 1) {
+                this.targets = [[classInfo.type, classInfo.id]]
+            } else {
+                this.targets = [defaultClass]
+            }
+            this.evaluationInfo.targets = this.targets
+            this.treeChangeToSetInfo()
+        }
+    },
+    beforeRouteLeave(to, from, next) {
+        if (to.name == 'answerSheet') {
+            from.meta.isKeep = true
+        } else {
+            from.meta.isKeep = false
+        }
+        next()
+    }
+}
+</script>
+<style scoped lang="less">
+@import "./CreatePrivExam.less";
+</style>
+<style lang="less">
+.one-hidden-check-box {
+    .el-cascader-menu:nth-child(1) .el-checkbox {
+        display: none;
+    }
+}
+.teacher-preview-container .paper-main-wrap {
+    padding: 0px;
+    margin-top: 0px;
+}
+.evaluation-attr-wrap .ivu-form .ivu-form-item-label {
+    color: var(--second-text-color);
+}
+
+.evaluation-question-main .ivu-tabs-tab-active {
+    font-weight: 600;
+}
+
+.evaluation-question-main .ivu-tabs .ivu-tabs-content-animated {
+    height: 100%;
+    margin-bottom: 10px;
+}
+
+.evaluation-question-main .ivu-tabs-bar {
+    border-color: #404040;
+    margin-bottom: 0px;
+    border-bottom: none;
+}
+.evaluation-question-main .ivu-tabs.ivu-tabs-card .ivu-tabs-bar .ivu-tabs-tab {
+    margin-right: 2px;
+}
+</style> 

+ 69 - 0
TEAMModelOS/ClientApp/src/view/mycourse/exam/Exam.less

@@ -0,0 +1,69 @@
+@first-bgColor: #141414;
+@second-bgColor: #1b1b1b;
+@third-bgColor: #222222;
+@primary-textColor: var(--primary-text-color); //文本主颜色
+@second-textColor: var(--second-text-color); //文本副级颜色
+@primary-fontSize: 14px;
+@borderColor: var(--border-color);
+@second-fontSize: 16px;
+
+.course-exam-container{
+    width: 100%;
+    height: ~"calc(100% - 46px)";
+}
+.exam-item:hover .common-item-icon{
+    display: inline-block;
+}
+.common-item-icon{
+    display: none;
+    font-size: 16px;
+    color: #2d8cf0;
+    vertical-align: baseline;
+    margin-left: 10px;
+    position: absolute;
+}
+.heart-active{
+    display: inline-block;
+    color: #ed4014;
+}
+.exam-action-wrap{
+    position: absolute;
+    right: 15px;
+    top: 10px;
+}
+.action-item{
+    margin-right:15px;
+    vertical-align: middle;
+    cursor: pointer;
+    user-select: none;
+    &:hover{
+        color: #2d8cf0;
+    }
+}
+.exam-item{
+    cursor: pointer;
+    padding: 20px 30px 20px 15px;
+    border-bottom: 1px solid #f0f0f0;
+    display: flex;
+    align-items: center;
+    &:hover{
+        background: var(--hover-text-color);
+    }
+    &:hover .item-action-wrap{
+        display: inline-block;
+    }
+}
+.list-icon{
+    color: var(--normal-icon-color);
+    font-size: 35px;
+    width: 90px;
+}
+.exam-name{
+    color: @primary-textColor;
+}
+.exam-detail-wrap{
+    margin-top: 10px;
+}
+.item-action-wrap{
+    height: 20px;
+}

+ 390 - 0
TEAMModelOS/ClientApp/src/view/mycourse/exam/Exam.vue

@@ -0,0 +1,390 @@
+<template>
+    <div class="course-exam-container">
+        <div class="exam-action-wrap">
+            <span class="action-item" @click="isShowGrade = !isShowGrade">
+                <Icon :type="isShowGrade ? 'md-list' : 'md-grid'" />
+                {{isShowGrade ? $t('cusMgt.cusTab6') : $t('cusMgt.cusTab7')}}
+            </span>
+            <span class="action-item" @click="toCreate">
+                <Icon type="md-add" />
+                {{$t('cusMgt.cusTab8')}}
+            </span>
+        </div>
+        <Loading v-show="isLoading"></Loading>
+        <Grade v-show="isShowGrade" :paramsInfo="gradeParams" :student="students" class="exam-grade-view"></Grade>
+        <vuescroll>
+            <div class="exam-item" v-for="(item,index) in examList" :key="index" @click="toExamDetail(index)">
+                <Icon custom="iconfont icon-test" class="list-icon" />
+                <div class="exam-info-wrap">
+                    <h3 class="exam-name">
+                        {{item.name}}
+                    </h3>
+                    <p class="exam-detail-wrap">
+                        <!-- 学校|个人 -->
+                        <Tag color="blue">
+                            {{ item.owner == 'school' ? $t('cusMgt.school') : $t('cusMgt.private') }}
+                        </Tag>
+                        <!-- 评测模式 -->
+                        <Tag color="geekblue">
+                            {{getModeLabel(item.source)}}
+                        </Tag>
+                        <!-- 活动评分状态 -->
+                        <Tag color="orange" v-show="item.scoreText">
+                            {{ item.scoreText }}
+                        </Tag>
+                        <!-- 活动进度状态 -->
+                        <Tag :color="item.progress == 'going' ? 'success' : 'warning'">
+                            {{ item.progText }}
+                        </Tag>
+                        <Tag>
+                            <Icon type="md-time" size="16" />
+                            {{$jsFn.timeFormat(item.startTime)}}
+                        </Tag>
+                    </p>
+                </div>
+                <div class="item-action-wrap">
+                    <span>
+                        <!-- 修改评测名称 -->
+                        <Icon v-show="item.owner === 'teacher'" type="md-create" style="right:80px" class="common-item-icon" @click.stop="editEvName(index)" :title="$t('learnActivity.mgtScEv.edName')" />
+                        <!-- 删除记录 -->
+                        <Icon v-show="item.owner === 'teacher'" type="md-trash" style="right:50px" class="common-item-icon" @click.stop="delEv(item)" :title="$t('cusMgt.delRcd')" />
+                        <!-- 收藏 -->
+                        <Icon :type="isFavorite(item.id) ? 'md-heart':'md-heart-outline'" style="right:20px" :class="['common-item-icon',isFavorite(item.id) ? 'heart-active':'']" @click.stop="toggleFavorite(item)" :title="isFavorite(item.id) ? $t('cusMgt.unfvt') : $t('cusMgt.fvt')" />
+                    </span>
+                </div>
+            </div>
+            <EmptyData v-show="examList.length == 0" :top="150"></EmptyData>
+        </vuescroll>
+        <!-- 修改评测名称 -->
+        <Modal v-model="editNameStatus" className="ed-name-modal" footer-hide>
+            <div slot="header" class="modal-header">
+                {{$t('learnActivity.mgtScEv.edName')}}
+            </div>
+            <div class="edit-name-content">
+                <p class="edit-name-label">
+                    {{$t('cusMgt.listName')}}
+                </p>
+                <Input v-model="editName" :placeholder="$t('learnActivity.mgtScEv.edNameHolder')" />
+                <Button :loading="btnLoading" @click="confirmEditName" long type="primary" class="confirm-btn">{{ $t('syllabus.confirm') }}</Button>
+            </div>
+        </Modal>
+    </div>
+</template>
+<script>
+import Grade from "./Grade.vue"
+export default {
+    components: {
+        Grade
+    },
+    props: {
+        students: {
+            type: Array,
+            default: () => {
+                return []
+            }
+        },
+        gradeParams: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        },
+        classInfo: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        },
+        courseInfo: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        },
+        teaClassList: {
+            type: Array,
+            default: () => {
+                return []
+            }
+        },
+    },
+    data() {
+        return {
+            isShowGrade: false,
+            isLoading: false,
+            btnLoading: false,
+            examList: [],
+            fIds: [],
+            editName: '',
+            editNameStatus: false,
+            edNameIndex: 0
+        }
+    },
+    created() {
+        this.findFavorite()
+    },
+    methods: {
+        async toCreate() {
+            let scope = this.courseInfo.scope
+            let isFull = await this.$tools.isBlobContainerFull(scope)
+            if (isFull) {
+                if (scope == 'school') {
+                    this.$Message.warning(this.$t('cusMgt.fullTips1'))
+                } else {
+                    this.$Message.warning(this.$t('cusMgt.fullTips2'))
+                }
+            } else {
+                this.$router.push({
+                    name: 'createPrivExam',
+                    params: {
+                        courseData: {
+                            course: this.courseInfo,
+                            classList: this.teaClassList,
+                            defaultClass: this.classInfo.classId || this.classInfo.stulist,
+                            classType: this.classInfo.classId ? 'class' : 'teach'
+                        }
+                    }
+                })
+            }
+        },
+        //收藏 取消
+        toggleFavorite(data) {
+            if (data && data.id) {
+                if (this.isFavorite(data.id)) {
+                    this.delCollection(data) //取消收藏
+                } else {
+                    this.collection(data) //收藏
+                }
+            }
+        },
+        collection(data) {
+            let params = {
+                favorite: {
+                    "name": data.name,
+                    "id": data.id,
+                    "code": this.$store.state.userInfo.TEAMModelId,
+                    "fromId": data.id,
+                    "fromCode": data.code,
+                    "scope": data.scope,
+                    "owner": data.scope == 'school' ? this.$store.state.userInfo.schoolCode : this.$store.state.userInfo.TEAMModelId,
+                    "type": data.pk
+                }
+            }
+            this.$api.courseMgmt.FavoriteUpsert(params).then(
+                res => {
+                    this.$Message.success(this.$t('cusMgt.fvtOk'))
+                    this.fIds.push(data.id)
+                },
+                err => {
+                    this.$Message.error(this.$t('cusMgt.fvtOk'))
+                }
+            )
+        },
+        delCollection(data) {
+            let params = {
+                "id": data.id,
+                "code": this.$store.state.userInfo.TEAMModelId,
+            }
+            this.$api.courseMgmt.FavoriteDelete(params).then(
+                res => {
+                    this.$Message.success(this.$t('cusMgt.unfvtOk'))
+                    let index = this.fIds.findIndex(item => item == data.id)
+                    if (index > -1) this.fIds.splice(index, 1)
+                },
+                err => {
+                    this.$Message.error(this.$t('cusMgt.unfvtErr'))
+                }
+            )
+        },
+        //删除评测
+        delEv(ev) {
+            this.$Modal.confirm({
+                title: this.$t('cusMgt.delAcTitle'),
+                content: this.$t('cusMgt.delAcContent'),
+                onOk: () => {
+                    this.isLoading = true
+                    let params = {
+                        id: ev.id,
+                        code: ev.code.replace('Exam-', ''),
+                        scope: ev.scope
+                    }
+                    this.$api.learnActivity.DeleteExamInfo(params).then(
+                        res => {
+                            if (!res.error) {
+                                let index = this.examList.findIndex(item => item.id === ev.id)
+                                if (index > -1) {
+                                    this.examList.splice(index, 1)
+                                }
+                                this.$Message.success(this.$t('learnActivity.mgtScEv.deleteOk'))
+                            } else {
+                                this.$Message.error(this.$t('learnActivity.mgtScEv.deleteErr'))
+                            }
+                        },
+                        err => {
+                            this.$Message.error(this.$t('learnActivity.mgtScEv.deleteErr'))
+                        }
+                    ).finally(() => {
+                        setTimeout(() => {
+                            this.isLoading = false
+                        }, 500)
+                    })
+                }
+            })
+        },
+        editEvName(index) {
+            this.editNameStatus = true
+            this.edNameIndex = index
+            this.editName = this.examList[index]?.name
+        },
+        // 修改活动名称
+        confirmEditName() {
+            if (this.editName) {
+                this.btnLoading = true
+                this.$api.learnActivity.updateAcInfo({
+                    id: this.examList[this.edNameIndex].id,
+                    code: this.examList[this.edNameIndex].code,
+                    name: this.editName
+                }).then(
+                    res => {
+                        if (res.code == 200) {
+                            this.examList[this.edNameIndex].name = this.editName
+                            this.$Message.success(this.$t('learnActivity.mgtScEv.updOk'))
+                        } else {
+                            this.$Message.error(this.$t('learnActivity.mgtScEv.updErr'))
+                        }
+                    },
+                    err => {
+                        this.$Message.error(this.$t('learnActivity.mgtScEv.updErr'))
+                    }
+                ).finally(() => {
+                    this.editNameStatus = false
+                    this.btnLoading = false
+                })
+            } else {
+                this.$Message.warning(this.$t('learnActivity.mgtScEv.edNameHolder'))
+            }
+        },
+        /**获取mode对应的label */
+        getModeLabel(code) {
+            for (let item of this.$GLOBAL.EV_MODE()) {
+                if (item.value == code) {
+                    return item.label
+                }
+            }
+        },
+        isFavorite(id) {
+            return this.fIds.includes(id)
+        },
+        toExamDetail(index) {
+            this.$router.push({
+                path: '/home/evDetail',
+                query: {
+                    examId: this.examList[index].id,
+                    code: this.examList[index].code
+                }
+            })
+        },
+        //获取活动列表
+        findTchAc(classId) {
+            this.examList = []
+            this.isShowGrade = false
+            this.isLoading = true
+            let requestData = {
+                school: this.$store.state.userInfo.schoolCode,
+                classes: [classId],
+                userid: this.$store.state.userInfo.TEAMModelId,
+                pk: 'Exam'
+            }
+            this.$api.courseMgmt.findTchAc(requestData).then(
+                res => {
+                    if (!res.error) {
+                        res.datas.forEach(item => {
+                            let statusInfo = this.getEvStatusInfo(item.progress, item.sStatus)
+                            item.progText = statusInfo.progText
+                            item.progColor = statusInfo.progColor
+                            item.scoreText = statusInfo.scoreText
+                            item.scoreColor = statusInfo.scoreColor
+                        })
+                        this.examList = res.datas
+                    }
+                },
+                err => {
+                    // this.$Message.error('API error')
+                }
+            ).finally(() => {
+                this.isLoading = false
+            })
+        },
+        getEvStatusInfo(progress, isScore) {
+            let info = {}
+            if (progress == 'pending') {
+                info.progText = this.$t('learnActivity.mgtScEv.pending')
+                info.progColor = '#2d8cf0'
+            } else if (progress == 'going') {
+                info.progText = this.$t('learnActivity.mgtScEv.going')
+                info.progColor = '#19be6b'
+            } else if (progress == 'finish') {
+                info.progText = this.$t('learnActivity.mgtScEv.finish')
+                info.progColor = '#515a6e'
+            }
+            if (isScore === 0) {
+                info.scoreText = this.$t('learnActivity.mgtScEv.scoreStatus')
+                info.scoreColor = '#ff9900'
+            } else if (isScore === 1) {
+                info.scoreText = this.$t('learnActivity.mgtScEv.scoreStatus1')
+                info.scoreColor = '#515a6e'
+            }
+            return info
+        },
+        //查询收藏列表
+        findFavorite() {
+            this.$api.courseMgmt.FavoriteFind({
+                "code": this.$store.state.userInfo.TEAMModelId
+            }).then(
+                res => {
+                    if (!res.error && res.favorites) {
+                        this.fIds = res.favorites.map(item => item.id)
+                    }
+                },
+                err => {
+
+                }
+            )
+        },
+    },
+    watch: {
+        classInfo: {
+            deep: true,
+            immediate: true,
+            handler(n, o) {
+                console.log(n)
+                if (n && (n.classId || n.stulist)) {
+                    this.findTchAc(n.classId || n.stulist)
+                }
+            }
+        }
+    }
+}
+</script>
+<style lang="less" scoped>
+@import "./Exam.less";
+</style>
+<style lang="less">
+.exam-detail-wrap .ivu-tag {
+    margin-right: 8px;
+}
+.course-exam-container .ivu-list-item:hover {
+    background: var(--active-item-start);
+}
+.exam-grade-view {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    background: white;
+    z-index: 99;
+    margin-left: 1px;
+}
+.exam-grade-view .ivu-table-wrapper-with-border {
+    border: none;
+}
+</style>

+ 163 - 0
TEAMModelOS/ClientApp/src/view/mycourse/exam/Grade.vue

@@ -0,0 +1,163 @@
+<template>
+    <div id="grade-container" class="grade-container">
+        <Table row-key="id" :columns="columns" :data="tableData" border stripe :max-height="tableHieght">
+            <template slot-scope="{ row }" slot="name">
+                <p style="margin-top:5px">
+                    <strong>{{ row.name }}</strong>
+                </p>
+                <div style="margin-bottom:5px;margin-top:2px">
+                    <Tag color="blue">
+                        {{ row.owner == 'school' ? $t('learnActivity.mgtScEv.listLabel1') : $t('learnActivity.mgtScEv.listLabel2') }}
+                    </Tag>
+                    <Tag color="green">
+                        {{getModeLabel(row.source,row.qamode)}}
+                    </Tag>
+                    <Tag color="cyan" v-show="row.eType">
+                        {{row.eType}}
+                    </Tag>
+                </div>
+            </template>
+        </Table>
+    </div>
+</template>
+<script>
+export default {
+    props: {
+        student: {
+            type: Array,
+            default: () => {
+                return []
+            }
+        },
+        //查询成绩表API参数
+        paramsInfo: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        }
+    },
+    data() {
+        return {
+            tableHieght: 600,
+            tableData: []
+        }
+    },
+    computed: {
+        columns() {
+            let c = [{
+                title: this.$t('cusMgt.evRcd'),
+                slot: 'name',
+                tree: true,
+                fixed: 'left',
+                minWidth: 260,
+                maxWidth: 500
+            }]
+            this.student.forEach(item => {
+                c.push({
+                    title: item.name,
+                    key: item.id,
+                    minWidth: 80
+                })
+            })
+            return c
+        }
+    },
+    methods: {
+        /**获取mode对应的label */
+        getModeLabel(code, qamode) {
+            if (qamode == 1) {
+                return this.$t('learnActivity.mgtScEv.paperExam')
+            }
+            for (let item of this.$GLOBAL.EV_MODE()) {
+                if (item.value == code) {
+                    return item.label
+                }
+            }
+        },
+        toTableData(data) {
+            let sIds = this.student.map(item => item.id)
+            this.tableData = data.map(item => {
+                let info = {
+                    id: item.examId,
+                    name: item.examName,
+                    source: item.source,
+                    owner: item.owner,
+                    qamode: item.qamode,
+                    eType: item.eType?.name
+                }
+                sIds.forEach(id => {
+                    let index = item.studentIds.findIndex(i => i === id)
+                    let score
+                    if (index > -1) {
+                        score = item.sum[index]
+                    } else {
+                        score = 0
+                    }
+                    info[id] = score
+                })
+                return info
+            })
+        },
+        getGradeInfo() {
+            this.tableData = []
+            let { courseId, cId, subjectId } = this.paramsInfo
+            if (courseId && cId) {
+                this.$api.learnActivity.getGradeInfo({ courseId, cId: [cId], subjectId }).then(
+                    res => {
+                        if (res.info) {
+                            this.toTableData(res.info)
+                        }
+                    },
+                    err => {
+                        this.$Message.error(this.$t('cusMgt.gradeErr'))
+                    }
+                )
+            }
+        }
+    },
+    watch: {
+        paramsInfo: {
+            deep: true,
+            immediate: true,
+            handler(n, o) {
+                this.getGradeInfo()
+            }
+        }
+    },
+    mounted() {
+        let dom = document.getElementById('grade-container')
+        if (dom) {
+            this.tableHieght = dom.clientHeight - 10
+        }
+
+    }
+}
+</script>
+<style lang="less" scoped>
+.grade-container {
+    width: 100%;
+    height: 100%;
+}
+.mutl-subject-tag {
+    background: #2d8cf0;
+    color: white;
+    padding: 0px 4px;
+    border-radius: 3px;
+    text-align: center;
+    margin-right: 10px;
+    font-size: 12px;
+}
+</style>
+<style lang="less">
+.grade-container .ivu-table-cell-slot {
+    display: inline-block;
+    user-select: none;
+}
+.grade-container ::-webkit-scrollbar {
+    height: 8px;
+}
+.grade-container ::-webkit-scrollbar-thumb {
+    background: #e9e9e9;
+}
+</style>

+ 95 - 0
TEAMModelOS/ClientApp/src/view/mycourse/homework/Homework.less

@@ -0,0 +1,95 @@
+@first-bgColor: #141414;
+@second-bgColor: #1b1b1b;
+@third-bgColor: #222222;
+@primary-textColor: var(--primary-text-color); //文本主颜色
+@second-textColor: var(--second-text-color); //文本副级颜色
+@primary-fontSize: 14px;
+@second-fontSize: 16px;
+
+.hw-container{
+    width: 100%;
+    height: ~"calc(100% - 46px)";
+}
+.hw-item:hover .common-item-icon{
+    display: inline-block;
+}
+.common-item-icon{
+    display: none;
+    font-size: 16px;
+    color: #2d8cf0;
+    vertical-align: baseline;
+    margin-left: 10px;
+    position: absolute;
+}
+.heart-active{
+    display: inline-block;
+    color: #ed4014;
+}
+.ev-description-wrap{
+    display: flex;
+    justify-content: space-between;
+}
+.evaluation-status-tag {
+    padding: 0px 2px;
+    border: 1px solid #1CC0F3;
+    margin-right: 8px;
+    border-radius: 2px;
+    font-size: 12px;
+}
+.ev-attr-wrap{
+    margin-right: 20px;
+    color: white;
+    margin-top: 5px;
+    display: inline-block;
+    .attr-label{
+        color: @second-textColor;
+
+        .ivu-icon{
+            color: #70B1E7;
+        }
+    }
+    .attr-value{
+        color: @primary-textColor;
+    }
+}
+.ac-action-wrap{
+    position: absolute;
+    right: 15px;
+    top: 10px;
+}
+.action-item{
+    margin-right:15px;
+    vertical-align: middle;
+    cursor: pointer;
+    user-select: none;
+    &:hover{
+        color: #2d8cf0;
+    }
+}
+.hw-item{
+    cursor: pointer;
+    padding: 20px 30px 20px 15px;
+    border-bottom: 1px solid #f0f0f0;
+    display: flex;
+    align-items: center;
+    &:hover{
+        background: var(--hover-text-color);
+    }
+    &:hover .item-action-wrap{
+        display: inline-block;
+    }
+}
+.list-icon{
+    color: var(--normal-icon-color);
+    font-size: 35px;
+    width: 90px;
+}
+.exam-name{
+    color: @primary-textColor;
+}
+.exam-detail-wrap{
+    margin-top: 10px;
+}
+.item-action-wrap{
+    height: 20px;
+}

+ 153 - 0
TEAMModelOS/ClientApp/src/view/mycourse/homework/Homework.vue

@@ -0,0 +1,153 @@
+<template>
+  <div class="hw-container">
+    <div class="ac-action-wrap">
+      <span class="action-item" @click="onCreateAc">
+        <Icon type="md-add" />
+        {{$t('cusMgt.cusTab9')}}
+      </span>
+    </div>
+    <vuescroll>
+      <div class="hw-item" v-for="(item,index) in hwList" :key="index" @click="toAcDetail(index)">
+        <Icon custom="iconfont icon-hw" class="list-icon" />
+        <div class="exam-info-wrap">
+          <h3 class="exam-name">
+            {{item.name}}
+          </h3>
+          <p class="exam-detail-wrap">
+            <!-- 学校|个人 -->
+            <Tag color="blue">
+              {{ item.owner == 'school' ? $t('cusMgt.school') : $t('cusMgt.private') }}
+            </Tag>
+            <!-- 活动进度状态 -->
+            <Tag :color="item.progress == 'going' ? 'success' : 'warning'">
+              {{ item.progText }}
+            </Tag>
+            <Tag>
+              <Icon type="md-time" size="16" />
+              {{$jsFn.timeFormat(item.startTime)}}
+            </Tag>
+          </p>
+        </div>
+        <div class="item-action-wrap">
+          <span style="float:right;margin-right:20px">
+            <!-- 修改名称 -->
+            <Icon v-show="item.owner === 'teacher'" type="md-create" style="right:80px" class="common-item-icon" @click.stop="editEvName(index)" :title="$t('learnActivity.mgtScEv.edName')" />
+            <!-- 删除记录 -->
+            <Icon v-show="item.owner === 'teacher'" type="md-trash" style="right:50px" class="common-item-icon" @click.stop="delEv(item)" :title="$t('cusMgt.delRcd')" />
+            <!-- 收藏 -->
+            <Icon :type="isFavorite(item.id) ? 'md-heart':'md-heart-outline'" style="right:20px" :class="['common-item-icon',isFavorite(item.id) ? 'heart-active':'']" @click.stop="toggleFavorite(item)" :title="isFavorite(item.id) ? $t('cusMgt.unfvt') : $t('cusMgt.fvt')" />
+          </span>
+        </div>
+      </div>
+      <EmptyData v-show="!hwList.length" :top="150"></EmptyData>
+    </vuescroll>
+  </div>
+</template>
+<script>
+export default {
+  props: {
+    classInfo: {
+      type: Object,
+      default: () => {
+        return {}
+      }
+    }
+  },
+  data() {
+    return {
+      fIds: [],
+      hwList: []
+    }
+  },
+  methods: {
+    /* 新建活动 跳转活动创建页面 */
+    async onCreateAc() {
+      let isFull = await this.$tools.isBlobContainerFull('private')
+      if (isFull) {
+        this.$Message.warning(this.$t('vote.fullTip'))
+      } else {
+        this.$router.push({
+          name: 'manageHomeWork',
+          params: {
+            isAdd: true
+          }
+        })
+      }
+    },
+    isFavorite(id) {
+      return this.fIds.includes(id)
+    },
+    toAcDetail(index) {
+      this.$router.push({
+        name: 'manageHomeWork',
+        params: {
+          ac: this.hwList[index]
+        }
+      })
+    },
+    getStatusInfo(progress) {
+      let info = {}
+      if (progress == 'pending') {
+        info.progText = this.$t('learnActivity.mgtScEv.pending')
+        info.progColor = '#2d8cf0'
+      } else if (progress == 'going') {
+        info.progText = this.$t('learnActivity.mgtScEv.going')
+        info.progColor = '#19be6b'
+      } else if (progress == 'finish') {
+        info.progText = this.$t('learnActivity.mgtScEv.finish')
+        info.progColor = '#515a6e'
+      }
+      return info
+    },
+    //获取活动列表
+    findTchAc(classId) {
+      this.examList = []
+      this.isShowGrade = false
+      this.isLoading = true
+      let requestData = {
+        school: this.$store.state.userInfo.schoolCode,
+        classes: [classId],
+        userid: this.$store.state.userInfo.TEAMModelId,
+        pk: 'Homework'
+      }
+      this.$api.courseMgmt.findTchAc(requestData).then(
+        res => {
+          if (!res.error) {
+            res.datas.forEach(item => {
+              let statusInfo = this.getStatusInfo(item.progress)
+              item.progText = statusInfo.progText
+              item.progColor = statusInfo.progColor
+            })
+            this.hwList = res.datas
+          }
+        },
+        err => {
+          // this.$Message.error('API error')
+        }
+      ).finally(() => {
+        this.isLoading = false
+      })
+    },
+  },
+  watch: {
+    classInfo: {
+      deep: true,
+      immediate: true,
+      handler(n, o) {
+        console.log(n)
+        if (n && (n.classId || n.stulist)) {
+          this.findTchAc(n.classId || n.stulist)
+        }
+      }
+    }
+  }
+}
+</script>
+<style lang="less" scoped>
+@import "./Homework.less";
+</style>
+<style lang="less">
+.hw-container .ivu-list-item:hover {
+  background: var(--active-item-start);
+}
+</style>

+ 31 - 0
TEAMModelOS/ClientApp/src/view/mycourse/notice/Create.less

@@ -0,0 +1,31 @@
+@main-bgColor: rgb(40,40,40); //主背景颜色
+@borderColor: #424242;
+@primary-textColor: var(--primary-text-color); //文本主颜色
+@second-textColor: #a5a5a5; //文本副级颜色
+@status-pending:#1CC0F3;
+@status-finish:#A0A0A0;
+@status-ready:#71AAE7;
+@primary-fontSize: 14px;
+@second-fontSize: 16px;
+@title-fontSize: 18px;
+
+.create-notify-container{
+    width: 100%;
+    height: 100%;
+    padding: 10px;
+}
+.create-notify-title{
+    color: @primary-textColor;
+    text-align: center;
+    margin-top: 20px;
+    width: 100%;
+}
+.notify-form-wrap{
+    width: 1000px;
+    margin: auto;
+    margin-top: 30px;
+}
+.form-action-btn{
+    width: 120px;
+    margin-right: 10px;
+}

+ 247 - 0
TEAMModelOS/ClientApp/src/view/mycourse/notice/Create.vue

@@ -0,0 +1,247 @@
+<template>
+    <div class="create-notify-container">
+        <vuescroll>
+            <h2 class="create-notify-title">
+                {{$t('notify.classTitle')}}
+            </h2>
+            <div class="notify-form-wrap light-iview-form">
+                <Form ref="noticeInfo" :rules="ruleValidate" :model="notifyInfo" label-colon class="notify-form" :label-width="80">
+                    <FormItem prop="title" :label="$t('notify.title')">
+                        <Input v-special-char v-model="notifyInfo.title" :placeholder="$t('notify.titleHolder')">
+                        </Input>
+                    </FormItem>
+                    <FormItem prop="date" :label="$t('notify.notifyTime')">
+                        <DatePicker v-model="noticeDate" :editable="false" type="daterange" @on-change="getTimestamp" split-panels :placeholder="$t('notify.timeTips')" style="width:100%"></DatePicker>
+                    </FormItem>
+                    <FormItem prop="classes" :label="$t('notify.classLabel')">
+                        <Select v-model="notifyInfo.classes" multiple>
+                            <Option v-for="item in classListFormat" :value="item.id" :key="item.id">{{ item.name }}</Option>
+                        </Select>
+                    </FormItem>
+                    <FormItem prop="content" :label="$t('notify.notifyContent')">
+                        <Input v-special-char v-model="notifyInfo.content" type="textarea" :maxlength="500" show-word-limit :autosize="{minRows: 5,maxRows: 8}" />
+                    </FormItem>
+                    <FormItem :label="$t('notify.attr')" style="user-select: none">
+                        <Checkbox v-model="notifyInfo.autoDelete" :true-value="true" :false-value="false">{{$t('notify.isAutoDel')}}</Checkbox>
+                    </FormItem>
+                    <FormItem style="margin-top:40px;text-align: center;">
+                        <Button type="primary" class="form-action-btn" @click="publish">
+                            {{notifyInfo.id ? $t('notify.saveText') : $t('notify.publishText')}}
+                        </Button>
+                        <Button class="form-action-btn" @click="cancelPublish">
+                            {{notifyInfo.id ? $t('notify.cancelEdit') : $t('notify.cancelPublish')}}
+                        </Button>
+                    </FormItem>
+                </Form>
+            </div>
+        </vuescroll>
+    </div>
+</template>
+<script>
+export default {
+    props: {
+        defaultClass: {
+            type: String,
+            default: '',
+        },
+        classList: {
+            type: Array,
+            default: () => {
+                return []
+            }
+        },
+        courseInfo: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        }
+    },
+    data() {
+        // 验证公告时间
+        const vDate = (rule, value, callback) => {
+            if (!this.notifyInfo.startTime || !this.notifyInfo.endTime) {
+                callback(new Error(this.$t('notify.timeTips')))
+            }
+            callback()
+        }
+        // 验证公告时间
+        const vContent = (rule, value, callback) => {
+            if (!this.notifyInfo.content) {
+                callback(new Error(this.$t('notify.contentTips')))
+            }
+            callback()
+        }
+        return {
+            notifyInfo: {
+                id: '',
+                title: '',
+                content: '',
+                startTime: '',
+                endTime: '',
+                files: [],
+                autoDelete: true,
+                classes: []//接受对象
+            },
+            noticeDate: [],
+            noticeEditor: undefined,
+            ruleValidate: {
+                title: [
+                    { required: true, message: this.$t('notify.titleTips'), trigger: 'change' }
+                ],
+                date: [
+                    { required: true, validator: vDate, trigger: 'change' },
+                ],
+                content: [
+                    { required: true, validator: vContent, trigger: 'change' },
+                ],
+                classes: [
+                    { required: true, type: 'array', trigger: 'change' },
+                ],
+            }
+        }
+    },
+    computed: {
+        classListFormat() {
+            if (this.classList) {
+                let data = this.classList.map(item => {
+                    return {
+                        id: item.classId || item.stulist,
+                        year: item.year,
+                        name: item.classInfo?.name || item.listName,
+                        type: item.classId ? 'class' : 'teach'
+                    }
+                })
+                return data
+            }
+            return []
+        }
+    },
+    methods: {
+        getTimestamp(date) {
+            if (date.length == 2) {
+                this.notifyInfo.startTime = new Date(date[0]).getTime()
+                this.notifyInfo.endTime = new Date(date[1]).getTime()
+            }
+        },
+        //初始化富文本编辑器
+        initEditor() {
+            if (!this.noticeEditor) {
+                let noticeEditor = new E(this.$refs.noticeEditor)
+                noticeEditor.customConfig.onchange = (html) => {
+                    //TODO 设置课程公告
+                    this.notifyInfo.content = html == '<p><br></p>' ? '' : html
+                    console.log(this.notifyInfo.content)
+                }
+                noticeEditor.customConfig.menus = [
+                    'bold', // 粗体
+                    'italic', // 斜体
+                    'underline', // 下划线
+                    'list', // 列表
+                    'link', // 插入链接
+                    'image' // 插入图片
+                ]
+                noticeEditor.customConfig.showLinkImg = false
+                noticeEditor.customConfig.uploadFileName = 'files'
+                noticeEditor.create()
+                noticeEditor.txt.html(this.notifyInfo.content)
+                this.noticeEditor = noticeEditor
+            }
+        },
+        cancelPublish() {
+            if (!this.notifyInfo.id && (this.notifyInfo.title || this.notifyInfo.content || this.notifyInfo.startTime || this.notifyInfo.endTime)) {
+                this.$Modal.confirm({
+                    title: this.$t('notify.cancelPublish'),
+                    content: this.$t('notify.reserveContent'),
+                    okText: this.$t('notify.reserveOk'),
+                    cancelText: this.$t('notify.reserveCancel'),
+                    onOk: () => {
+                        this.saveNotice(0)
+                    },
+                    onCancel: () => {
+                        this.$router.push({
+                            name: 'classnotice'
+                        })
+                    }
+                })
+            } else {
+                this.$emit('cancel-publish')
+            }
+        },
+        publish() {
+            this.$refs['noticeInfo'].validate((valid) => {
+                if (valid) {
+                    this.saveNotice(1)
+                } else {
+                    this.$Message.error(this.$t('notify.fullInfoTips'));
+                }
+            })
+
+        },
+        saveNotice(publish) {
+            let params = {
+                id: this.notifyInfo.id,
+                code: this.$store.state.userInfo.schoolCode,
+                school: this.$store.state.userInfo.schoolCode,
+                startTime: this.notifyInfo.startTime,
+                endTime: this.notifyInfo.endTime,
+                type: 'course',
+                classes: this.notifyInfo.classes,
+                content: this.notifyInfo.content,
+                title: this.notifyInfo.title,
+                creatorId: this.$store.state.userInfo.TEAMModelId,
+                autoDelete: this.notifyInfo.autoDelete,
+                publish,
+                scope: this.courseInfo?.scope,
+                createTime: new Date().getTime()
+            }
+            this.$api.notice.UpsertNotice(params).then(
+                res => {
+                    this.$emit('on-publish')
+                },
+                err => {
+                    // this.$Message.error('API error!')
+                }
+            )
+        }
+    },
+    created() {
+        let routeData = this.$route.params
+        //编辑公告
+        if (routeData.notice) {
+            this.notifyInfo.title = routeData.notice.title
+            this.notifyInfo.startTime = routeData.notice.startTime
+            this.notifyInfo.endTime = routeData.notice.endTime
+            this.notifyInfo.content = routeData.notice.content
+            this.notifyInfo.classes = routeData.notice.classes
+            this.notifyInfo.autoDelete = routeData.notice.autoDelete
+            this.notifyInfo.id = routeData.notice.id
+            this.noticeDate = []
+            this.noticeDate.push(new Date(this.notifyInfo.startTime))
+            this.noticeDate.push(new Date(this.notifyInfo.endTime))
+        }
+    },
+    watch: {
+        defaultClass: {
+            immediate: true,
+            handler(n, o) {
+                this.notifyInfo.classes = []
+                this.notifyInfo.classes.push(this.defaultClass)
+            }
+        }
+    }
+}
+</script>
+<style lang="less" scoped>
+@import "./Create.less";
+</style>
+<style lang="less">
+.notify-form-wrap {
+    .w-e-menu {
+        z-index: 100 !important;
+    }
+    .w-e-text-container {
+        z-index: 99 !important;
+    }
+}
+</style>

+ 86 - 0
TEAMModelOS/ClientApp/src/view/mycourse/notice/Notice.less

@@ -0,0 +1,86 @@
+@main-bgColor: rgb(40, 40, 40); //主背景颜色
+@borderColor: var(--border-color);
+@primary-textColor: var(--primary-text-color); //文本主颜色
+@second-textColor: var(--second-text-color); //文本副级颜色
+@status-pending: #1CC0F3;
+@status-finish: #A0A0A0;
+@status-ready: #71AAE7;
+@primary-fontSize: 14px;
+@second-fontSize: 16px;
+@title-fontSize: 18px;
+
+.cus-notice-container {
+    width: 100%;
+    height: ~"calc(100% - 50px)";
+}
+
+.ac-action-wrap {
+    position: absolute;
+    right: 15px;
+    top: 10px;
+}
+
+.action-item {
+    margin-right: 15px;
+    vertical-align: baseline;
+    cursor: pointer;
+    user-select: none;
+
+    &:hover {
+        color: #2d8cf0;
+    }
+}
+
+.notify-item {
+    padding: 20px 30px 20px 15px;
+    border-bottom: 1px solid @borderColor;
+    display: flex;
+    align-items: center;
+    cursor: pointer;
+
+    &:hover {
+        background: var(--hover-text-color);
+    }
+
+    .nitify-icon {
+        color: var(--normal-icon-color);
+        font-size: 40px;
+        width: 90px;
+    }
+
+    .notify-info-wrap {
+        flex: 1;
+    }
+
+    .notify-action-wrap {
+        width: 210px;
+        text-align: right;
+    }
+
+    .notify-title {
+        color: @primary-textColor;
+    }
+
+    .notify-time {
+        margin-top: 5px;
+        color: @second-textColor;
+    }
+
+    .file-tag {
+        font-size: 12px;
+        background: #ff9900;
+        color: white;
+        padding: 2px 8px;
+        font-weight: 400;
+        border-radius: 2px;
+        margin-left: 10px;
+    }
+}
+
+.notice-content {
+    margin-bottom: 30px;
+    text-indent: 2em;
+
+    font-size: 16px;
+    color: #515a6e;
+}

+ 166 - 0
TEAMModelOS/ClientApp/src/view/mycourse/notice/Notice.vue

@@ -0,0 +1,166 @@
+<template>
+    <div class="cus-notice-container">
+        <div class="ac-action-wrap">
+            <span class="action-item" v-show="!isCreate" @click="isCreate = !isCreate">
+                <Icon type="md-add" />
+                {{$t('cusMgt.cusTab10')}}
+            </span>
+        </div>
+        <Create v-if="isCreate" :courseInfo="courseInfo" :classList="classList" @on-publish="onPublish" @cancel-publish="isCreate = !isCreate" :defaultClass="classInfo.classId || classInfo.stulist"></Create>
+        <vuescroll v-else>
+            <div v-for="(item,index) in noticeList" :key="index" class="notify-item" @click="viewNotice(index)">
+                <Icon custom="iconfont icon-notify" class="nitify-icon" />
+                <div class="notify-info-wrap">
+                    <h3 class="notify-title">
+                        {{item.title}}
+                        <span class="file-tag" style="background:#2db7f5" v-show="item.status == 'pending'">
+                            {{$t('notify.pending')}}
+                        </span>
+                        <span class="file-tag" style="background:#ff9900" v-show="item.status == 'going'">
+                            {{$t('notify.going')}}
+                        </span>
+                        <span class="file-tag" style="background:#19be6b" v-show="item.status == 'finish'">
+                            {{$t('notify.finish')}}
+                        </span>
+                    </h3>
+                    <p class="notify-time">
+                        {{$t('notify.ntTime')}}
+                        {{$jsFn.dateFormat(item.startTime)}}
+                        —
+                        {{$jsFn.dateFormat(item.endTime)}}
+                    </p>
+                </div>
+                <div class="notify-action-wrap">
+                    <Button type="warning" size="small" style="width:60px;" @click.stop="delNotice(index)">
+                        {{$t('notify.delete')}}
+                    </Button>
+                    <!-- <Button type="primary" size="small" style="width:60px;margin-left:15px" @click="editNotice(index)">
+                        {{$t('notify.edit')}}
+                    </Button> -->
+                </div>
+            </div>
+            <EmptyData v-show="!noticeList.length" :top="100" :textContent="$t('notify.noNotify')"></EmptyData>
+        </vuescroll>
+        <Modal v-model="viewStatus" width="800" footer-hide className="ed-name-modal">
+            <div slot="header" class="modal-header" style="text-align: center;width: 100%;">
+                {{noticeList[viewIndex] ? noticeList[viewIndex].title : ''}}
+            </div>
+            <div class="edit-name-content" v-if="noticeList[viewIndex]">
+                <p class="notice-content" v-html="noticeList[viewIndex].content"></p>
+            </div>
+        </Modal>
+    </div>
+</template>
+<script>
+import Create from "./Create.vue"
+export default {
+    components: {
+        Create
+    },
+    props: {
+        classList: {
+            type: Array,
+            default: () => {
+                return []
+            }
+        },
+        classInfo: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        },
+        courseInfo: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        }
+    },
+    data() {
+        return {
+            isCreate: false,
+            viewStatus: false,
+            viewIndex: 0,
+            noticeList: []
+        }
+    },
+    methods: {
+        onPublish() {
+            this.isCreate = !this.isCreate
+            this.findNotice()
+        },
+        editNotice() {
+
+        },
+        delNotice(index) {
+            this.$Modal.confirm({
+                title: this.$t('notify.delNotifyTitle'),
+                content: `${this.$t('notify.delNotifyContent')}${this.noticeList[index].title}?`,
+                okText: this.$t('notify.yes'),
+                cancelText: this.$t('notify.no'),
+                onOk: () => {
+                    let params = {
+                        id: this.noticeList[index].id,
+                        code: this.noticeList[index].code
+                    }
+                    this.$api.notice.DelNotice(params).then(
+                        res => {
+                            this.$Message.success(this.$t('notify.delOk'))
+                            this.noticeList.splice(index, 1)
+                        },
+                        err => {
+                            this.$Message.error(this.$t('notify.delErr'))
+                        }
+                    )
+                }
+            })
+        },
+        viewNotice(index) {
+            this.viewIndex = index
+            this.viewStatus = true
+        },
+        findNotice() {
+            let params = {
+                admin: "1",
+                code: this.$store.state.userInfo.schoolCode,
+                type: "course",
+                scope: this.courseInfo.scope,
+                classes: [this.classInfo.classId || this.classInfo.stulist]
+            }
+            this.$api.notice.FindNotice(params).then(
+                res => {
+                    let curTime = new Date().getTime()
+                    res.notices.forEach(item => {
+                        if (item.startTime > curTime) {
+                            item.status = 'pending'
+                        } else if (item.endTime < curTime) {
+                            item.status = 'finish'
+                        } else {
+                            item.status = 'going'
+                        }
+                    })
+                    this.noticeList = res.notices
+                },
+                err => {
+                    // this.$Message.error('API error!')
+                }
+            )
+        },
+    },
+    watch: {
+        classInfo: {
+            deep: true,
+            immediate: true,
+            handler(n, o) {
+                if (n && (n.classId || n.stulist)) this.findNotice()
+            }
+        }
+    }
+}
+</script>
+<style lang="less" scoped>
+@import "Notice.less";
+</style>
+<style lang="less">
+</style>

+ 65 - 0
TEAMModelOS/ClientApp/src/view/mycourse/record/Record.less

@@ -0,0 +1,65 @@
+.rcd-item{
+    cursor: pointer;
+    display: flex;
+    border-bottom: 1px solid #e8eaec;
+    padding: 20px 30px 20px 15px;
+    &:hover .common-item-icon{
+        display: inline-block;
+    }
+    &:hover{
+        background: var(--active-item-start);
+    }
+}
+.record-poster-wrap{
+    width: 120px;
+    height: 65px;
+    background-size: contain;
+    background-repeat: no-repeat;
+    background-position: center;
+    margin: 0px 10px;
+}
+.record-name{
+    color:var(--primary-text-color);
+    font-size:16px;
+    margin-top: 5px;
+}
+.record-info{
+    margin-right: 24px;
+}
+.record-info-value{
+    color: #17233d;
+}
+.common-item-icon{
+    display: none;
+    font-size: 16px;
+    color: #2d8cf0;
+    vertical-align: baseline;
+    margin-left: 10px;
+    position: absolute;
+}
+.delete-item{
+    right: 60px;
+}
+.ed-name{
+    right: 90px;
+}
+.heart-item{
+    right: 2px;
+}
+.heart-active{
+    display: inline-block;
+    color: #ed4014;
+}
+.share-student{
+    right: 30px;
+    margin-top: -4px;
+}
+.share-active{
+    display: inline-block;
+    color: #2d8cf0;
+}
+.item-icon-wrap{
+    float:right;
+    margin-right:20px;
+    position: relative;    
+}

+ 256 - 0
TEAMModelOS/ClientApp/src/view/mycourse/record/Record.vue

@@ -0,0 +1,256 @@
+<template>
+    <div class="record-container">
+        <vuescroll>
+            <Alert v-show="rcdParams.scope == 'private'" show-icon type="warning" closable>
+                {{$t('cusMgt.recordTips')}}
+            </Alert>
+            <div class="rcd-item" v-for="(item,index) in recordList" :key="index" @click="toClassRecoerd(index)">
+                <RcdPoster class="record-poster-wrap" :poster="item.poster"></RcdPoster>
+                <div style="flex:1">
+                    <p class="record-name" style="padding-left:10px">
+                        {{item.name}}
+                        <span class="item-icon-wrap">
+                            <Icon type="md-create" class="common-item-icon ed-name" @click.stop="editRecordName(index)" :title="$t('cusMgt.edRdName')" />
+                            <Icon type="md-trash" class="common-item-icon delete-item" @click.stop="delRecord(item.id)" :title="$t('cusMgt.delRcd')" />
+                            <Icon :type="item.isShare ? 'ios-share-alt':'ios-share-alt-outline'" :size="20" :class="['common-item-icon','share-student',item.isShare ? 'share-active':'']" @click.stop="toggleShare(item)" :title="item.isShare ? $t('cusMgt.unShare') : $t('cusMgt.shareToStu')" />
+                            <Icon :type="isFavorite(item.id) ? 'md-heart':'md-heart-outline'" :class="['common-item-icon','heart-item',isFavorite(item.id) ? 'heart-active':'']" @click.stop="toggleFavorite(item)" :title="isFavorite(item.id) ? $t('cusMgt.unfvt') : $t('cusMgt.fvt')" />
+                        </span>
+                    </p>
+                    <div style="padding-left:10px;margin-top:10px">
+                        <!-- 出席人数 -->
+                        <span class="record-info">
+                            <span>{{$t('cusMgt.rcd.attendCount')}}:</span>
+                            <span class="record-info-value"> {{item.attendCount}}{{$t('unit.text7')}}</span>
+                        </span>
+                        <!-- 总计分 -->
+                        <span class="record-info">
+                            <span>{{$t('cusMgt.rcdScore')}}</span>
+                            <span class="record-info-value">
+                                {{item.totalPoint}}
+                            </span>
+                        </span>
+                        <!-- 作品总数 -->
+                        <span class="record-info">
+                            <span>{{$t('cusMgt.rcd.colctCount')}}: </span>
+                            <span class="record-info-value">
+                                {{item.collateCount}}
+                            </span>
+                        </span>
+                        <!-- 测验得分率 -->
+                        <span class="record-info">
+                            <span>{{$t('cusMgt.rcd.scoreRate')}}: </span>
+                            <span class="record-info-value">
+                                {{item.examPointRate}}
+                            </span>
+                        </span>
+                        <!-- 互动总数 -->
+                        <span class="record-info">
+                            <span>{{$t('cusMgt.rcd.interactionCount')}}: </span>
+                            <span class="record-info-value">
+                                {{item.clientInteractionCount}}
+                            </span>
+                        </span>
+                        <!-- 时长 -->
+                        <span class="record-info">
+                            <span>{{$t('cusMgt.duration')}}</span>
+                            <span class="record-info-value">{{handleDuration(item.duration)}}</span>
+                        </span>
+                        <!-- 过期时间 -->
+                        <span class="record-info" v-if="item.expire > -1">
+                            <span style="color:red">
+                                {{$tools.getRelativeTime(item.expire)}}
+                            </span>
+                            <span style="color:red">
+                                {{$t('cusMgt.rcdExpired')}}
+                            </span>
+                        </span>
+                        <!-- 时间 -->
+                        <span class="record-info" style="float:right">
+                            <span>
+                                <Icon type="md-time" color="#70B1E7" />
+                            </span>
+                            <span class="record-info-value">
+                                {{$jsFn.timeFormat(item.startTime)}}
+                            </span>
+                        </span>
+                        <div class="record-action-wrap">
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <EmptyData v-show="recordList.length == 0" :textContent="$t('cusMgt.noRecord')" :top="150"></EmptyData>
+        </vuescroll>
+        <!-- 修改课堂记录名称 -->
+        <Modal v-model="editRdStatus" className="ed-name-modal" footer-hide>
+            <div slot="header" class="modal-header">
+                {{$t('cusMgt.edRdName')}}
+            </div>
+            <div class="edit-name-content">
+                <p class="edit-name-label">
+                    {{$t('cusMgt.listName')}}
+                </p>
+                <Input v-model="editName" :placeholder="$t('cusMgt.edRdName')" />
+                <Button :loading="btnLoading" @click="confirmEditRd" long type="primary" class="confirm-btn">{{ $t('syllabus.confirm') }}</Button>
+            </div>
+        </Modal>
+    </div>
+</template>
+<script>
+import RcdPoster from "../../homepage/RcdPoster.vue"
+export default {
+    components: {
+        RcdPoster
+    },
+    props: {
+        fIds: {
+            type: Array,
+            default: () => {
+                return []
+            }
+        },
+        rcdParams: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        }
+    },
+    data() {
+        return {
+            btnLoading: false,
+            editName: '',
+            recordList: [],
+            editRdStatus: false,
+        }
+    },
+    methods: {
+        //根据时长换算时间
+        handleDuration(duration) {
+            if (!duration) return this.$t('cusMgt.noData')
+            let calc = duration
+            //天
+            let dayC = 24 * 60 * 60
+            let day = parseInt(calc / dayC)
+            calc = calc % dayC
+
+            //时
+            let hourC = 60 * 60
+            let hour = parseInt(calc / hourC)
+            calc = calc % hourC
+
+            //分
+            let minC = 60
+            let min = parseInt(calc / minC)
+            calc = calc % minC
+
+            //秒
+            let sec = calc
+            return `${day > 0 ? day + this.$t('unit.day') : ''} ${hour > 0 ? hour + this.$t('unit.hour') : ''} ${min > 0 ? min + this.$t('unit.text5') : ''} ${sec > 0 ? sec + this.$t('unit.second') : ''}`
+        },
+        isFavorite(id) {
+            return this.fIds.includes(id)
+        },
+        // 修改记录名称
+        confirmEditRd() {
+            if (this.editName) {
+                let recordInfo = this.recordList[this.edRdIndex]
+                this.btnLoading = true
+                this.$api.lessonRecord.updateLesson({
+                    "lesson_id": recordInfo.id,
+                    "tmdid": recordInfo.tmdid,
+                    "school": recordInfo.school,
+                    "scope": recordInfo.scope,
+                    "grant_types": [{
+                        "grant_type": "up-baseinfo",
+                        "data": {
+                            "name": this.editName,
+                        }
+                    }]
+                }).then(
+                    res => {
+                        if (!res.error) {
+                            this.$Message.success(this.$t('cusMgt.editOk'))
+                            let i = this.recordList.findIndex(item => item.id == recordInfo.id)
+                            if (i > -1) {
+                                this.$set(this.recordList[i], 'name', this.editName)
+                            }
+                            this.editRdStatus = false
+                        }
+                    }).finally(() => {
+                        this.btnLoading = false
+                    })
+            } else {
+                this.$Message.warning(this.$t('learnActivity.mgtScEv.edNameHolder'))
+            }
+        },
+        editRecordName(index) {
+            this.editRdStatus = true
+            this.edRdIndex = index
+            this.editName = this.recordList[index]?.name
+        },
+        //查看课堂记录详情
+        toClassRecoerd(index) {
+            this.$router.push({
+                name: 'classRecord',
+                params: {
+                    record: this.recordList[index]
+                }
+            })
+        },
+        getRecordList() {
+            let params = {
+                "tmdid": this.$store.state.userInfo.TEAMModelId,
+                "scope": this.rcdParams.scope,
+                "school": this.$store.state.userInfo.schoolCode,
+                "DESC": "startTime",
+                "pageCount": 50,
+                "courseId": this.rcdParams.courseId,
+                "groupIds": [this.rcdParams.classId]
+            }
+            this.$api.lessonRecord.getLessonList(params).then(
+                res => {
+                    if (res.lessonRecords) {
+                        res.lessonRecords.forEach(item => {
+                            item.show = item.show ? item.show : []
+                            item.isShare = item.show.includes('student')
+                        })
+                        this.recordList = res.lessonRecords
+                        let sasInfo = {}
+                        let blobInfo = this.listType === 'school' ? this.$store.state.user.schoolProfile : this.$store.state.user.userProfile
+                        sasInfo.sas = '?' + blobInfo.blob_sas
+                        sasInfo.name = this.listType === 'school' ? this.$store.state.userInfo.schoolCode : this.$store.state.userInfo.TEAMModelId
+                        sasInfo.url = blobInfo.blob_uri.slice(0, blobInfo.blob_uri.lastIndexOf(sasInfo.name) - 1)
+                        this.recordList.forEach(item => {
+                            item.sokrateImg = `${sasInfo.url}/${sasInfo.name}/records/${item.id}/Sokrates/SokratesResults/event.png${sasInfo.sas}`
+                            item.eNote = `${sasInfo.url}/${sasInfo.name}/records/${item.id}/Note.pdf${sasInfo.sas}`
+                            item.video = `${sasInfo.url}/${sasInfo.name}/records/${item.id}/Record/CourseRecord.mp4${sasInfo.sas}`
+                            item.poster = `${sasInfo.url}/${sasInfo.name}/records/${item.id}/Record/CoverImage.jpg${sasInfo.sas}`
+                        })
+                    }
+                },
+                err => {
+                    this.$Message.error(this.$t('cusMgt.rcdErr'))
+                }
+            )
+        },
+    },
+    watch: {
+        rcdParams: {
+            deep: true,
+            immediate: true,
+            handler(n, o) {
+                if (n) {
+                    let { scope, courseId, classId } = n
+                    if (scope && courseId, classId) this.getRecordList()
+                }
+
+            }
+        }
+    }
+}
+</script>
+<style lang="less" scoped>
+@import "./Record.less";
+</style>
+<style lang="less">
+</style>

+ 18 - 0
TEAMModelOS/ClientApp/src/view/mycourse/student/Student.less

@@ -0,0 +1,18 @@
+.cus-student-contianer{
+    height: ~"calc(100% - 50px)";
+    width: 100%;
+}
+.stu-action-wrap{
+    position: absolute;
+    right: 20px;
+    top: 7px;
+}
+.action-item{
+    margin-right:15px;
+    vertical-align: baseline;
+    cursor: pointer;
+    user-select: none;
+    &:hover{
+        color: #2d8cf0;
+    }
+}

+ 512 - 0
TEAMModelOS/ClientApp/src/view/mycourse/student/Student.vue

@@ -0,0 +1,512 @@
+<template>
+    <div class="cus-student-contianer">
+        <div class="stu-action-wrap common-save-btn">
+            <span v-if="stuList && stuList.scope == 'private'" class="action-item" @click="delStudents">
+                <Icon type="md-trash" size="16" />
+                {{$t('cusMgt.delStu')}}
+            </span>
+            <span v-if="stuList && stuList.scope == 'private'" v-show="$store.state.userInfo.hasSchool" class="action-item" @click="addStuStatus = true">
+                <Icon type="md-add" size="16" />
+                {{$t('cusMgt.addStu')}}
+            </span>
+            <span v-if="stuList && stuList.scope == 'private'" class="action-item" @click="setIrsStatus = true">
+                <Icon type="md-settings" size="16" />
+                {{$t('cusMgt.editStu')}}
+            </span>
+            <Button icon="md-download" class="save-btn action-item" @click="generateQrcodes" :loading="btnLoading">
+                {{$t('schoolBaseInfo.exportQrCode')}}
+            </Button>
+            <Button icon="md-download" class="save-btn action-item" @click="generatePDF" :loading="pdfLoading">
+                {{$t('schoolBaseInfo.exportList')}}
+            </Button>
+            <span class="action-item" @click="exportStudents">
+                <Icon type="md-download" size="16" />
+                {{$t('cusMgt.exportStu')}}
+            </span>
+        </div>
+        <vuescroll style="height:100%;">
+            <Alert @on-close="isClose = true" v-show="!isClose" show-icon type="warning" style="margin-bottom:0px" closable>
+                {{$t('cusMgt.groupTips')}}
+            </Alert>
+            <Table :columns="isStuList ? listColumn : classColumn" :data="students" @on-selection-change="(selections)=>{delSelection = selections}" class="system-classroom-table" :loading="stuLoading" :no-data-text="$t('cusMgt.noStu')">
+                <Loading slot="loading" bgColor="rgba(103, 103, 103, 0.27)"></Loading>
+                <template slot-scope="{ row }" slot="picture">
+                    <PersonalPhoto :name="row.name || ''" :picture="row.picture" />
+                </template>
+                <template slot-scope="{ row,index }" slot="nickname">
+                    <span v-show="editIndex !== index" :style="{color: row.nickname ? '#17233d' : '#ed4014'}">
+                        {{row.nickname || $t('cusMgt.notSet')}}
+                    </span>
+                    <Input :min="1" v-model="nickname" v-show="editIndex == index" style="width: 80px;" />
+                    <Icon type="md-checkmark" v-show="editIndex == index" @click="confirmSetNickName(row,index)" class="reset-no-btn" />
+                    <Icon type="md-close" v-show="editIndex == index" @click="cancelSetInfo()" class="reset-no-btn" />
+                </template>
+                <template slot-scope="{ row }" slot="no">
+                    <span :style="{color:row.no ? '#303030':'red'}">{{row.no || $t('cusMgt.notSet')}}</span>
+                </template>
+                <template slot-scope="{ row }" slot="type">
+                    <span>{{row.type === 2 ? $t('cusMgt.schoolType') : $t('cusMgt.tmIDType')}}</span>
+                </template>
+                <template slot-scope="{ row,index }" slot="irs">
+                    <span v-show="editIndex !== index" :style="{color:row.irs ? '#303030':'red'}">{{row.irs || $t('cusMgt.notSet')}}</span>
+                    <InputNumber :min="1" v-model="editIrs" v-show="editIndex == index" style="width: 60px;"></InputNumber>
+                    <Icon type="md-checkmark" v-show="editIndex == index" @click="confirmSetNo(row,index)" class="reset-no-btn" />
+                    <Icon type="md-close" v-show="editIndex == index" @click="cancelSetInfo()" class="reset-no-btn" />
+                </template>
+                <template slot-scope="{ row }" slot="groupId">
+                    <span>{{row.groupId ? row.groupId : '--'}}</span>
+                </template>
+                <template slot-scope="{ row }" slot="groupName">
+                    <span>{{row.groupName ? row.groupName : '--'}}</span>
+                </template>
+                <template slot-scope="{ row,index }" slot="action">
+                    <div class="item-tools">
+                        <Icon type="md-create" size="18" style="cursor:pointer;margin-left:10px" :title="$t('schoolBaseInfo.noSetLabel')" @click="setStuInfo(row,index)" />
+                    </div>
+                </template>
+            </Table>
+        </vuescroll>
+        <Modal v-model="addStuStatus" :title="$t('cusMgt.addStu')" width="1200" @on-ok="confirmAddStu">
+            <!-- 添加学生 -->
+            <StudentList v-if="canAddStu" @getSelectInfo="(selction)=>{selections = selction}"></StudentList>
+            <!-- 跨校不能添加学生 -->
+            <TipsInfo v-else-if="addStuStatus" :msg="crossSchool"></TipsInfo>
+        </Modal>
+        <!-- 快速设置IRS -->
+        <Modal v-model="setIrsStatus" footer-hide className="ed-name-modal">
+            <div slot="header" class="modal-header">
+                {{$t('cusMgt.editStu')}}
+            </div>
+            <div class="edit-name-content">
+                <p class="set-irs-tips">{{$t('cusMgt.irsTips1')}}</p>
+                <p class="set-irs-tips">{{$t('cusMgt.irsTips2')}}</p>
+                <Button :loading="btnLoading" @click="confirmFastSet" long type="primary" class="confirm-btn">{{ $t('syllabus.confirm') }}</Button>
+            </div>
+        </Modal>
+    </div>
+</template>
+<script>
+import StudentList from '@/components/coursemgt/StudentList.vue'
+import excel from '@/utils/excel.js'
+export default {
+    components: {
+        StudentList
+    },
+    props: {
+        stuList: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        },
+        classInfo: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        }
+    },
+    data() {
+        return {
+            btnLoading: false,
+            pdfLoading: false,
+            setIrsStatus: false,
+            addStuStatus: false,
+            editIrs: null,
+            nickname: '',
+            editIndex: -1,
+            delSelection: [],
+            stuLoading: false,
+            isClose: false,
+            classColumn: [
+                {
+                    title: ' ',
+                    slot: 'picture',
+                    align: 'left ',
+                    width: '150'
+                },
+                {
+                    title: this.$t('courseManage.classroom.studentTableC2'),
+                    key: 'name',
+                    align: 'center '
+                },
+                {
+                    title: this.$t('courseManage.classroom.studentTableC7'),
+                    key: 'id',
+                    align: 'center',
+                    sortable: true
+                },
+                {
+                    title: this.$t('courseManage.classroom.studentTableC6'),
+                    slot: 'groupName',
+                    align: 'center'
+                },
+                {
+                    title: this.$t('courseManage.classroom.studentTableC1'),
+                    slot: 'no',
+                    align: 'center',
+                    sortable: true
+                },
+                {
+                    title: this.$t('courseManage.classroom.studentTableC9'),
+                    slot: 'irs',
+                    align: 'center',
+                    sortable: true
+                },
+            ],
+            listColumn: [
+                {
+                    type: 'selection',
+                    align: 'center ',
+                    width: '60'
+                },
+                {
+                    title: ' ',
+                    slot: 'picture',
+                    align: 'center ',
+                    width: '100'
+                },
+                {
+                    title: this.$t('courseManage.classroom.studentTableC2'),
+                    key: 'name',
+                    align: 'center '
+                },
+                {
+                    title: this.$t('courseManage.classroom.studentTableC11'),
+                    slot: 'nickname',
+                    align: 'center ',
+                    width: '180'
+                },
+                {
+                    title: this.$t('courseManage.classroom.studentTableC7'),
+                    key: 'id',
+                    align: 'center',
+                    sortable: true
+                },
+                {
+                    title: this.$t('courseManage.classroom.studentTableC8'),
+                    slot: 'type',
+                    align: 'center'
+                },
+                {
+                    title: this.$t('courseManage.classroom.studentTableC9'),
+                    key: 'irs',
+                    slot: 'irs',
+                    align: 'center',
+                    sortable: true,
+                    sortMethod: (a, b, type) => {
+                        if (!b) {
+                            return -1
+                        }
+                        if (type == 'desc') {
+                            return parseInt(a) > parseInt(b) ? -1 : 1
+                        } else {
+                            return parseInt(a) < parseInt(b) ? -1 : 1
+                        }
+                    }
+                },
+                {
+                    title: ' ',
+                    slot: 'action',
+                    align: 'center'
+                }
+            ],
+        }
+    },
+    computed: {
+        //判断是否可以添加学生
+        canAddStu() {
+            return this.addStuStatus && (!this.classInfo.listSchool || this.classInfo.listSchool == this.$store.state.userInfo.schoolCode)
+        },
+        crossSchool() {
+            return `${this.$t('cusMgt.listSchoolTips1')}<strong>${this.getSchoolName(this.classInfo?.listSchool)}</strong>${this.$t('cusMgt.listSchoolTips2')}`
+        },
+        isStuList() {
+            return !!this.classInfo.stulist
+        },
+        students() {
+            return this.classInfo.allStu || []
+        },
+    },
+    methods: {
+        // 批量设置irs
+        confirmFastSet() {
+            if (this.stuList) {
+                this.btnLoading = true
+                this.stuList.members.forEach((item, index) => {
+                    item.irs = (index + 1) + ''
+                })
+                this.$api.common.upsertGroupInfo(this.stuList).then(
+                    res => {
+                        this.$Message.success(this.$t('cusMgt.listSaveOk'))
+                        //TODO 更新父组件数据
+                        this.$emit('on-set-irs', {
+                            stuListId: this.stuList.id,
+                            students: this.stuList.members
+                        })
+                        this.setIrsStatus = false
+                    },
+                    err => {
+                        this.$Message.error(this.$t('cusMgt.listSaveErr'))
+                    }
+                ).finally(() => {
+                    this.btnLoading = false
+                })
+            }
+        },
+        getSchoolName(school) {
+            debugger
+            if (!school) {
+                return this.$t('cusMgt.listSchoolTips3')
+            }
+            let schools = this.$store.state?.user?.userProfile?.schools
+            if (schools) {
+                let s = schools.find(item => item.schoolId === school)
+                return s ? s.name : school
+            }
+            return this.$t('cusMgt.listSchoolTips3')
+        },
+        //确认添加学生
+        confirmAddStu() {
+            if (this.selections.length > 0) {
+                let stuIds = []
+                let addStudents = []
+                if (this.classInfo?.allStu) {
+                    stuIds = this.classInfo.allStu.map(item => {
+                        return item.id
+                    })
+                }
+                this.selections.forEach(item => {
+                    if (!stuIds.includes(item.id)) { //如果已经有学生,根据账号去重
+                        let irs = this.getDefIRS(this.stuList)
+                        let info = {
+                            id: item.id,
+                            code: this.$store.state.userInfo.schoolCode,
+                            name: item.name,
+                            picture: item.picture,
+                            type: 2,
+                            irs: irs + '',//这里需要设置默认号码
+                            className: item.className,
+                            classId: item.classId
+                        }
+                        addStudents.push(info)
+                        this.stuList.members.push(info)
+                    }
+                })
+                this.listLoading = true
+                this.$api.common.upsertGroupInfo(this.stuList).then(
+                    res => {
+                        this.$Message.success(this.$t('cusMgt.listSaveOk'))
+                        //TODO 更新父组件数据
+                        this.$emit('on-add-student', {
+                            stuListId: this.stuList.id,
+                            addStudents: addStudents
+                        })
+                    },
+                    err => {
+                        this.$Message.error(this.$t('cusMgt.listSaveErr'))
+                    }
+                ).finally(() => {
+                    this.listLoading = false
+                })
+            }
+        },
+        getDefIRS(stulist) {
+            let defIRS = stulist.members.length + 1
+            let irs = stulist.members.map(item => item.irs)
+            irs = irs.filter(item => !!item)
+            irs.sort((a, b) => {
+                return parseInt(a) - parseInt(b) > 0 ? 1 : -1
+            })
+            for (let index = 0; index < irs.length; index++) {
+                if ((index + 1) != irs[index]) {
+                    defIRS = index + 1
+                    break
+                }
+            }
+            return defIRS
+        },
+        /* 快速生成名单清单 */
+        async generatePDF() {
+            this.pdfLoading = true
+            let listName = this.classInfo.listName || this.classInfo.classInfo.name
+            this.$tools.batchStuList(this.students, listName).then(
+                res => {
+                    this.$Message.success(this.$t('common.generatOk'))
+                },
+                err => {
+                    this.$Message.error(this.$t('common.generateErr'))
+                }
+            ).finally(() => {
+                this.pdfLoading = false
+            })
+        },
+        generateQrcodes() {
+            this.btnLoading = true
+            this.$tools.batchQrcodes(this.students.map(i => i.id), this.classInfo.listName || this.classInfo.classInfo.name).then(
+                res => {
+                    this.$Message.success(this.$t('common.generatOk'))
+                },
+                err => {
+                    this.$Message.error(this.$t('common.generateErr'))
+                }
+            ).finally(() => {
+                this.btnLoading = false
+            })
+        },
+        //导出名单
+        exportStudents() {
+            let fileName = this.classInfo.listName || this.classInfo.classInfo.name
+            let sIndex = this.isStuList ? 2 : 1
+            let column = this.isStuList ? this.listColumn : this.classColumn
+            let title = column.map(item => item.title)
+            let key = column.map(item => item.key || item.slot)
+            title.splice(0, sIndex)
+            key.splice(0, sIndex)
+            let data = this._.cloneDeep(this.students)
+            if (this.isStuList) {
+                title.splice(title.length - 1)
+                key.splice(key.length - 1)
+                data.forEach(item => {
+                    item.type = item.type === 2 ? this.$t('cusMgt.schoolType') : this.$t('cusMgt.tmIDType')
+                })
+            }
+            const params = {
+                title,
+                key,
+                data,
+                autoWidth: true,
+                filename: fileName,
+                setTitle: {
+                    title: fileName,
+                    rules: [{//合并第一行数据
+                        s: {//s为开始
+                            c: 0,//开始列
+                            r: 0//开始取值范围
+                        },
+                        e: {//e结束
+                            c: key.length - 1,//结束列
+                            r: 0//结束范围
+                        }
+                    }]
+                }
+            }
+            excel.export_array_to_excel(params)
+        },
+        //删除学生
+        delStudents() {
+            if (this.delSelection.length > 0) {
+                let names = this.delSelection.map(item => {
+                    return item.name
+                })
+                this.$Modal.confirm({
+                    title: this.$t('cusMgt.delStu'),
+                    content: `${this.$t('cusMgt.delStuContent')}${names.join(', ')}?`,
+                    onOk: () => {
+                        this.listLoading = true
+                        let delIds = this.delSelection.map(item => {
+                            return item.id
+                        })
+                        if (this.stuList && this.stuList.members) {
+                            for (let i = 0; i < this.stuList.members.length; i++) {
+                                if (delIds.includes(this.stuList.members[i].id)) {
+                                    this.stuList.members.splice(i, 1)
+                                    i--
+                                }
+                            }
+                            this.$api.common.upsertGroupInfo(this.stuList).then(
+                                res => {
+                                    this.$Message.success(this.$t('cusMgt.delOk'))
+                                    //TODO 更新父组件数据
+                                    this.$emit('on-del-student', {
+                                        stuListId: this.stuList.id,
+                                        delIds: delIds
+                                    })
+                                },
+                                err => {
+                                    this.$Message.error(this.$t('cusMgt.delErr'))
+                                }
+                            ).finally(() => {
+                                this.listLoading = false
+                            })
+                        }
+                    }
+                })
+            } else {
+                this.$Message.warning(this.$t('cusMgt.delStuTips'))
+            }
+        },
+        confirmSetNo(row, rowIndex) {
+            // if (!this.editIrs) {
+            //     this.$Message.warning(this.$t('schoolBaseInfo.setIrsWarning'))
+            //     return
+            // }
+            // //检查irs重复
+            // let irsRep = this.students.some((item, index) => {
+            //     return index != rowIndex && item.irs == this.editIrs
+            // })
+            // if (irsRep) {
+            //     this.$Message.warning(this.$t('schoolBaseInfo.irsRep'))
+            //     return
+            // }
+
+            // // 当前名单对应的课程安排用于更新UI
+            // let schedule = this.courseListShow[this.curCusIndex].schedule.find(item => {
+            //     return item.stulist == this.teaClassList[this.curClassIndex].stulist && !item.classId //只有自定名单能添加学生,所以classId == ‘’
+            // })
+            // let stulist = this.groupList.find(item => {
+            //     return item.id == this.teaClassList[this.curClassIndex].stulist
+            // })
+            // if (stulist) {
+            //     let s = stulist.members.find(item => item.id == row.id)
+            //     s.irs = this.editIrs + ''
+            //     schedule.allStu = this._.cloneDeep(stulist.members)
+            //     this.saveStuList(stulist)
+            // }
+            // this.editIndex = -1
+        },
+        cancelSetInfo() {
+            this.editIrs = null
+            this.editIndex = -1
+        },
+        confirmSetNickName(row, rowIndex) {
+            // if (!this.nickname) {
+            //     this.$Message.warning(this.$t('cusMgt.nicknameTips'))
+            //     return
+            // }
+            // // 当前名单对应的课程安排用于更新UI
+            // let schedule = this.courseListShow[this.curCusIndex].schedule.find(item => {
+            //     return item.stulist == this.teaClassList[this.curClassIndex].stulist && !item.classId
+            // })
+            // let stulist = this.groupList.find(item => {
+            //     return item.id == this.teaClassList[this.curClassIndex].stulist
+            // })
+            // if (stulist) {
+            //     let s = stulist.members.find(item => item.id == row.id)
+            //     s.nickname = this.nickname
+            //     schedule.allStu = this._.cloneDeep(stulist.members)
+            //     this.saveStuList(stulist)
+            // }
+            // this.editIndex = -1
+        },
+    },
+    watch: {
+        classInfo: {
+            deep: true,
+            immediate: true,
+            handler(n, o) {
+                console.log(n)
+            }
+        }
+    }
+}
+</script>
+<style lang="less" scoped>
+@import "./Student.less";
+</style>
+<style lang="less">
+.cus-student-contianer .common-save-btn .ivu-btn {
+    color: #515a6e !important;
+    padding: 0px 5px !important;
+}
+</style>

+ 68 - 0
TEAMModelOS/ClientApp/src/view/mycourse/survey/Survey.less

@@ -0,0 +1,68 @@
+@first-bgColor: #141414;
+@second-bgColor: #1b1b1b;
+@third-bgColor: #222222;
+@primary-textColor: var(--primary-text-color); //文本主颜色
+@second-textColor: var(--second-text-color); //文本副级颜色
+@primary-fontSize: 14px;
+@second-fontSize: 16px;
+
+.hw-container{
+    width: 100%;
+    height: ~"calc(100% - 46px)";
+}
+.survey-item:hover .common-item-icon{
+    display: inline-block;
+}
+.common-item-icon{
+    display: none;
+    font-size: 16px;
+    color: #2d8cf0;
+    vertical-align: baseline;
+    margin-left: 10px;
+    position: absolute;
+}
+.heart-active{
+    display: inline-block;
+    color: #ed4014;
+}
+.ac-action-wrap{
+    position: absolute;
+    right: 15px;
+    top: 10px;
+}
+.action-item{
+    margin-right:15px;
+    vertical-align: middle;
+    cursor: pointer;
+    user-select: none;
+    &:hover{
+        color: #2d8cf0;
+    }
+}
+.survey-item{
+    cursor: pointer;
+    padding: 20px 30px 20px 15px;
+    border-bottom: 1px solid #f0f0f0;
+    display: flex;
+    align-items: center;
+    &:hover{
+        background: var(--hover-text-color);
+    }
+    &:hover .item-action-wrap{
+        display: inline-block;
+    }
+}
+.list-icon{
+    color: var(--normal-icon-color);
+    font-size: 35px;
+    width: 90px;
+}
+.exam-name{
+    color: @primary-textColor;
+}
+.exam-detail-wrap{
+    margin-top: 10px;
+}
+.item-action-wrap{
+    height: 20px;
+}

+ 154 - 0
TEAMModelOS/ClientApp/src/view/mycourse/survey/Survey.vue

@@ -0,0 +1,154 @@
+<template>
+  <div class="hw-container">
+    <div class="ac-action-wrap">
+      <span class="action-item" @click="onCreateAc">
+        <Icon type="md-add" />
+        {{$t('cusMgt.cusTab11')}}
+      </span>
+    </div>
+    <vuescroll>
+      <div class="survey-item" v-for="(item,index) in surveyList" :key="index" @click="toAcDetail(index)">
+        <Icon custom="iconfont icon-vote" class="list-icon" />
+        <div class="exam-info-wrap">
+          <h3 class="exam-name">
+            {{item.name}}
+          </h3>
+          <p class="exam-detail-wrap">
+            <!-- 学校|个人 -->
+            <Tag color="blue">
+              {{ item.owner == 'school' ? $t('cusMgt.school') : $t('cusMgt.private') }}
+            </Tag>
+            <!-- 活动进度状态 -->
+            <Tag :color="item.progress == 'going' ? 'success' : 'warning'">
+              {{ item.progText }}
+            </Tag>
+            <Tag>
+              <Icon type="md-time" size="16" />
+              {{$jsFn.timeFormat(item.startTime)}}
+            </Tag>
+          </p>
+        </div>
+        <div class="item-action-wrap">
+          <span style="float:right;margin-right:20px">
+            <!-- 修改评测名称 -->
+            <Icon v-show="item.owner === 'teacher'" type="md-create" style="right:80px" class="common-item-icon" @click.stop="editEvName(index)" :title="$t('learnActivity.mgtScEv.edName')" />
+            <!-- 删除记录 -->
+            <Icon v-show="item.owner === 'teacher'" type="md-trash" style="right:50px" class="common-item-icon" @click.stop="delEv(item)" :title="$t('cusMgt.delRcd')" />
+            <!-- 收藏 -->
+            <Icon :type="isFavorite(item.id) ? 'md-heart':'md-heart-outline'" style="right:20px" :class="['common-item-icon',isFavorite(item.id) ? 'heart-active':'']" @click.stop="toggleFavorite(item)" :title="isFavorite(item.id) ? $t('cusMgt.unfvt') : $t('cusMgt.fvt')" />
+          </span>
+        </div>
+      </div>
+      <EmptyData v-show="!surveyList.length" :top="150"></EmptyData>
+    </vuescroll>
+  </div>
+</template>
+<script>
+export default {
+  props: {
+    classInfo: {
+      type: Object,
+      default: () => {
+        return {}
+      }
+    }
+  },
+  data() {
+    return {
+      fIds: [],
+      surveyList: []
+    }
+  },
+  methods: {
+    /* 新建活动 跳转活动创建页面 */
+    async onCreateAc() {
+      let isFull = await this.$tools.isBlobContainerFull('private')
+      if (isFull) {
+        this.$Message.warning(this.$t('vote.fullTip'))
+      } else {
+        this.$router.push({
+          name: 'personalSurvey',
+          params: {
+            isAdd: true
+          }
+        })
+      }
+    },
+    isFavorite(id) {
+      return this.fIds.includes(id)
+    },
+    toAcDetail(index) {
+      let owner = this.surveyList[index].owner
+      this.$router.push({
+        name: owner == 'school' ? 'manageQuestionnaire' : 'personalSurvey',
+        params: {
+          ac: this.surveyList[index]
+        }
+      })
+    },
+    getStatusInfo(progress) {
+      let info = {}
+      if (progress == 'pending') {
+        info.progText = this.$t('learnActivity.mgtScEv.pending')
+        info.progColor = '#2d8cf0'
+      } else if (progress == 'going') {
+        info.progText = this.$t('learnActivity.mgtScEv.going')
+        info.progColor = '#19be6b'
+      } else if (progress == 'finish') {
+        info.progText = this.$t('learnActivity.mgtScEv.finish')
+        info.progColor = '#515a6e'
+      }
+      return info
+    },
+    //获取活动列表
+    findTchAc(classId) {
+      this.examList = []
+      this.isShowGrade = false
+      this.isLoading = true
+      let requestData = {
+        school: this.$store.state.userInfo.schoolCode,
+        classes: [classId],
+        userid: this.$store.state.userInfo.TEAMModelId,
+        pk: 'Survey'
+      }
+      this.$api.courseMgmt.findTchAc(requestData).then(
+        res => {
+          if (!res.error) {
+            res.datas.forEach(item => {
+              let statusInfo = this.getStatusInfo(item.progress)
+              item.progText = statusInfo.progText
+              item.progColor = statusInfo.progColor
+            })
+            this.surveyList = res.datas
+          }
+        },
+        err => {
+          // this.$Message.error('API error')
+        }
+      ).finally(() => {
+        this.isLoading = false
+      })
+    },
+  },
+  watch: {
+    classInfo: {
+      deep: true,
+      immediate: true,
+      handler(n, o) {
+        console.log(n)
+        if (n && (n.classId || n.stulist)) {
+          this.findTchAc(n.classId || n.stulist)
+        }
+      }
+    }
+  }
+}
+</script>
+<style lang="less" scoped>
+@import "./Survey.less";
+</style>
+<style lang="less">
+.hw-container .ivu-list-item:hover {
+  background: var(--active-item-start);
+}
+</style>

+ 0 - 0
TEAMModelOS/ClientApp/src/view/mycourse/vote/Vote.less


Some files were not shown because too many files changed in this diff