CrazyIter_Bin 1 rok temu
rodzic
commit
3379a7a1b1
100 zmienionych plików z 52705 dodań i 2090 usunięć
  1. 4 0
      TEAMModelBI/ClientApp/src/api/index.js
  2. 5 3
      TEAMModelBI/ClientApp/src/until/http.js
  3. 41 12
      TEAMModelBI/ClientApp/src/view/issueCoupons/crteadCoupon.vue
  4. 4 0
      TEAMModelBI/ClientApp/src/view/product/details.vue
  5. 199 134
      TEAMModelBI/ClientApp/src/view/product/index.vue
  6. 85 13
      TEAMModelBI/ClientApp/src/view/schoolmanage/schoolAnalyse.vue
  7. 1130 0
      TEAMModelBI/ClientApp/src/view/userInquire/idIncrement.vue
  8. 9 4
      TEAMModelBI/ClientApp/src/view/userInquire/index.vue
  9. 2 2
      TEAMModelBI/Controllers/BIBlob/AnalyseFileController.cs
  10. 4 4
      TEAMModelBI/Controllers/BIHome/OnLineController.cs
  11. 212 7
      TEAMModelBI/Controllers/BIProductAnalysis/ProductAnalysisController.cs
  12. 382 242
      TEAMModelBI/Controllers/BITmid/TmidController.cs
  13. 7 2
      TEAMModelBI/Controllers/Census/SchoolController.cs
  14. 32 0
      TEAMModelBI/Filter/AspNetCoreBuilderServiceCollectionExtensions.cs
  15. 221 0
      TEAMModelBI/Filter/RequestAuditFilter.cs
  16. 12 2
      TEAMModelBI/Models/RearEndMiddle.cs
  17. 2 2
      TEAMModelBI/Startup.cs
  18. 3 3
      TEAMModelBI/TEAMModelBI.csproj
  19. 3 1
      TEAMModelBI/appsettings.Development.json
  20. 22 7
      TEAMModelOS.FunctionV4/CosmosDB/TriggerArt.cs
  21. 8 6
      TEAMModelOS.FunctionV4/CosmosDB/TriggerExam.cs
  22. 37 2
      TEAMModelOS.FunctionV4/HttpTrigger/IESHttpTrigger.cs
  23. 1 0
      TEAMModelOS.FunctionV4/Program.cs
  24. 25 3
      TEAMModelOS.FunctionV4/ServiceBus/ActiveTaskTopic.cs
  25. 9 3
      TEAMModelOS.FunctionV4/TEAMModelOS.FunctionV4.csproj
  26. 77 38
      TEAMModelOS.FunctionV4/TimeTrigger/IESTimerTrigger.cs
  27. 42708 0
      TEAMModelOS.FunctionV4/latlng.json
  28. 18 6
      TEAMModelOS.FunctionV4/local.settings.json
  29. 0 1
      TEAMModelOS.SDK/DI/AzureCosmos/AzureCosmosExtensions.cs
  30. 0 1
      TEAMModelOS.SDK/DI/AzureStorage/Inner/AzureBlobModel.cs
  31. 10 6
      TEAMModelOS.SDK/DI/CoreAPI/CoreAPIHttpService.cs
  32. 30 0
      TEAMModelOS.SDK/DI/CoreAPI/Region2LongitudeLatitudeTranslator.cs
  33. 3 0
      TEAMModelOS.SDK/DI/HttpTrigger/WebHookHttpTrigger.cs
  34. 25 90
      TEAMModelOS.SDK/Helper/Common/DateTimeHelper/DateTimeHelper.cs
  35. 1 2
      TEAMModelOS.SDK/Helper/Common/JsonHelper/JsonParser.cs
  36. 1 1
      TEAMModelOS.SDK/Helper/Common/JsonHelper/JsonPath/IJsonPathValueSystem.cs
  37. 70 0
      TEAMModelOS.SDK/Helper/Common/JsonHelper/JsonPath/JsonNetValueSystem.cs
  38. 1 1
      TEAMModelOS.SDK/Helper/Common/JsonHelper/JsonPath/JsonPathContext.cs
  39. 2 3
      TEAMModelOS.SDK/Helper/Common/JsonHelper/JsonPath/JsonApiValueSystem.cs
  40. 9 5
      TEAMModelOS.SDK/Helper/Network/IP2Region/IPSearcher.cs
  41. 1 0
      TEAMModelOS.SDK/Helper/Network/IP2Region/IPSearcherExtensions.cs
  42. 0 1
      TEAMModelOS.SDK/Helper/Security/RSACrypt/RsaHelper.cs
  43. 8 0
      TEAMModelOS.SDK/Models/Cosmos/BI/CurrencyModel.cs
  44. 68 1
      TEAMModelOS.SDK/Models/Cosmos/BI/RecAppGWInfo.cs
  45. 8 0
      TEAMModelOS.SDK/Models/Cosmos/BI/StatsInfo.cs
  46. 105 2
      TEAMModelOS.SDK/Models/Cosmos/Common/Activity.cs
  47. 6 1
      TEAMModelOS.SDK/Models/Cosmos/Common/ArtEvaluation.cs
  48. 38 0
      TEAMModelOS.SDK/Models/Cosmos/Common/GroupList.cs
  49. 16 0
      TEAMModelOS.SDK/Models/Cosmos/Common/Inner/BaseItem.cs
  50. 5 0
      TEAMModelOS.SDK/Models/Cosmos/Common/Inner/GroupChange.cs
  51. 2 0
      TEAMModelOS.SDK/Models/Cosmos/Common/ItemInfo.cs
  52. 2 2
      TEAMModelOS.SDK/Models/Cosmos/Common/LearnRecord.cs
  53. 4 4
      TEAMModelOS.SDK/Models/Cosmos/School/Course.cs
  54. 20 0
      TEAMModelOS.SDK/Models/Cosmos/School/CourseBase.cs
  55. 1 0
      TEAMModelOS.SDK/Models/Cosmos/School/Debate.cs
  56. 103 0
      TEAMModelOS.SDK/Models/Dtos/Accumulate.cs
  57. 19 9
      TEAMModelOS.SDK/Models/Service/BI/BILogAnalyseService.cs
  58. 24 2
      TEAMModelOS.SDK/Models/Service/Common/ActivityService.cs
  59. 0 1
      TEAMModelOS.SDK/Models/Service/FixDataService.cs
  60. 300 12
      TEAMModelOS.SDK/Models/Service/GroupListService.cs
  61. 0 1
      TEAMModelOS.SDK/Models/Service/LessonService.cs
  62. 10 0
      TEAMModelOS.SDK/Models/Service/OpenApiService.cs
  63. 32 5
      TEAMModelOS.SDK/Models/Service/StudentService.cs
  64. 1793 0
      TEAMModelOS.SDK/Models/Service/SystemService.cs
  65. 3 3
      TEAMModelOS.SDK/Models/Service/Third/ThirdService.cs
  66. 46 46
      TEAMModelOS.SDK/TEAMModelOS.SDK.csproj
  67. 74 0
      TEAMModelOS.TEST/Program.cs
  68. 1 0
      TEAMModelOS/ClientApp/package.json
  69. 1438 0
      TEAMModelOS/ClientApp/public/bill.html
  70. 329 116
      TEAMModelOS/ClientApp/public/lang/en-US.js
  71. 216 3
      TEAMModelOS/ClientApp/public/lang/zh-CN.js
  72. 448 238
      TEAMModelOS/ClientApp/public/lang/zh-TW.js
  73. BIN
      TEAMModelOS/ClientApp/public/login_bg.jpg
  74. 16 13
      TEAMModelOS/ClientApp/src/api/common.js
  75. 2 0
      TEAMModelOS/ClientApp/src/api/http.js
  76. 95 3
      TEAMModelOS/ClientApp/src/assets/iconfont/demo_index.html
  77. 19 3
      TEAMModelOS/ClientApp/src/assets/iconfont/iconfont.css
  78. 1 1
      TEAMModelOS/ClientApp/src/assets/iconfont/iconfont.js
  79. 28 0
      TEAMModelOS/ClientApp/src/assets/iconfont/iconfont.json
  80. BIN
      TEAMModelOS/ClientApp/src/assets/iconfont/iconfont.ttf
  81. BIN
      TEAMModelOS/ClientApp/src/assets/iconfont/iconfont.woff
  82. BIN
      TEAMModelOS/ClientApp/src/assets/iconfont/iconfont.woff2
  83. 1 1
      TEAMModelOS/ClientApp/src/common/BaseClassSelectPri.vue
  84. 44 3
      TEAMModelOS/ClientApp/src/common/BaseLayout.vue
  85. 2 1
      TEAMModelOS/ClientApp/src/common/BaseSelectSchool.vue
  86. 2 1
      TEAMModelOS/ClientApp/src/common/PrivateTargetSingle.vue
  87. 1 1
      TEAMModelOS/ClientApp/src/components/dashboard/art/BasePointLineBar.vue
  88. 222 69
      TEAMModelOS/ClientApp/src/components/dashboard/art/LeftBottom.vue
  89. 2 0
      TEAMModelOS/ClientApp/src/components/dashboard/art/RightTop.vue
  90. 662 690
      TEAMModelOS/ClientApp/src/components/evaluation/ExerciseList.vue
  91. 455 192
      TEAMModelOS/ClientApp/src/components/selflearn/ExerciseList.vue
  92. 117 0
      TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/Cowork.vue
  93. 2 1
      TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/ExamTable.vue
  94. 51 4
      TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/RecordView.vue
  95. 115 8
      TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/ShowQues.vue
  96. 120 36
      TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/SmartRating.vue
  97. 3 3
      TEAMModelOS/ClientApp/src/components/student-web/CourseView/CourseView/ActivityView.vue
  98. 16 7
      TEAMModelOS/ClientApp/src/components/student-web/DiscussionBoard.vue
  99. 195 0
      TEAMModelOS/ClientApp/src/components/student-web/EventView/EventContentTypeTemplate/LessonTestReportCharts/AudioRecorder.vue
  100. 0 0
      TEAMModelOS/ClientApp/src/components/student-web/EventView/EventContentTypeTemplate/LessonTestReportCharts/LessonTestReportCharts.vue

+ 4 - 0
TEAMModelBI/ClientApp/src/api/index.js

@@ -559,6 +559,10 @@ export default {
     getfilterSchool(data) {
         return post('/prodanalysis/get-school', data)
     },
+    //ID歸戶增量統計
+    getTmidUseprod(data) {
+        return post('/tmid/get-tmid-useprod', data)
+    },
     /*产品使用分析end*/
 
     //获取地址location

+ 5 - 3
TEAMModelBI/ClientApp/src/until/http.js

@@ -21,8 +21,7 @@ axios.interceptors.request.use(
             config.url.indexOf('bizconfig') != -1 ||
             config.url.indexOf('paper') != -1 ||
             config.url.indexOf('notice') != -1 ||
-            config.url.indexOf('bizuser') != -1 ||
-            config.url.indexOf('prodanalysis') != -1 
+            config.url.indexOf('bizuser') != -1
         ) {
             config.headers = {
                 'Content-Type': 'application/json',
@@ -33,7 +32,10 @@ axios.interceptors.request.use(
                 'Content-Type': 'application/json',
                 // 'site': 'china'
             }
-        } else if (config.url.indexOf('ies5') != -1 || config.url.indexOf('tmidstics') != -1 || config.url.indexOf('/service/PushNotify') != -1) {
+        } else if (config.url.indexOf('ies5') != -1 ||
+            config.url.indexOf('tmid') != -1 ||
+            config.url.indexOf('/service/PushNotify') != -1 ||
+            config.url.indexOf('prodanalysis') != -1) {
             config.headers = {
                 'Content-Type': 'application/json',
                 'authorization': 'Bearer ' + JSON.parse(localStorage.access_token)

+ 41 - 12
TEAMModelBI/ClientApp/src/view/issueCoupons/crteadCoupon.vue

@@ -9,13 +9,15 @@
                     <el-radio label="China" size="large" border>大陸站</el-radio>
                 </el-radio-group>
             </el-form-item>
-            <el-form-item label="活動票券" prop="eventType">
-                <el-radio-group v-model="eventType" @change="setEventType">
+            <el-form-item v-if="!detailSettingFlag" label="活動票券" prop="eventType">
+                <el-radio-group v-model="eventType" @change="setEventType" style="max-width: 1000px;">
                     <el-radio label="hiteach50-3"  border>Hiteach 50 第三階段</el-radio>
                     <el-radio label="hiteach333-1"  border>HiTeach 333 第一階段</el-radio>
                     <el-radio label="hiteach333-3"  border>HiTeach 333 第三階段</el-radio>
                     <el-radio label="hiteachBookSPLicense"  border>HiTeach專書贈送授權</el-radio>
                     <el-radio label="fbGroupOpening"  border>HiTeach社團開幕活動</el-radio>
+                    <el-radio label="giftWebIRS501M"  border>贈送WEBIRS50一個月</el-radio>
+                    <el-radio label="giftSmart1Y"  border>贈送智慧評分一年</el-radio>
                 </el-radio-group>
             </el-form-item>
             <el-form-item v-if="detailSettingFlag" label="票券類型" prop="couponType" >
@@ -94,8 +96,8 @@
                             </div>
                         </template>
                         <div style="text-align: left;">
-                            票券標題:<el-input v-model="info.n" :readonly="!detailSettingFlag"/>
-                            連結:<el-input v-model="info.u" :readonly="!detailSettingFlag"/>
+                            票券標題:<el-input v-model="info.n"/>
+                            連結:<el-input v-model="info.u"/>
                         </div>
                         <template #footer>
                             <div style="display: flex;justify-content: center;">
@@ -226,7 +228,7 @@ const setEventType = (type)=>{
                     val:''
                 }
             ]
-            crtCouponForm.rule[0].b = '901003'
+            crtCouponForm.rule[0].b = '901009'
             crtCouponForm.info[0].l = 'zh-tw' 
             crtCouponForm.info[0].n = '教師自主增能三'
             crtCouponForm.info[0].u = 'https://www.habook.com/zh-tw/event.php?act=view&id=170'
@@ -321,6 +323,28 @@ const setEventType = (type)=>{
             crtCouponForm.info[2].n = 'Facebook Group Opening'
             crtCouponForm.info[2].u = 'https://www.habook.com/en/'
         break
+        case 'giftWebIRS501M':
+            crtCouponForm.couponType =  'Event'
+            crtCouponForm.eventName = 'giftWebIRS501M'
+            crtCouponForm.rule[0].b = '901004'
+            crtCouponForm.info[0].l = 'zh-tw' 
+            crtCouponForm.info[0].u = 'https://www.habook.com/zh-tw'
+            crtCouponForm.info[1].l = 'zh-cn'
+            crtCouponForm.info[1].u = 'https://www.habook.com.cn/'
+            crtCouponForm.info[2].l = 'en-us'
+            crtCouponForm.info[2].u = 'https://www.habook.com/en/'
+        break
+        case 'giftSmart1Y':
+            crtCouponForm.couponType =  'Event'
+            crtCouponForm.eventName = 'giftSmart1Y'
+            crtCouponForm.rule[0].b = '901005'
+            crtCouponForm.info[0].l = 'zh-tw' 
+            crtCouponForm.info[0].u = 'https://www.habook.com/zh-tw'
+            crtCouponForm.info[1].l = 'zh-cn'
+            crtCouponForm.info[1].u = 'https://www.habook.com.cn/'
+            crtCouponForm.info[2].l = 'en-us'
+            crtCouponForm.info[2].u = 'https://www.habook.com/en/'
+        break        
     }
 }
 
@@ -361,8 +385,8 @@ const howList = [
 ]
 
 const ruleBList = [
-    { label: "AI文字分析模組(展延)", val: "901001"},
-    { label: "AI蘇格拉底小數據(展延)", val: "901002"},
+    { label: "AI文字分析模組(一年)", val: "901001"},
+    { label: "AI蘇格拉底小數據(一年)", val: "901002"},
     { label: "Web IRS 50人 (展延3個月)", val: "901003"},
     { label: "Web IRS 50人 (延長1個月)", val: "901004"},
     { label: "智慧評分 (一年)", val: "901005"},
@@ -372,7 +396,8 @@ const ruleBList = [
     { label: "HiTeachCC-200連線數(展延)", val: "903004"},
     { label: "小組協作(一年)", val: "901006"},
     { label: "AI GPT(一年)", val: "901007"},
-    { label: "智慧評分 (三個月)", val: "901008"}
+    { label: "智慧評分 (三個月)", val: "901008"},
+    { label: "Web IRS 50人(3個月+75天- 50券STEP3專用)", val: "901009"}    
 ]
 
 const crtCouponForm = reactive({
@@ -511,10 +536,14 @@ const linkToPage = (url) => {
 
 // 日曆元件限制設定
 const disabledDate = time => {
-    if(time.getTime() > (Date.now() + 7776000000)){ // 不能超過3個月
-        return true
-    } else if(time.getTime() < Date.now()- 3600 * 1000 * 24){
-        return true
+    if(!detailSettingFlag.value) {
+        if(time.getTime() > (Date.now() + 7776000000)){ // 不能超過3個月
+            return true
+        } else if(time.getTime() < Date.now()- 3600 * 1000 * 24){
+            return true
+        }
+    } else {
+        true
     }
 }
 

+ 4 - 0
TEAMModelBI/ClientApp/src/view/product/details.vue

@@ -800,6 +800,10 @@ function init (againvalue) {
   console.log(propsbox, '数据')
   //学校基础信息
   let { name, region, province, city, dist } = propsbox.detailsData.school
+  //如果沒有學校名稱 改為用城市區名
+  if(propsbox.detailsData.school.name === "" || propsbox.detailsData.school.name === null || propsbox.detailsData.school.name === undefined){
+    name = propsbox.detailsData.name;
+  }  
   schoolData.value.name = appearState.value ==='default' ? name: appearState.value ==='area' ? propsbox.detailsData.name:''
   schoolData.value.region = region
   schoolData.value.province = province

+ 199 - 134
TEAMModelBI/ClientApp/src/view/product/index.vue

@@ -166,10 +166,10 @@
         </div>
       </div>
     </div>
-    <div class="data-tables">
+    <div class="data-tables" v-loading="searchLoading" v-if="showState==='default'"  element-loading-text="正在准备数据中...">
       <el-auto-resizer>
         <template #default="{ height, width }">
-          <el-table-v2 v-model:sort-state="sortState" :columns="columns" :data="filterdata" :width="width" :height="height" @column-sort="onSort" fixed />
+          <el-table-v2 v-model:sort-state="sortState" :columns="columns" :data="filterdata" :width="width" :height="height" @column-sort="onSort" fixed  :row-class="rowClassName" />
         </template>
       </el-auto-resizer>
     </div>
@@ -329,7 +329,7 @@
 import option_cn from '@/static/regions/region_cn.json'
 import option_gl from '@/static/regions/region_gl.json'
 import { ref, getCurrentInstance, watch, h, nextTick } from 'vue'
-import { ElMessage, TableV2SortOrder, ElLoading, ElCheckbox,HeaderCellSlotProps,ElPopover} from 'element-plus'
+import { ElMessage, TableV2SortOrder, ElLoading, ElCheckbox,HeaderCellSlotProps,ElPopover,Column, RowClassNameGetter} from 'element-plus'
 import { Filter } from '@element-plus/icons'
 import { multipleSheetExport } from '@/until/multipleSheetExport'
 import { Search, CirclePlus } from '@element-plus/icons-vue'
@@ -345,6 +345,8 @@ let activeNames = ref(['1'])
 let showState = ref('default')
 let findValue = ref()
 let test = ref([])
+let searchLoading=ref(false)
+
 let columns = ref([
   {
     key: "name",
@@ -355,6 +357,15 @@ let columns = ref([
     // sortable: true,
     headerClass: 'general',
   },
+  {
+    key: "date",
+    dataKey: "date",//需要渲染当前列的数据字段,如{id:9527,name:'Mike'},则填id
+    title: "時間",//显示在单元格表头的文本
+    width: 100,//当前列的宽度,必须设置
+    fixed: false,//是否固定列
+     sortable: true,
+    headerClass: 'general',
+  },
   {
     key: "schoolId",
     dataKey: "schoolId",//需要渲染当前列的数据字段,如{id:9527,name:'Mike'},则填id
@@ -778,7 +789,12 @@ const monthOptions=function disabledDate(time){
                 const elTimeData = timeyear.toString() + timemonth.toString();
                 return elTimeData >= nowDate; 
 }
-
+function rowClassName({rowIndex}) {  
+      if (filterdata.value[rowIndex].geoInfo) {
+        return 'highlight-row'; // 对第二行应用特定样式
+      }
+      return ''; // 其他行不做特殊处理
+    }
 function changeState (value) {
   console.log(value)
   showState.value = 'details'
@@ -798,7 +814,7 @@ function dataInit () {
   initLoading.value=true
   let dataList = []
   proxy.$api.getCapacity({}).then((res) => {
-    console.log(res, 'areList')
+    console.log(res, 'areList')    
     dataSource.value.area = res.areas
     res.areas.forEach((item) => {
       dataList.push({ id: item.id, name: item.name, province: item.provName, city: item.cityName, children: [] })
@@ -815,7 +831,7 @@ function dataInit () {
         })
       })
       console.log(dataList, 'result!')
-      options.value = dataList
+      options.value = dataList      
       dataSource.value.composite = dataList
       dataSource.value.originalSchool = res.scInfos
       tableData.value = res.scInfos
@@ -831,156 +847,181 @@ function dataInit () {
     ElMessage.error('API异常,基础数据获取异常')
   })
 }
- function serachToresult (startTime, endTime, product, schools, unit) {
+function serachToresult(startTime, endTime, product, schools, unit) {
   // let data = { "dateFrom": "2023-04-12", "dateTo": "2023-04-19", "prod": "HiTeach", "schoolIds": ["tbslgb", "habook"], "dateUnit": "Day" }
-  if(!startTime || !endTime){
+  if (!startTime || !endTime) {
     ElMessage.info('请选择时间进行数据搜索')
     return
   }
+  searchLoading.value = true;
   let data = { "dateFrom": startTime, "dateTo": endTime, "prod": product, "schoolIds": schools, "dateUnit": unit }
   console.log(data, '内容')
   console.log(clickNum.value.time, '数字')
-  proxy.$api.getUseproduct(data).then(async(res) => {
+  proxy.$api.getUseproduct(data).then(async (res) => {
     console.log(res, 'backPromise')
-    console.log(clickNum.value.subject,'选的数字')
+    console.log(clickNum.value.subject, '选的数字')
     //区别学校or学区呈现数据
-    if(clickNum.value.subject === 0){
-      let adds={
-      key: "schoolId",
-      dataKey: "schoolId",//需要渲染当前列的数据字段,如{id:9527,name:'Mike'},则填id
-      title: "简码",//显示在单元格表头的文本
-      width: 100,//当前列的宽度,必须设置
-      headerClass: 'general',
+    if (clickNum.value.subject === 0) {
+      let adds = {
+        key: "schoolId",
+        dataKey: "schoolId",//需要渲染当前列的数据字段,如{id:9527,name:'Mike'},则填id
+        title: "简码",//显示在单元格表头的文本
+        width: 100,//当前列的宽度,必须设置
+        headerClass: 'general',
+      }
+      let resultA = columns.value.findIndex((item) => { return item.key == 'schoolId' })
+      console.log(resultA, '查找')
+      resultA === -1 ? columns.value.splice(1, 0, adds) : ''
+      cellWidth.value = (100 / columns.value.length).toFixed(2)
+      if (res.state === 200) {
+        res.data.forEach((item) => { item.name = item.school.name ? item.school.name : '暂无' });
+        res.geo.forEach((item) => {
+
+          if (item.school.name || (item.school.name === "")) {
+            item.name = item.school.name;
+          } else if (item.geoInfo) {
+            item.name = item.geoInfo;
+          } else { item.name = '暂无'; }
+        });
+
+
+        filterdata.value = [...res.geo, ...res.data];
+        primevalData.value = [...res.geo, ...res.data];
       }
-      let resultA=columns.value.findIndex((item)=>{return item.key =='schoolId'})
-      console.log(resultA,'查找')
-      resultA === -1 ? columns.value.splice(1,0,adds):''
-      cellWidth.value=(100 / columns.value.length).toFixed(2)
-      res.state === 200 ? (res.data.forEach((item) => { item.name = item.school.name ? item.school.name : '暂无' }), filterdata.value = res.data,primevalData.value=res.data) : ''
-    }else if(clickNum.value.subject === 1){
+      //res.state === 200 ? (res.data.forEach((item) => { item.name = item.school.name ? item.school.name : '暂无' }), filterdata.value = res.data,primevalData.value=res.data) : ''
+    } else if (clickNum.value.subject === 1) {
       console.log(optionsValue.value)
-      primevalData.value=res.data
-      let allDatas=primevalData.value
-      let catalogueArr=[];let resultArr=[]
-      if(clickNum.value.time === 1){   //做学区 月 的数据累加
-      //时间分类
-       let multiDimensionalArray = allDatas.reduce((acc, curr) => {
-        let { year, month } = curr;
-        let yearIndex = acc.findIndex(item => item[0] === year);
-        if (yearIndex === -1) {
-          acc.push([year, [month]]);
-        } else {
-          acc[yearIndex][1].push(month);
-        }
-        return acc;
-      }, []);
-      console.log(multiDimensionalArray,'结果')
-      multiDimensionalArray.forEach((item)=>{
-        let yearValue=item[0]
-        item[1]=[...new Set(item[1])]
-        let monthDats=item[1]
-        item[1].forEach((itemMonth)=>{
-          for(let i=0;i< optionsValue.value.length;i++){
-            let areaIdTime=[]
-            let areaIdValue=optionsValue.value[i]
-            res.data.forEach((itema)=>{
-              itema.school.areaId === areaIdValue && itema.year ===yearValue && itema.month ===itemMonth ? areaIdTime.push(itema):''
-            })
-            catalogueArr.push(areaIdTime)
+      primevalData.value = [...res.geo, ...res.data]
+      let allDatas = primevalData.value
+      let catalogueArr = []; let resultArr = []
+      if (clickNum.value.time === 1) {   //做学区 月 的数据累加
+        //时间分类
+        let multiDimensionalArray = allDatas.reduce((acc, curr) => {
+          let { year, month } = curr;
+          let yearIndex = acc.findIndex(item => item[0] === year);
+          if (yearIndex === -1) {
+            acc.push([year, [month]]);
+          } else {
+            acc[yearIndex][1].push(month);
           }
+          return acc;
+        }, []);
+        console.log(multiDimensionalArray, '结果')
+        multiDimensionalArray.forEach((item) => {
+          let yearValue = item[0]
+          item[1] = [...new Set(item[1])]
+          let monthDats = item[1]
+          item[1].forEach((itemMonth) => {
+            for (let i = 0; i < optionsValue.value.length; i++) {
+              let areaIdTime = []
+              let areaIdValue = optionsValue.value[i]
+              res.data.forEach((itema) => {
+                itema.school.areaId === areaIdValue && itema.year === yearValue && itema.month === itemMonth ? areaIdTime.push(itema) : ''
+              })
+              catalogueArr.push(areaIdTime)
+            }
+          })
         })
-      })
-      catalogueArr=catalogueArr.filter((items)=>{return items.length>0})
-        console.log(catalogueArr,'进行时间分类区分')
-        for(let i in catalogueArr){
-          let result=await someDataMerge(catalogueArr[i])
+        catalogueArr = catalogueArr.filter((items) => { return items.length > 0 })
+        console.log(catalogueArr, '进行时间分类区分')
+        for (let i in catalogueArr) {
+          let result = await someDataMerge(catalogueArr[i])
           resultArr.push(result)
         }
-      console.log(resultArr,'月 结果')
-      // let newArray=[]
-      // let resultArr=[]
-      //  for(let i=0;i< optionsValue.value.length;i++){
-      //    let arrays=[]
-      //    res.data.forEach((item)=>{
-      //     item.school.areaId === optionsValue.value[i] ? arrays.push(item):''
-      //    })
-      //    newArray.push(arrays)
-      //  }
-      //  newArray=newArray.filter(item => item.length > 0)
-      //  console.log(newArray,'处理前的newArray')
-      //  for(let i in newArray){
-      //    let result=await someDataMerge(newArray[i])
-      //    resultArr.push(result)
-      //  }
-      //  console.log(newArray,'得出的结果')
-      //  resultArr.forEach((item)=>{
-      //   let areIds=item.school.length >0 ? item.school[0].areaId:''
-      //   if(areIds){
-      //     options.value.forEach((items)=>{
-      //     items.id === areIds ? item.name=items.name:''
-      //     })
-      //   }
-      //   //去重schollId
-      //   item.schoolId=[...new Set(item.schoolId)]
-      //  })
-      // console.log(resultArr,'集合')
-      }else if(clickNum.value.time === 0){   //处理 学区  天 的结果 
+        console.log(resultArr, '月 结果')
+        // let newArray=[]
+        // let resultArr=[]
+        //  for(let i=0;i< optionsValue.value.length;i++){
+        //    let arrays=[]
+        //    res.data.forEach((item)=>{
+        //     item.school.areaId === optionsValue.value[i] ? arrays.push(item):''
+        //    })
+        //    newArray.push(arrays)
+        //  }
+        //  newArray=newArray.filter(item => item.length > 0)
+        //  console.log(newArray,'处理前的newArray')
+        //  for(let i in newArray){
+        //    let result=await someDataMerge(newArray[i])
+        //    resultArr.push(result)
+        //  }
+        //  console.log(newArray,'得出的结果')
+        //  resultArr.forEach((item)=>{
+        //   let areIds=item.school.length >0 ? item.school[0].areaId:''
+        //   if(areIds){
+        //     options.value.forEach((items)=>{
+        //     items.id === areIds ? item.name=items.name:''
+        //     })
+        //   }
+        //   //去重schollId
+        //   item.schoolId=[...new Set(item.schoolId)]
+        //  })
+        // console.log(resultArr,'集合')
+      } else if (clickNum.value.time === 0) {   //处理 学区  天 的结果 
         //时间分类
         let uniqueNames = [...new Set(allDatas.map(item => item.date))];
-        uniqueNames.forEach((item)=>{
-          for(let i=0;i< optionsValue.value.length;i++){
-            let areaIdTime=[]
-            let areaIdValue=optionsValue.value[i]
-            res.data.forEach((itema)=>{
-              itema.school.areaId === areaIdValue && itema.date ===item ? areaIdTime.push(itema):''
+        uniqueNames.forEach((item) => {
+          for (let i = 0; i < optionsValue.value.length; i++) {
+            let areaIdTime = []
+            let areaIdValue = optionsValue.value[i]
+            res.data.forEach((itema) => {
+              itema.school.areaId === areaIdValue && itema.date === item ? areaIdTime.push(itema) : ''
             })
             catalogueArr.push(areaIdTime)
           }
         })
-        catalogueArr=catalogueArr.filter((items)=>{return items.length>0})
-        console.log(catalogueArr,'进行时间分类区分')
-        for(let i in catalogueArr){
-          let result=await someDataMerge(catalogueArr[i])
+        catalogueArr = catalogueArr.filter((items) => { return items.length > 0 })
+        console.log(catalogueArr, '进行时间分类区分')
+        for (let i in catalogueArr) {
+          let result = await someDataMerge(catalogueArr[i])
           resultArr.push(result)
         }
-        console.log(resultArr,'天 结果')
-        filterdata.value=resultArr
-      }else if(clickNum.value.time === 2){   //学区  年
+        console.log(resultArr, '天 结果')
+        filterdata.value = [...res.geo, ...resultArr];
+        //filterdata.value=resultArr
+      } else if (clickNum.value.time === 2) {   //学区  年
         //时间分类
         let uniqueYear = [...new Set(allDatas.map(item => item.year))];
-        uniqueYear.forEach((item)=>{
-          for(let i=0;i< optionsValue.value.length;i++){
-            let areaIdTime=[]
-            let areaIdValue=optionsValue.value[i]
-            res.data.forEach((itema)=>{
-              itema.school.areaId === areaIdValue && itema.year ===item ? areaIdTime.push(itema):''
+        uniqueYear.forEach((item) => {
+          for (let i = 0; i < optionsValue.value.length; i++) {
+            let areaIdTime = []
+            let areaIdValue = optionsValue.value[i]
+            res.data.forEach((itema) => {
+              itema.school.areaId === areaIdValue && itema.year === item ? areaIdTime.push(itema) : ''
             })
             catalogueArr.push(areaIdTime)
           }
         })
-        catalogueArr=catalogueArr.filter((items)=>{return items.length>0})
-        console.log(catalogueArr,'进行时间分类区分')
-        for(let i in catalogueArr){
-          let result=await someDataMerge(catalogueArr[i])
+        catalogueArr = catalogueArr.filter((items) => { return items.length > 0 })
+        console.log(catalogueArr, '进行时间分类区分')
+        for (let i in catalogueArr) {
+          let result = await someDataMerge(catalogueArr[i])
           resultArr.push(result)
         }
-       }
-         //数据标准 学区名称
-         resultArr.forEach((item)=>{
-          console.log(item)
-          let areas=item.school[0].areaId
-          options.value.forEach((items)=>{
-            items.id === areas ? item.name=items.name:''
-          })
+      }
+      //数据标准 学区名称
+      resultArr.forEach((item) => {
+        console.log(item)
+        let areas = item.school[0].areaId
+        options.value.forEach((items) => {
+          items.id === areas ? item.name = items.name : ''
         })
-        filterdata.value=resultArr
+      })
+      filterdata.value = [...res.geo, ...resultArr];
+      //filterdata.value=resultArr
       //处理table表header内容
-      let resultColumns=columns.value.findIndex((item)=>{return  item.key==='schoolId'})
-      resultColumns !==-1 ? columns.value.splice(1,1):''
-      cellWidth.value=(100 / columns.value.length).toFixed(2)
-    }else{
-      filterdata.value=res.data
+      let resultColumns = columns.value.findIndex((item) => { return item.key === 'schoolId' })
+      resultColumns !== -1 ? columns.value.splice(1, 1) : ''
+      cellWidth.value = (100 / columns.value.length).toFixed(2)
+    } else {
+      filterdata.value = [...res.geo, ...res.data];
+      //filterdata.value=res.data
     }
+    filterdata.value.forEach((item) => {
+      if ((item.name === null || item.name === "" || item.name === undefined) && item.geoInfo !== null && item.geoInfo !== "" && item.geoInfo !== undefined) {
+        item.name = item.geoInfo;
+      }
+    })
+    searchLoading.value = false;
   }).catch((err) => {
     ElMessage.error('API异常,数据获取失败')
   })
@@ -1000,15 +1041,15 @@ async function searchData () {
   let dateUnits = ''
   let times = { start: productData.value.timevalue[0], end: productData.value.timevalue[1] }
   let yearValues=''
-  clickNum.value.time === 2 ? (yearValues=productData.value.timevalue.slice(0,4),times.start=productData.value.timevalue,times.end=yearValues+'-12-31'):''
-  if (clickNum.value.filter === 0 && searchValue) {
-    if (clickNum.value.subject === 0) {
+  clickNum.value.time === 2 ? (yearValues=productData.value.timevalue.slice(0,4),times.start=productData.value.timevalue,times.end=yearValues+'-12-31'):'' 
+  if (clickNum.value.filter === 0 && searchValue) { // 篩選類型 => 來源類型
+    if (clickNum.value.subject === 0) {// 目标范围 => 學校     
       console.log(searchValue, '进入1')
       searchValue.forEach((item) => {
         schoolArr.push(item[1])
       })
       console.log(schoolArr, '结果0')
-    } else if (clickNum.value.subject === 1) {
+    } else if (clickNum.value.subject === 1) { // 目标范围 => 學區              
       searchValue.forEach((item) => {
         let ids = item
         dataSource.value.composite.forEach((itema) => {
@@ -1016,16 +1057,16 @@ async function searchData () {
         })
       })
       console.log(schoolArr, '结果1')
-    } else if (clickNum.value.subject === 2) {
+    } else if (clickNum.value.subject === 2) { // 目标范围 => 沒用到        
       let state = ''
       typeof optionsValue.value == 'string' ? state = 'province' : state = 'city'
       let resultData = await filterDistrict(state, optionsValue.value)
       console.log(resultData, '结果2')
       schoolArr = resultData
     }
-  } else if (clickNum.value.filter === 1 && searchValue) {
+  } else if (clickNum.value.filter === 1 && searchValue) {// 篩選類型 => 地區城市    
     console.log(clickNum.value.district, '999999')
-    if (clickNum.value.district !== 2) {
+    if (clickNum.value.district !== 2) {// 地区选择 => 不等於學區(省或是城市)
       console.log(optionsValue.value, '城市关键值')
       console.log(typeof optionsValue.value, 'type')
       let state = ''
@@ -1033,7 +1074,7 @@ async function searchData () {
       let resultData = await filterDistrict(state, optionsValue.value)
       schoolArr = resultData
       console.log(resultData, state, '省级查询及状态')
-    } else {
+    } else {// 地区选择 => 學區
       searchValue.forEach((item) => {
         let ids = item
         dataSource.value.composite.forEach((itema) => {
@@ -1430,10 +1471,12 @@ function someDataMerge(arr){
         month:0,
         day:0,
     }
-    const mergedData = arr.reduce((acc, cur) => {
+    
+  const mergedData = arr.reduce((acc, cur) => {
     // 合并对象的基本信息
     acc.deviceAuth += cur.deviceAuth;
-    acc.deviceCnt +=cur.deviceCnt
+    //acc.deviceCnt +=cur.deviceCnt
+    acc.deviceCnt += 0
     acc.deviceNoAuth +=cur.deviceNoAuth
     acc.interact +=cur.interact
     acc.item +=cur.item
@@ -1454,7 +1497,8 @@ function someDataMerge(arr){
     acc.stuLessonLengMin +=cur.stuLessonLengMin
     acc.stuShow +=cur.stuShow
     acc.tGreen +=cur.tGreen
-    acc.tmidCnt +=cur.tmidCnt
+    //acc.tmidCnt +=cur.tmidCnt
+    acc.tmidCnt += 0
     acc.toolType='HiTeach'
     acc.date=cur.date
     acc.year=cur.year
@@ -1479,13 +1523,29 @@ function someDataMerge(arr){
     // 返回累加器对象
     return acc;
   },tatalArr)
+  // 教室數及教師數去重處理
+  let arrdeviceCnt = [];
+  let arrtmidCnt = [];  
+  arr.forEach((arritem) => {
+    arritem.deviceList.forEach((item) => {
+      arrdeviceCnt.push(item);
+    })
+    arritem.tmidList.forEach((item) => {
+      arrtmidCnt.push(item);
+    })
+  })  
+  const uniquedcArray = Array.from(new Set(arrdeviceCnt));
+  const uniquetiArray = Array.from(new Set(arrtmidCnt));
+  mergedData.deviceCnt = uniquedcArray.length;
+  mergedData.tmidCnt = uniquetiArray.length;
+  
   return mergedData
 }
 //数据筛选
 function filterSchooltype(){
   let filterValue=filterType.value
   let dataArr=primevalData.value
-  let result=[]
+  let result=[]  
   console.log(primevalData.value,'原始值')
   console.log(filterValue,'状态值')
   filterValue.totalShow ? result=primevalData.value:result=dataArr.filter((item)=>{return item.schoolId !=="allschool"})
@@ -1872,5 +1932,10 @@ watch(clickNum, (newv) => {
 .dialog-teachlist .el-dialog__header{
   padding: 5px;
 
+}
+.highlight-row {
+  /* background-color: #ffcccc; 特定行的背景色 */
+  background-color: #c6e2ff; /* 特定行的背景色 */
+  
 }
 </style>

+ 85 - 13
TEAMModelBI/ClientApp/src/view/schoolmanage/schoolAnalyse.vue

@@ -215,10 +215,30 @@
       </div>
     </div>
     <div class="schoolList-all">
-      <div class="school-list-title">学校列表:</div>
+      <div class="school-list-title">学校列表:</div>    
       <div class="school-search">
+        <el-select  v-model="selectCity" placeholder="篩選城市" style="width: 240px;margin-bottom: 10px;display: flex;" @change="personnelSearch">
+         <el-option
+           v-for="item in schoolCitys"
+           :key="item.city"
+           :label="item.city"
+           :value="item.city"
+         />                 
+       </el-select>                             
         <el-input v-model="searchText.values" placeholder="搜索 学校名称/学校简码" class="input-with-select" size="small" clearable />
       </div>
+      <!-- <div >        
+        <el-select  v-model="selectCity" placeholder="篩選城市" style="width: 240px" @change="personnelSearch">
+         <el-option
+           v-for="item in schoolCitys"
+           :key="item.city"
+           :label="item.city"
+           :value="item.city"
+         />                 
+       </el-select>            
+      </div> -->
+      
+      
       <div class="school-tables" style="width: 100%; height: 60vh" v-loading="searchText.loading" element-loading-background="rgba(0, 0, 0, 0.5)">
         <!-- <el-table :data="schoolDefault" height="55vh" style="width: 100%" v-loading="searchText.loading" element-loading-background="rgba(0, 0, 0, 0.5)" empty-text="暂无数据">
           <el-table-column label="校徽" align="center">
@@ -373,7 +393,7 @@
     </div>
   </div>
 </template>
-<script>
+<script >
 import { ref, reactive, onMounted, getCurrentInstance, watch } from 'vue'
 import Bar from '@/components/echarts/commonBar.vue'
 import CommonLine from '@/components/echarts/commonLine.vue'
@@ -383,9 +403,12 @@ import Gradepie from '@/components/echarts/gradePie.vue'
 import ConventionPie from '@/components/echarts/conventionPie.vue'
 import { useRouter } from 'vue-router'
 import { useStore } from 'vuex'
-import { ElMessage, ElLoading } from 'element-plus'
+import { ElMessage, TableV2SortOrder, ElLoading, ElCheckbox,HeaderCellSlotProps,ElPopover,Column, RowClassNameGetter} from 'element-plus'
+import { Filter } from '@element-plus/icons'
+import { Search, CirclePlus } from '@element-plus/icons-vue'
 import jwt_decode from 'jwt-decode'
 import * as echarts from 'echarts'
+import { unique } from 'element-plus/es/utils'
 export default {
   components: {
     Bar,
@@ -396,6 +419,9 @@ export default {
     CommonBar
   },
   setup () {
+    const selectCity = ref('')
+    let schoolCitys = reactive([{city:'篩選城市'}])    
+    //let schoolDefault = ref([])
     let showState = ref('all')
     let router = useRouter()
     let { proxy } = getCurrentInstance()
@@ -445,7 +471,7 @@ export default {
         // key: "name",
         // dataKey: "name",//需要渲染当前列的数据字段,如{id:9527,name:'Mike'},则填name
         title: "校徽",
-        width: 400,
+        width: 300,
         fixed: false,
         align: 'center',
         cellRenderer: (data) => (
@@ -455,11 +481,19 @@ export default {
           </>
         )
       },
+      {
+        key: "city",
+        dataKey: "city",//需要渲染当前列的数据字段,如{id:9527,name:'Mike'},则填name
+        title: "城市",
+        width: 300,
+        fixed: false,
+        align: 'center',
+      },
       {
         key: "name",
         dataKey: "name",//需要渲染当前列的数据字段,如{id:9527,name:'Mike'},则填name
         title: "名称",
-        width: 400,
+        width: 300,
         fixed: false,
         align: 'center',
       },
@@ -483,12 +517,12 @@ export default {
         key: "schoolId",
         dataKey: "schoolId",//需要渲染当前列的数据字段,如{id:9527,name:'Mike'},则填id
         title: "学校简码",//显示在单元格表头的文本
-        width: 400,//当前列的宽度,必须设置
+        width: 300,//当前列的宽度,必须设置
         align: 'center',
       },
       {
         title: "",
-        width: 400,
+        width: 300,
         align: "center",
         // fixed: 'right',
         cellRenderer: (data) =>
@@ -1788,6 +1822,35 @@ export default {
           })
           allLoding.value.rankData = false
           schoolDefault.value = res.scInfos
+          
+          let cityarr = [];
+          res.scInfos.forEach((item) =>{
+            let citem = {
+              city:item.city
+            }
+            cityarr.push(citem);
+          })
+          
+           //cityarr = [...new Set(cityarr)];
+           cityarr = [...new Set(cityarr.map(item => item.city))];
+           
+          cityarr.forEach((item) => {
+            if (item !== "" && item !== null && item !== undefined) {
+              let citem = {
+                city: item
+              }
+              schoolCitys.push(citem)
+            }
+          })          
+
+          // //schoolCitys.value = cityarr;
+          // schoolCitys = [
+          //   {city:'111'},
+          //   {city:'222'},
+          //   {city:'333'},
+          // ]
+          
+
           searchText.original = res.scInfos
           searchText.loading = false
           serviceVersion(res.productGroup, res.scInfos)
@@ -1807,7 +1870,7 @@ export default {
       }).catch((error) => {
         ElMessage.error('数据获取API异常')
       })
-    }
+    }    
     function allAspects (val, state, behinddata) {
       console.log(val, state, behinddata)
       //let data = state === 'details' ? { schoolId: val } : { tmdId: val }
@@ -1969,8 +2032,7 @@ export default {
       // })
     }
     function detailsSchool (val) {
-      console.log(val);
-      // debugger;
+      console.log(val);      
       let schoolData = val
       schoolDeatilsInfo(schoolData.schoolId)
       showState.value = 'particular'
@@ -2359,6 +2421,14 @@ export default {
       let newArr = arr.filter((item) => {
         return item.name.includes(searchText.values) || item.id.includes(searchText.values)
       })
+      debugger
+      if (selectCity.value !== "" && selectCity.value !== "篩選城市") {
+        newArr = newArr.filter((item) => {
+          if (item.city !== null && item.city !== undefined && item.city !== "") {
+            return item.city.includes(selectCity.value)
+          }
+        })
+      }      
       schoolDefault.value = newArr
     }
     showInit()
@@ -2367,13 +2437,15 @@ export default {
       newdata.hasOwnProperty('name') ? (showState.value = 'particular', schoolDeatils.value = newdata, schoolDeatilsInfo(newdata.id), isAreaon.value = true) : ''
     }, { immediate: true, deep: true })
     watch(() => searchText.values, (newvalues) => {
-      if (newvalues.trim().length !== 0) {
+      if (newvalues.trim().length !== 0 || selectCity.value !== "篩選城市") {
         debounce(personnelSearch, 500)
-      } else {
+      } else {        
         schoolDefault.value = searchText.original
       }
     })
-    return {
+    return {            
+      schoolCitys,
+      selectCity,
       headerbasics,
       echartsData,
       imgdata,

Plik diff jest za duży
+ 1130 - 0
TEAMModelBI/ClientApp/src/view/userInquire/idIncrement.vue


+ 9 - 4
TEAMModelBI/ClientApp/src/view/userInquire/index.vue

@@ -3,8 +3,8 @@
         <div class="inquire-title">
             <p>TEAM Model 智慧教育</p>
         </div>
-        <el-tabs v-if="pageShow ==='default'" style="width: 100%;height:100%;" type="card" class="demo-tabs">
-            <el-tab-pane label="用户查询" style="padding: 1%;display: flex;justify-content: center;">
+        <el-tabs v-if="pageShow ==='default'" style="width: 100%;height:100%;" type="card" class="demo-tabs" v-model="activeTab">
+            <el-tab-pane label="用户查询" style="padding: 1%;display: flex;justify-content: center;" name="tab1">
                 <div class="searchbox" v-loading="searchLoading" element-loading-text="数据搜索中...">
                     <div class="searchbox-title">
                         <p>用户查询</p>
@@ -39,9 +39,12 @@
                     </div>
                 </div>
             </el-tab-pane>
-            <el-tab-pane label="弱歸戶" style="padding: 1%">
+            <el-tab-pane label="弱歸戶" style="padding: 1%" name="tab2">
                 <UpdCodeW />
             </el-tab-pane>
+            <el-tab-pane label="ID歸戶增量統計"  name="tab3">
+                <IdIncrement />
+            </el-tab-pane>
         </el-tabs>
         <div class="inquirebox-details" v-else-if="pageShow ==='details'">
             <Detailsbox :searchdata="searchResult" :defDate="defDate" @parentClick="backClicks"></Detailsbox>
@@ -49,11 +52,12 @@
     </div>
 </template>
 <script setup>
-import { ref, getCurrentInstance, watch, h, nextTick,provide } from 'vue'
+import { ref, reactive, getCurrentInstance, watch, h, nextTick,provide } from 'vue'
 import { ElMessage, ElLoading } from 'element-plus'
 import { Search,Delete } from '@element-plus/icons'
 import Detailsbox from './details.vue'
 import UpdCodeW from './updCodeW.vue'
+import IdIncrement from './idIncrement.vue'
 import {useRoute} from "vue-router"
 import { useStore } from 'vuex'
 let { proxy } = getCurrentInstance()
@@ -75,6 +79,7 @@ let selecttypes = ref('precise')
 let searchResult=ref()
 let defDate = ref()
 let searchLoading=ref(false)
+let activeTab = reactive('tab1')
 const searchRecordsArr = ref(localStorage.getItem('searchRecords') ?  JSON.parse(localStorage.getItem('searchRecords')):[]);
 const backClicks=()=>{pageShow.value='default'}
 console.log(searchRecordsArr.value,'搜索记录')

+ 2 - 2
TEAMModelBI/Controllers/BIBlob/AnalyseFileController.cs

@@ -206,7 +206,7 @@ namespace TEAMModelBI.Controllers.BIBlob
             RecCnt saveCnts = new();
             //var ipGroup = aGInfos.GroupBy(g => g.properties.clientIp).ToDictionary(k => k.Key, k => k.Count()).ToList();
 
-            List<RecAppGWInfo> recInfo = aGInfos.Select(s => new RecAppGWInfo { hour = cHour, ip = s.properties.clientIp, api = s.properties.requestUri.Split("?").ToList().Count() > 1 ? s.properties.requestUri.Split("?").ToList()[0] : s.properties.requestUri, hostName = s.properties.hostname }).ToList();
+            List<RecAppGWInfo> recInfo = aGInfos.Select(s => new RecAppGWInfo { hour = cHour, ip = s.properties.CIp, api = s.properties.CsUriStem.Split("?").ToList().Count() > 1 ? s.properties.CsUriStem.Split("?").ToList()[0] : s.properties.CsUriStem, hostName = s.properties.CsHost }).ToList();
 
             List<RecApiCnt> apiCnt = recInfo.GroupBy(a => a.api).Select(g => new RecApiCnt { api = g.Key, count = g.Count(), hour = cHour, hostName = g.Select(h => h.hostName).Distinct().ToList(), ip = g.Select(i => i.ip).Distinct().ToList() }).ToList();
             saveCnts.apiCnt= apiCnt;
@@ -296,7 +296,7 @@ namespace TEAMModelBI.Controllers.BIBlob
 
                 RecCnt saveCnts = new();
 
-                List<RecAppGWInfo> recInfo = aGInfos.Select(s => new RecAppGWInfo { hour = cHour, ip = s.properties.clientIp, api = s.properties.requestUri.Split("?").ToList().Count() > 1 ? s.properties.requestUri.Split("?").ToList()[0] : s.properties.requestUri, hostName = s.properties.hostname }).ToList();
+                List<RecAppGWInfo> recInfo = aGInfos.Select(s => new RecAppGWInfo { hour = cHour, ip = s.properties.CIp, api = s.properties.CsUriStem.Split("?").ToList().Count() > 1 ? s.properties.CsUriStem.Split("?").ToList()[0] : s.properties.CsUriStem, hostName = s.properties.CsHost }).ToList();
 
                 List<RecApiCnt> apiCnt = recInfo.GroupBy(a => a.api).Select(g => new RecApiCnt { api = g.Key, count = g.Count(), hour = cHour, hostName = g.Select(h => h.hostName).Distinct().ToList(), ip = g.Select(i => i.ip).Distinct().ToList() }).ToList();
                 saveCnts.apiCnt = apiCnt;

+ 4 - 4
TEAMModelBI/Controllers/BIHome/OnLineController.cs

@@ -123,7 +123,7 @@ namespace TEAMModelBI.Controllers.BIHome
                 
                 var tchDaysOrderBy = tchDays.OrderBy(o => o.Key).ToList();
                 tchkey = (int)tchDaysOrderBy[tchDaysOrderBy.Count - 1].Key;
-                onTchCnt = (int)tchDaysOrderBy[tchkey].Value;              
+                onTchCnt = (int)tchDaysOrderBy[tchDaysOrderBy.Count - 1].Value;              
             }
             else
             {// 如果redis沒資料去Azure Storage取
@@ -138,7 +138,7 @@ namespace TEAMModelBI.Controllers.BIHome
                     }
                     var tchDaysOrderBy = tchDays.OrderBy(o => o.Key).ToList();
                     tchkey = (int)tchDaysOrderBy[tchDaysOrderBy.Count - 1].Key;
-                    onTchCnt = (int)tchDaysOrderBy[tchkey].Value;
+                    onTchCnt = (int)tchDaysOrderBy[tchDaysOrderBy.Count - 1].Value;
                 }
             }
             // 撈redis記錄的學生在線人數
@@ -153,7 +153,7 @@ namespace TEAMModelBI.Controllers.BIHome
                 }
 
                 var stuDaysOrderBy = stuDays.OrderBy(o => o.Key).ToList();
-                onStuCnt = (int)stuDaysOrderBy[stuDaysOrderBy.Count - 2].Value;
+                onStuCnt = (int)stuDaysOrderBy[stuDaysOrderBy.Count - 1].Value;
             }
             else
             {// 如果redis沒資料去Azure Storage取
@@ -166,7 +166,7 @@ namespace TEAMModelBI.Controllers.BIHome
                         stuDays.Add(item.Hour, item.Student);
                     }
                     var stuDaysOrderBy = stuDays.OrderBy(o => o.Key).ToList();
-                    onStuCnt = (int)stuDaysOrderBy[stuDaysOrderBy.Count - 2].Value;             
+                    onStuCnt = (int)stuDaysOrderBy[stuDaysOrderBy.Count - 1].Value;             
                 }
             }
 

Plik diff jest za duży
+ 212 - 7
TEAMModelBI/Controllers/BIProductAnalysis/ProductAnalysisController.cs


Plik diff jest za duży
+ 382 - 242
TEAMModelBI/Controllers/BITmid/TmidController.cs


Plik diff jest za duży
+ 7 - 2
TEAMModelBI/Controllers/Census/SchoolController.cs


+ 32 - 0
TEAMModelBI/Filter/AspNetCoreBuilderServiceCollectionExtensions.cs

@@ -0,0 +1,32 @@
+using Google.Protobuf.WellKnownTypes;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+
+namespace TEAMModelOS.Filter
+{
+    public static class AspNetCoreBuilderServiceCollectionExtensions
+    {  /// <summary>
+       /// 注册 Mvc 过滤器
+       /// </summary>
+       /// <typeparam name="TFilter"></typeparam>
+       /// <param name="services"></param>
+       /// <param name="configure"></param>
+       /// <returns></returns>
+        public static IServiceCollection AddMvcFilter<TFilter>(this IServiceCollection services, Action<MvcOptions> configure = default)
+            where TFilter : IFilterMetadata
+        {
+            services.Configure<MvcOptions>(options =>
+            {
+                options.Filters.Add<TFilter>();
+
+                // 其他额外配置
+                configure?.Invoke(options);
+            });
+
+            return services;
+        }
+    }
+
+}

+ 221 - 0
TEAMModelBI/Filter/RequestAuditFilter.cs

@@ -0,0 +1,221 @@
+using Microsoft.AspNetCore.Mvc.Controllers;
+using Microsoft.AspNetCore.Mvc.Filters;
+using System.Security.Claims;
+using System;
+using System.Threading.Tasks;
+using TEAMModelOS.SDK.Extension;
+using Microsoft.Extensions.Logging;
+using TEAMModelOS.SDK;
+using System.IdentityModel.Tokens.Jwt;
+using System.Linq;
+using Azure.Core;
+using TEAMModelOS.SDK.DI;
+using Microsoft.Extensions.Primitives;
+using HTEXLib.Helpers.ShapeHelpers;
+using System.Net.Http;
+using System.ServiceModel.Channels;
+using System.Net;
+using System.Net.Http.Json;
+using Microsoft.Extensions.Options;
+using TEAMModelOS.Models;
+using OpenXmlPowerTools;
+
+namespace TEAMModelOS.Filter
+{
+    public class RequestAuditFilter : IAsyncActionFilter
+    {
+        //private readonly ILogger _logger;
+        private readonly IHttpClientFactory _httpClient;
+        private readonly DingDing _dingding;
+        private readonly Option _option; private string p = "bi";
+        public RequestAuditFilter(/*ILoggerFactory loggerFactory*/IHttpClientFactory httpClient, DingDing dingding, IOptionsSnapshot<Option> option)
+        {
+            //  _logger = loggerFactory.CreateLogger<RequestAuditFilter>();
+            _httpClient = httpClient;
+            _dingding=dingding;
+            _option = option?.Value;
+        }
+        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+        {
+            try
+            {
+                string id = string.Empty, name = string.Empty, picture = string.Empty, school = string.Empty, scope = string.Empty, roles = string.Empty;
+                //============== 这里是执行方法之前获取数据 ====================
+
+                // 获取控制器、路由信息
+                //var actionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
+
+                // 获取请求的方法
+                //var method = actionDescriptor.MethodInfo;
+
+                // 获取 HttpContext 和 HttpRequest 对象
+                var httpContext = context.HttpContext;
+                string ua = httpContext.GetUserAgent();
+                var httpRequest = httpContext.Request;
+
+                // 获取客户端 Ipv4 地址
+                var remoteIPv4 = httpContext.GetRemoteIpAddressToIPv4();
+
+                // 获取请求的 Url 地址
+                // var requestUrl = httpRequest.GetRequestUrlAddress();
+
+
+                // 获取来源 Url 地址
+                var refererUrl = httpRequest.GetRefererUrlAddress();
+
+                // 获取请求参数(写入日志,需序列化成字符串后存储)
+                var parameters = context.ActionArguments;
+
+                // 获取操作人(必须授权访问才有值)"userId" 为你存储的 claims type,jwt 授权对应的是 payload 中存储的键名
+                //var userId = httpContext.User?.FindFirstValue("userId");
+                var authtoken = context.HttpContext.GetXAuth("AuthToken");
+                string tokenSha = string.Empty, client = string.Empty;
+                if (context.HttpContext.Request.Headers.TryGetValue("Authorization", out StringValues Authorization) && Authorization.Any())
+                {
+                    try
+                    {
+                        var jwt = new JwtSecurityTokenHandler().ReadJwtToken(Authorization.ToString().Replace("Bearer ", ""));
+                        client= roles = jwt.Claims.FirstOrDefault(claim => claim.Type.Equals("roles"))?.Value;
+                        tokenSha= ShaHashHelper.GetSHA1(Authorization.ToString());
+                    }
+                    catch (Exception ex)
+                    {
+                        // await _dingding.SendBotMsg($"{ex.Message}\n{ex.StackTrace}\n{Authorization}\n{httpRequest.PathBase}{httpRequest.Path}", GroupNames.成都开发測試群組);
+                    }
+                }
+                if (context.HttpContext.Request.Headers.TryGetValue("X-Auth-IdToken", out StringValues XAuthIdToken)  && XAuthIdToken.Any())
+                {
+                    try
+                    {
+                        var jwt = new JwtSecurityTokenHandler().ReadJwtToken(XAuthIdToken.ToString());
+                        id = jwt.Payload.Sub;
+                        name = jwt.Claims.FirstOrDefault(claim => claim.Type.Equals("name"))?.Value;
+                        if (string.IsNullOrEmpty(tokenSha))
+                        {
+                            tokenSha= ShaHashHelper.GetSHA1(XAuthIdToken.ToString());
+                        }
+                        scope="teacher";
+                    }
+                    catch (Exception ex)
+                    {
+                        await _dingding.SendBotMsg($"{ex.Message}\n{ex.StackTrace}\n{XAuthIdToken}\n{httpRequest.PathBase}{httpRequest.Path}", GroupNames.成都开发測試群組);
+                    }
+                }
+
+                if (context.HttpContext.Request.Headers.TryGetValue("X-Auth-School", out StringValues XAuthSchool) && XAuthSchool.Any())
+                {
+                    try
+                    {
+                        school = XAuthSchool.ToString();
+                    }
+                    catch (Exception ex) { }
+                }
+                if (context.HttpContext.Request.Headers.TryGetValue("X-Auth-ApiToken", out StringValues XAutApiToken) && XAutApiToken.Any())
+                {
+                    try
+                    {
+                        var jwt = new JwtSecurityTokenHandler().ReadJwtToken(XAutApiToken);
+                        id = jwt.Payload.Sub;
+
+                        if (string.IsNullOrEmpty(tokenSha))
+                        {
+                            tokenSha= jwt.Payload.Jti;
+                        }
+                        client="Open";
+                    }
+                    catch (Exception ex)
+                    {
+
+                        await _dingding.SendBotMsg($"{ex.Message}\n{ex.StackTrace}\n{XAutApiToken}\n{httpRequest.PathBase}{httpRequest.Path}", GroupNames.成都开发測試群組);
+                    }
+                }
+                if (!string.IsNullOrWhiteSpace(authtoken))
+                {
+                    try
+                    {
+                        var jwt = new JwtSecurityTokenHandler().ReadJwtToken(authtoken);
+                        id = jwt.Payload.Sub;
+                        school = $"{jwt.Payload.Azp}".Equals("true")|| $"{jwt.Payload.Azp}".Equals("false") ? "" : $"{jwt.Payload.Azp}";
+                        name = jwt.Claims.FirstOrDefault(claim => claim.Type.Equals("name"))?.Value;
+                        scope = jwt.Claims.FirstOrDefault(claim => claim.Type.Equals("scope"))?.Value;
+                        if (string.IsNullOrEmpty(tokenSha))
+                        {
+                            tokenSha= ShaHashHelper.GetSHA1(authtoken);
+                        }
+                    }
+                    catch (Exception ex)
+                    {
+                        await _dingding.SendBotMsg($"{ex.Message}\n{ex.StackTrace}\n{authtoken}\n{httpRequest.PathBase}{httpRequest.Path}", GroupNames.成都开发測試群組);
+                    }
+                }
+                string secChUaPlatform = string.Empty;
+                if (httpContext.Request.Headers.TryGetValue("Sec-Ch-Ua-Platform", out var values))
+                {
+                    secChUaPlatform = values.FirstOrDefault();
+                }
+                if (string.IsNullOrEmpty(tokenSha))
+
+                {
+                    tokenSha= ShaHashHelper.GetSHA1($"{ua}{remoteIPv4}{httpRequest.Host}{secChUaPlatform}");
+                }
+                // 请求时间
+                var requestedTime = DateTimeOffset.Now.GetGMTTime(8).ToUnixTimeMilliseconds();
+                //============== 这里是执行方法之后获取数据 ====================
+                var actionContext = await next();
+                // 获取返回的结果
+                // var returnResult = actionContext.Result;
+
+                // 判断是否请求成功,没有异常就是请求成功
+                // var isRequestSucceed = actionContext.Exception == null;
+
+                // 获取调用堆栈信息,提供更加简单明了的调用和异常堆栈
+                // var stackTrace = EnhancedStackTrace.Current();
+                // string region = await _searcher.SearchIpAsync(remoteIPv4);
+                //同一个账号,同一IP,同一接口,UA标识(UA标识随意切换则表示可能会存在DDOS),时间段
+                //_logger.LogInformation(new{ ua=httpContext.GetUserAgent(), ip=remoteIPv4,time=requestedTime,path =$"{httpRequest.PathBase}{httpRequest.Path}",host= $"{httpRequest.Host}", param=parameters,id ,name ,school,succeed =isRequestSucceed }.ToJsonString());
+
+                var data = new
+                {
+                    //ua =ua,
+                    ip = remoteIPv4,
+                    time = requestedTime,
+                    path = $"{httpRequest.PathBase}{httpRequest.Path}",
+                    host = $"{httpRequest.Host}",
+                    param = parameters,
+                    id = id,
+                    name = name,
+                    school = school,
+                    client = client,
+                    tid = tokenSha,
+                    scope = scope,
+                    // referer = refererUrl,
+                    //platform = secChUaPlatform,
+                    p = p,
+                    l = _option.Location.Contains("China", StringComparison.OrdinalIgnoreCase) ? "China" : "Global"
+                    //idToken=XAuthIdToken
+                };
+                var httpclient = _httpClient.CreateClient();
+                httpclient.Timeout=  TimeSpan.FromSeconds(10);
+#if DEBUG
+                var response = await httpclient.PostAsJsonAsync("http://cdhabook.teammodel.cn:8805/api/http-log", data);
+                //if (response.StatusCode==HttpStatusCode.OK) 
+                //{
+                //    string result =   await response.Content.ReadAsStringAsync();
+
+                //}
+#else
+            _=  httpclient.PostAsJsonAsync("http://cdhabook.teammodel.cn:8805/api/http-log",data);
+#endif
+                //   _ = _httpTrigger.RequestHttpTrigger(data, "China", "http-log");
+            }
+            catch (Exception ex)
+            {
+                await _dingding.SendBotMsg($"HTTP日志访问错误:{ex.Message}\n{ex.StackTrace}", GroupNames.成都开发測試群組);
+                var actionContext = await next();
+            }
+
+        }
+
+    }
+
+}

+ 12 - 2
TEAMModelBI/Models/RearEndMiddle.cs

@@ -27,7 +27,17 @@ namespace TEAMModelBI.Models
         public List<string> serial { get; set; }
         public List<string> service { get; set; }
         public List<string> hard { get; set; }
-
-
+        /// <summary>
+        /// 城市
+        /// </summary>
+        public string city { get; set; }
+        /// <summary>
+        /// 区/县/郡
+        /// </summary>
+        public string dist { get; set; }
+        /// <summary>
+        /// 區碼
+        /// </summary>
+        public string areaId { get; set; }     
     }
 }

+ 2 - 2
TEAMModelBI/Startup.cs

@@ -26,6 +26,7 @@ using TEAMModelOS.SDK.Helper.Common.ReflectorExtensions;
 using TEAMModelOS.SDK.Models;
 using VueCliMiddleware;
 using System.Net.Http;
+using TEAMModelOS.Filter;
 
 namespace TEAMModelBI
 {
@@ -185,6 +186,7 @@ namespace TEAMModelBI
             });
             //等保安全性验证。
             services.AddScoped<SecurityHeadersAttribute>();
+            services.AddMvcFilter<RequestAuditFilter>();
             services.AddAntiforgery(options => options.HeaderName = "X-CSRF-TOKEN");
         }
 
@@ -233,8 +235,6 @@ namespace TEAMModelBI
 #endif
 
             });
-
-
         }
     }
 }

+ 3 - 3
TEAMModelBI/TEAMModelBI.csproj

@@ -65,9 +65,9 @@
 		<SpaRoot>ClientApp\</SpaRoot>
 		<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
 		<UserSecretsId>078b5d89-7d90-4f6a-88fc-7d96025990a8</UserSecretsId>
-		<Version>5.2401.31</Version>
-		<AssemblyVersion>5.2401.31.1</AssemblyVersion>
-		<FileVersion>5.2401.31.1</FileVersion>
+		<Version>5.2404.03</Version>
+		<AssemblyVersion>5.2404.03.1</AssemblyVersion>
+		<FileVersion>5.2404.03.1</FileVersion>
 		<Description>TEAMModelBI(BI)</Description>
 		<PackageReleaseNotes>BI版本说明版本切换标记2022000908</PackageReleaseNotes>
 		<PackageId>TEAMModelBI</PackageId>

+ 3 - 1
TEAMModelBI/appsettings.Development.json

@@ -16,7 +16,9 @@
     "JwtSecretKey": "fXO6ko/qyXeYrkecPeKdgXnuLXf9vMEtnBC9OB3s+aA=",
     "Exp": 86400,
     "IdTokenSalt": "8263692E2213497BB55E74792B7900B4",
-    "HttpTrigger": "https://teammodelosfunction-test.chinacloudsites.cn/api/"
+    "HttpTrigger": "https://teammodelosfunction-test.chinacloudsites.cn/api/",
+	"Audience": "72643704-b2e7-4b26-b881-bd5865e7a7a5",
+    "Authority": "https://login.chinacloudapi.cn/4807e9cf-87b8-4174-aa5b-e76497d7392b/v2.0"
     //"HttpTrigger": "http://localhost:7071/api/"
   },
   //大陆站连接字符串 现在是测试站

+ 22 - 7
TEAMModelOS.FunctionV4/CosmosDB/TriggerArt.cs

@@ -362,7 +362,7 @@ namespace TEAMModelOS.FunctionV4.CosmosDB
                             {
 
                             }
-                            if (art.publish == 0)
+                            if (art.classes.Any())
                             {
                                 //获取该艺术评测的评测Id
                                 List<(string id, string subjectId)> ids = new();
@@ -394,7 +394,7 @@ namespace TEAMModelOS.FunctionV4.CosmosDB
                                 var sta = examClassResults.SelectMany(x => x.status).ToList();
                                 var ansCount = sta.Where(x => x == 0).ToList();
                                 var persent = ansCount.Count * 1.0 / sta.Count * 100;
-                                var period = scInfo.period.Where(x => x.id.Equals(art.period.id)).FirstOrDefault();
+                                var period = scInfo.period.Where(x => x.id.Equals(art.period.id))?.FirstOrDefault();
 
                                 List<StudentArtResult> studentArtResults = new();
                                 string sql = $"SELECT value c FROM c   where  c.pk='ArtResult' ";
@@ -417,7 +417,13 @@ namespace TEAMModelOS.FunctionV4.CosmosDB
                                 if (exams.Count == 0) return; 
                                 foreach (var ss in art.subjects)
                                 {
-                                    knoledge.Add(await getKnowledge(art.periodType, exams.Where(c => c.subjects[0].id.Equals(ss.id))?.FirstOrDefault().papers[0].code, client, ss.id, exams.Where(c => c.subjects[0].id.Equals(ss.id))?.FirstOrDefault().papers[0].periodId));
+                                    if (string.IsNullOrEmpty(exams[0].papers[0].periodId)) {
+                                        
+                                        knoledge.Add(await getKnowledge("university", "hbcn", client, ss.id, "be32942d-97a9-52ba-45d6-2e5b722583f5"));
+                                    } else {
+                                        knoledge.Add(await getKnowledge(art.periodType, exams.Where(c => c.subjects[0].id.Equals(ss.id))?.FirstOrDefault().papers[0].code, client, ss.id, exams.Where(c => c.subjects[0].id.Equals(ss.id))?.FirstOrDefault().papers[0].periodId));
+                                    }
+                                   
                                 }
 
                                 List<(string name, double score, double aver, string subject)> blockScore = new();
@@ -523,7 +529,16 @@ namespace TEAMModelOS.FunctionV4.CosmosDB
                                         aver = z.ToList().Sum(j => j.aver)
                                     })
                                 });
-                                var subjectKnow = knoledge.Where(c => c.ptype.Equals(art.periodType)).Select(x => new { x.subId, x.knos }).ToList();
+                                //List<(string subId,List<(string name, List<string> kno)>)> subjectKnow = knoledge.Select(x => new { x.subId, x.knos }).ToList();
+                                //var subjectKnow;
+                                var subjectKnow = string.IsNullOrEmpty(exams[0].papers[0].periodId) ? knoledge.Select(x => new { x.subId, x.knos }).ToList() : knoledge.Where(c => c.ptype.Equals(art.periodType)).Select(x => new { x.subId, x.knos }).ToList();
+                                /*if (string.IsNullOrEmpty(exams[0].papers[0].periodId))
+                                {
+                                    var subjectKnow = knoledge.Select(x => new { x.subId, x.knos }).ToList();
+                                }
+                                else {
+                                    var subjectKnow = knoledge.Where(c => c.ptype.Equals(art.periodType)).Select(x => new { x.subId, x.knos }).ToList();
+                                }*/
                                 List<(string subjectId, List<(string name, double score, double persent, double aver, List<string> dim)> bks)> bs = new();
                                 List<(string subjectId, List<(string stuId, List<(string name, double score, double point, List<string> dim)> values)> stuBks)> sbs = new();
                                 List<(string name, double score, double av, string sId)> stuBlockScore = new();
@@ -658,7 +673,7 @@ namespace TEAMModelOS.FunctionV4.CosmosDB
                                             if (res.score >= 95) {
                                                 res.score = new Random().Next(90, 99);
                                             }*/
-                                            res.score = Math.Round(res.score);
+                                            //res.score = Math.Round(res.score);
                                         }
                                     }
 
@@ -726,7 +741,7 @@ namespace TEAMModelOS.FunctionV4.CosmosDB
                                             totalScore = 100,
                                             id = sj.id,  
                                             type = sj.id,
-                                            block = stuBlocks.Where(c => c.subjectId.Equals(sj.id)).SelectMany(x => x.dim).Where(z => z.stuId.Equals(rs.studentId)).FirstOrDefault().blk,
+                                            block = stuBlocks.Where(c => c.subjectId.Equals(sj.id)).SelectMany(x => x.dim).Where(z => z.stuId.Equals(rs.studentId))?.FirstOrDefault().blk,
                                             kno = studentScores.Where(c => c.stuId.Equals(rs.studentId)).SelectMany(c => c.studentScore).Where(
                                                 p => p.subject.Equals(sj.id)).Select(z => new
                                                 {
@@ -803,7 +818,7 @@ namespace TEAMModelOS.FunctionV4.CosmosDB
 
                     string code = $"Knowledge-{school}-{subjectId}";
                     StringBuilder sql = new StringBuilder($"select value(c) from c");
-                    if (string.IsNullOrWhiteSpace(pId))
+                    if (!string.IsNullOrWhiteSpace(pId))
                     {
                         sql.Append($" where c.periodId = '{pId}'");
                     }

+ 8 - 6
TEAMModelOS.FunctionV4/CosmosDB/TriggerExam.cs

@@ -17,7 +17,6 @@ using Azure.Storage.Blobs.Models;
 using System.IO;
 using System.Text;
 using System.Text.Json.Nodes;
-using TEAMModelOS.SDK.Helper.Common.JsonHelper.JsonPath;
 using Newtonsoft.Json.Linq;
 using TEAMModelOS.SDK.Models.Cosmos.Student;
 using HTEXLib.Helpers.ShapeHelpers;
@@ -37,6 +36,7 @@ using TEAMModelOS.Models;
 using Microsoft.AspNetCore.Razor.TagHelpers;
 using HtmlAgilityPack;
 using Azure.Storage.Blobs;
+using System.ComponentModel;
 
 namespace TEAMModelOS.FunctionV4
 {
@@ -807,12 +807,12 @@ namespace TEAMModelOS.FunctionV4
             {// 如果是是非題 正確答案要用true false的方式設定
                 if (questionData.exercise.answer[0] == "A")
                 {
-                    learnRecordItem.Correct = true;
+                    learnRecordItem.Correct = new string[] { "true" };
                 }
                 else
                 {
-                    learnRecordItem.Correct = false;
-                }
+                    learnRecordItem.Correct = new string[] { "false" };
+                }               
             }
             else
             {// 防呆 去html標籤
@@ -1879,7 +1879,9 @@ namespace TEAMModelOS.FunctionV4
             result.scope = info.scope;
             result.name = info.name;
             result.time = info.startTime;
-            await examRecordCount(info, subject, _dingDing, no, result, examClassResults, _azureCosmos);
+            if (info.qamode != 2) {
+                await examRecordCount(info, subject, _dingDing, no, result, examClassResults, _azureCosmos);
+            }          
             await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, "Common").UpsertItemAsync(result, new Azure.Cosmos.PartitionKey($"ExamResult-{info.id}"));
             List<ErrorItems> errorItems = new();
             string code = string.Empty;
@@ -1964,7 +1966,7 @@ namespace TEAMModelOS.FunctionV4
                         int level = item_json.level;
                         var knowledge = item_json.knowledges;
                         //string pid = keys.Value<string>("pid");
-                        itemInfos.Add((id, pid.GetString(), level, type, knowledge));
+                        itemInfos.Add((id, pid.ToString(), level, type, knowledge));
                     }
 
                     /* double[] point = StringHelper.ListTodouble(result.paper.point);

+ 37 - 2
TEAMModelOS.FunctionV4/HttpTrigger/IESHttpTrigger.cs

@@ -1,5 +1,6 @@
 using Azure.Cosmos;
 using Azure.Storage.Blobs.Models;
+using Azure.Storage.Blobs.Specialized;
 using Azure.Storage.Sas;
 using DocumentFormat.OpenXml.Bibliography;
 using DocumentFormat.OpenXml.Drawing.Wordprocessing;
@@ -28,6 +29,7 @@ using System.Net.Http;
 using System.Net.Http.Json;
 using System.Reflection;
 using System.Security.Policy;
+using System.ServiceModel.Channels;
 using System.Text;
 using System.Text.Json;
 using System.Threading;
@@ -72,9 +74,42 @@ namespace TEAMModelOS.FunctionV4
             _option = option?.Value;
             _configuration = configuration;
         }
+        // <summary>
+        /// </summary>
+        /// <param name="req"></param>
+        /// <param name="log"></param>
+        /// <returns></returns>
+        [Function("http-log")]
+        public async Task<HttpResponseData> HttpLog([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequestData req) {
+            var response = req.CreateResponse(HttpStatusCode.OK);
+            try {
+                string data;
+                using (var reader = new StreamReader(req.Body))
+                {
+                    data = await reader.ReadToEndAsync();
+                }
+                var gmt8Time = DateTimeOffset.UtcNow.GetGMTTime(8);
+                var appendBlob = _azureStorage.GetBlobContainerClient("0-service-log").GetAppendBlobClient($"http-log/{gmt8Time:yyyy}/{gmt8Time:MM}/{gmt8Time:dd}/{gmt8Time:HH}.log");
+                if (!await appendBlob.ExistsAsync())
+                {
+                    await appendBlob.CreateAsync();
+                }
+                using (var stream = new MemoryStream(Encoding.UTF8.GetBytes($"{data},\n")))
+                {
+                    await appendBlob.AppendBlockAsync(stream);
+                }
+                await response.WriteAsJsonAsync(new { code = 1 });
+                return response;  
 
-        
-        /// <summary>
+            } catch(Exception ex ) {
+                await _dingDing.SendBotMsg($"{ex.Message}\n{ex.StackTrace}", GroupNames.成都开发測試群組);
+                await response.WriteAsJsonAsync(new { code = 1 });
+                return response;
+            }
+           
+        }
+
+         /// <summary>
          /// </summary>
          /// <param name="req"></param>
          /// <param name="log"></param>

+ 1 - 0
TEAMModelOS.FunctionV4/Program.cs

@@ -83,6 +83,7 @@ namespace TEAMModelOS.FunctionV4
                services.AddMultipleAzureStorage(storageConnects);
                services.AddHostedService<BlobRootServiceBusSub>();
                services.AddIPSearcher("");
+               services.TryAddSingleton(new Region2LongitudeLatitudeTranslator(""));
            })
            .Build();
             await host.RunAsync();

+ 25 - 3
TEAMModelOS.FunctionV4/ServiceBus/ActiveTaskTopic.cs

@@ -27,7 +27,6 @@ using System.IO;
 using Azure;
 using static TEAMModelOS.SDK.Models.Service.LessonService;
 using DinkToPdf.Contracts;
-using TEAMModelOS.SDK.Helper.Common.DateTimeHelper;
 using static TEAMModelOS.SDK.StatisticsService;
 using DocumentFormat.OpenXml.Office2010.Excel;
 using DocumentFormat.OpenXml.Wordprocessing;
@@ -44,6 +43,8 @@ using System.Collections.Concurrent;
 using Microsoft.Azure.Amqp.Framing;
 using System.Security.Policy;
 using TEAMModelOS.SDK.Models.Service.BI;
+using TEAMModelOS.SDK.Models.Dtos;
+using System.ServiceModel.Channels;
 
 namespace TEAMModelOS.FunctionV4.ServiceBus
 {
@@ -495,6 +496,15 @@ namespace TEAMModelOS.FunctionV4.ServiceBus
                 //await _dingDing.SendBotMsg($"名单变化{msg}", GroupNames.成都开发測試群組);
                 var jsonMsg = JsonDocument.Parse(msg);
                 GroupChange groupChange = msg.ToObject<GroupChange>();
+                int add = groupChange.tmdjoin.Count-groupChange.tmdleave.Count+groupChange.stujoin.Count-groupChange.stuleave.Count;
+                if (groupChange.scope.Equals("private") && (groupChange.type.Equals("student") || groupChange.type.Equals("class") || groupChange.type.Equals("teach")))
+                {
+                    await SystemService.RecordAccumulateData(_azureRedis, _dingDing, new Accumulate { client=groupChange.client, count=add, id= groupChange.listid, scope="teacher", key= "grouplist", name=groupChange.name, target= groupChange.creatorId });
+                }
+                if (groupChange.scope.Equals("school")  && (groupChange.type.Equals("student") || groupChange.type.Equals("class") || groupChange.type.Equals("teach")))
+                {
+                    await SystemService.RecordAccumulateData(_azureRedis,_dingDing, new Accumulate { client=groupChange.client, count=add, id= groupChange.listid, scope="school", key= "grouplist", name=groupChange.name, target=groupChange.school });
+                }
                 //名单变动修改学生课程关联信息
                 //await StuListService.FixStuCourse(client, stuListChange);
                 //Vote投票 Survey问卷 Exam评测 Learn学习活动 Homework作业活动
@@ -685,10 +695,10 @@ namespace TEAMModelOS.FunctionV4.ServiceBus
                     {
                         artResult.pdf.prime = false;//此处的作用是判断是否已经生成OK.
                     }
-                    await _azureRedis.GetRedisClient(8).HashSetAsync($"ArtPDF:{_artId}", artResult.studentId, artResult.ToJsonString());
+                    await _azureRedis.GetRedisClient(8).HashSetAsync($"ArtPDF:{_artId}:{_schoolCode}", artResult.studentId, artResult.ToJsonString());
                 }
                 //2个小时。
-                await _azureRedis.GetRedisClient(8).KeyExpireAsync($"ArtPDF:{_artId}", new TimeSpan(2, 0, 0));
+                await _azureRedis.GetRedisClient(8).KeyExpireAsync($"ArtPDF:{_artId}:{_schoolCode}", new TimeSpan(2, 0, 0));
                 List<Task<string>> uploads = new List<Task<string>>();
                 studentPdfs.ForEach(x => {
                     x.blob = $"art/{x.artId}/report/{x.studentId}.json";
@@ -1631,6 +1641,8 @@ namespace TEAMModelOS.FunctionV4.ServiceBus
 
                                 lessonRecord.show = teacher.lessonShow;
                                 lessonRecord.upload = 0;
+
+
                                 if (!string.IsNullOrEmpty(lessonRecord.courseId))
                                 {
                                     CourseBase course = null;
@@ -1677,6 +1689,7 @@ namespace TEAMModelOS.FunctionV4.ServiceBus
                                         lessonRecord.subjectId = course.subject?.id;
                                     }
                                 }
+                              
                                 //处理课堂选用的名单
                                 if (lessonRecord.groupIds.IsNotEmpty())
                                 {
@@ -1701,6 +1714,13 @@ namespace TEAMModelOS.FunctionV4.ServiceBus
                                 {
                                     if (scope.Equals("school")) {
                                         School schoolBase = await client.GetContainer(Constant.TEAMModelOS, Constant.School).ReadItemAsync<School>(school, new PartitionKey("Base"));
+
+                                        {
+                                            await SystemService.RecordAccumulateData(_azureRedis, _dingDing, new SDK.Models.Dtos.Accumulate
+                                            { client="hiteach", count=1, id=schoolBase.id, key="lesson-create", name=schoolBase.name, scope="school", target=schoolBase.id });
+                                            await SystemService.RecordAccumulateData(_azureRedis, _dingDing, new SDK.Models.Dtos.Accumulate
+                                            { client="hiteach", count=1, id=lessonRecord.tmdid, key="lesson-create", name="课例", scope="teacher", target=lessonRecord.tmdid });
+                                        }
                                         var space = await BlobService.GetSurplusSpace(school, scope, $"{Environment.GetEnvironmentVariable("Option:Location")}", _azureCosmos, _azureRedis, _azureStorage, _dingDing, _httpTrigger);
                                         SchoolSetting setting = null;
                                         Azure.Response schoolSetting = await client.GetContainer(Constant.TEAMModelOS, Constant.School).ReadItemStreamAsync(school, new PartitionKey("SchoolSetting"));
@@ -2044,6 +2064,8 @@ namespace TEAMModelOS.FunctionV4.ServiceBus
                                         {
                                             //=-1 则表示不限制上传数量
                                         }
+                                        await SystemService.RecordAccumulateData(_azureRedis, _dingDing, new SDK.Models.Dtos.Accumulate
+                                        { client="hiteach", count=1, id=lessonRecord.tmdid, key="lesson-create", name="课例", scope="teacher", target=lessonRecord.tmdid });
                                     }
                                 }
                                 catch (Exception e)

+ 9 - 3
TEAMModelOS.FunctionV4/TEAMModelOS.FunctionV4.csproj

@@ -5,9 +5,9 @@
 		<OutputType>Exe</OutputType>
 		<_FunctionsSkipCleanOutput>true</_FunctionsSkipCleanOutput>
 		<SignAssembly>true</SignAssembly>
-		<Version>5.2401.31</Version>
-		<AssemblyVersion>5.2401.31.1</AssemblyVersion>
-		<FileVersion>5.2401.31.1</FileVersion>
+		<Version>5.2404.03</Version>
+		<AssemblyVersion>5.2404.03.1</AssemblyVersion>
+		<FileVersion>5.2404.03.1</FileVersion>
 		<PackageId>TEAMModelOS.FunctionV4</PackageId>
 		<Authors>teammodel</Authors>
 		<Company>醍摩豆(成都)信息技术有限公司</Company>
@@ -24,6 +24,7 @@
 		<None Remove="Lang\en-us.json" />
 		<None Remove="Lang\zh-cn.json" />
 		<None Remove="Lang\zh-tw.json" />
+		<None Remove="latlng.json" />
 	</ItemGroup>
 	<ItemGroup>
 		<Content Include="Lang\en-us.json">
@@ -41,6 +42,11 @@
 			<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
 			<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
 		</Content>
+ 		<Content Include="latlng.json">
+ 		  <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ 		  <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
+ 		  <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
+ 		</Content>
 	</ItemGroup>
 
 	<ItemGroup>

+ 77 - 38
TEAMModelOS.FunctionV4/TimeTrigger/IESTimerTrigger.cs

@@ -1,5 +1,6 @@
 using Azure.Cosmos;
 using Azure.Storage.Blobs.Models;
+using Azure.Storage.Blobs.Specialized;
 using DinkToPdf;
 using DinkToPdf.Contracts;
 using HTEXLib.COMM.Helpers;
@@ -7,8 +8,10 @@ using Microsoft.Azure.Cosmos.Table;
 using Microsoft.Azure.Functions.Worker;
 using Microsoft.Azure.Functions.Worker.Http;
 using Microsoft.Extensions.Logging;
+using Microsoft.OData.Edm;
 using StackExchange.Redis;
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
@@ -27,6 +30,7 @@ using TEAMModelOS.SDK.Models.Cosmos.Teacher;
 using TEAMModelOS.SDK.Models.Service;
 using TEAMModelOS.SDK.Models.Service.BI;
 using TEAMModelOS.SDK.Models.Table;
+using static TEAMModelOS.SDK.Models.Service.SystemService;
 using static TEAMModelOS.SDK.Models.Teacher;
 
 namespace TEAMModelOS.FunctionV4.TimeTrigger
@@ -45,7 +49,8 @@ namespace TEAMModelOS.FunctionV4.TimeTrigger
         private readonly SnowflakeId _snowflakeId;
         private readonly IHttpClientFactory _httpClient;
         private IPSearcher _ipSearcher;
-        public IESTimerTrigger(IPSearcher ipSearcher, IHttpClientFactory httpClient,SnowflakeId snowflakeId,IConverter converter, AzureCosmosFactory azureCosmos, DingDing dingDing, AzureStorageFactory azureStorage, AzureRedisFactory azureRedis)
+        private readonly Region2LongitudeLatitudeTranslator _longitudeLatitudeTranslator;
+        public IESTimerTrigger(Region2LongitudeLatitudeTranslator longitudeLatitudeTranslator, IPSearcher ipSearcher, IHttpClientFactory httpClient, SnowflakeId snowflakeId, IConverter converter, AzureCosmosFactory azureCosmos, DingDing dingDing, AzureStorageFactory azureStorage, AzureRedisFactory azureRedis)
         {
             _azureCosmos = azureCosmos;
             _dingDing = dingDing;
@@ -55,14 +60,16 @@ namespace TEAMModelOS.FunctionV4.TimeTrigger
             _snowflakeId=snowflakeId;
             _httpClient = httpClient;
             _ipSearcher = ipSearcher;
+            _longitudeLatitudeTranslator = longitudeLatitudeTranslator;
         }
+
         /// <summary>
         /// 防火墙日志记录文件
         /// </summary>
         /// <param name="req"></param>
         /// <param name="log"></param>
         /// <returns></returns>
-      //  [Function("FireWallFileLog")]
+        [Function("FireWallFileLog")]
         //https://docs.azure.cn/zh-cn/azure-functions/functions-bindings-timer?tabs=in-process&pivots=programming-language-csharp
         //0 1 * * * * 一天中每小时的第 1 分钟
         //0 */10 * * * *  每五分钟一次
@@ -71,7 +78,7 @@ namespace TEAMModelOS.FunctionV4.TimeTrigger
             try
             {
                 string location = Environment.GetEnvironmentVariable("Option:Location");
-                var datetime = DateTimeOffset.UtcNow.AddHours(-1);
+                var datetime = DateTimeOffset.Now.AddHours(-1);
                 var y = datetime.Year;
                 var m = datetime.Month >= 10 ? $"{datetime.Month}" : $"0{datetime.Month}";
                 var d = datetime.Day >= 10 ? $"{datetime.Day}" : $"0{datetime.Day}";
@@ -83,46 +90,60 @@ namespace TEAMModelOS.FunctionV4.TimeTrigger
 #endif
 
                 {
-                    string path = $"resourceId=/SUBSCRIPTIONS/73B7F9EF-D8B7-4444-9E8D-D80B43BF3CD4/RESOURCEGROUPS/TEAMMODELCHENGDU/PROVIDERS/MICROSOFT.NETWORK/APPLICATIONGATEWAYS/OSFIREWARE/y={y}/m={m}/d={d}/h={h}/m=00/PT1H.json";
-                    var retn = await BILogAnalyseService.GetPathAnalyse(_azureStorage,_ipSearcher,_dingDing, path, "LogStorage");
+                    //string path = $"resourceId=/SUBSCRIPTIONS/73B7F9EF-D8B7-4444-9E8D-D80B43BF3CD4/RESOURCEGROUPS/TEAMMODELCHENGDU/PROVIDERS/MICROSOFT.NETWORK/APPLICATIONGATEWAYS/OSFIREWARE/y={y}/m={m}/d={d}/h={h}/m=00/PT1H.json";
+                    string path = $"resourceId=/SUBSCRIPTIONS/73B7F9EF-D8B7-4444-9E8D-D80B43BF3CD4/RESOURCEGROUPS/TEAMMODELCHENGDU/PROVIDERS/MICROSOFT.WEB/SITES/TEAMMODELOS/y={y}/m={m}/d={d}/h={h}/m=00/PT1H.json";
+                    var retn = await BILogAnalyseService.GetPathAnalyse(_azureStorage, _ipSearcher, _dingDing, path, "LogStorage");
                     if (retn.recCnts.IsNotEmpty())
                     {
                         //https://teammodelos.blob.core.chinacloudapi.cn/0-public/pie-borderRadius.html
-                        string publishUrl = $"https://teammodelos.blob.core.chinacloudapi.cn/0-public/api-count.html?url={HttpUtility.UrlEncode(retn.saveUrls.First(),Encoding.UTF8)}&time={HttpUtility.UrlEncode(datetime.AddHours(8).ToString("yyyy年MM月dd日 HH时"),Encoding.UTF8)}";
+                        string publishUrl = $"https://teammodelos.blob.core.chinacloudapi.cn/0-public/api-count.html?url={HttpUtility.UrlEncode(retn.saveUrls.First(), Encoding.UTF8)}&time={HttpUtility.UrlEncode(datetime.AddHours(8).ToString("yyyy年MM月dd日 HH时"), Encoding.UTF8)}";
                         string ulrs = $"https://teammodelos.blob.core.chinacloudapi.cn/0-public/api-count.html?url={retn.saveUrls.First()}&time={datetime.AddHours(8).ToString("yyyy年MM月dd日 HH时")}";
-                        string ulr = $"http://cdhabook.teammodel.cn:8805/screen/screenshot-png?width=1920&height=1450&url={HttpUtility.UrlEncode(ulrs,Encoding.UTF8)}&delay=5000";
+                        string ulr = $"http://cdhabook.teammodel.cn:8805/screen/screenshot-png?width=1920&height=1450&url={HttpUtility.UrlEncode(ulrs, Encoding.UTF8)}&delay=5000";
                         string image = "";
                         try
                         {
                             string strs = await _httpClient.CreateClient().GetStringAsync(ulr);
-                            if (!string.IsNullOrWhiteSpace(strs)) {
+                            if (!string.IsNullOrWhiteSpace(strs))
+                            {
                                 JsonElement json = strs.ToObject<JsonElement>();
                                 json.TryGetProperty("url", out JsonElement base64);
-                                using (MemoryStream ms = new MemoryStream(Convert.FromBase64String($"{base64}"))) {
-                                    image = await _azureStorage.GetBlobContainerClient("0-public").UploadFileByContainer( ms, $"visitCnt/{y}{m}{d}", $"{y}{m}{d}{h}.png",false);
+                                using (MemoryStream ms = new MemoryStream(Convert.FromBase64String($"{base64}")))
+                                {
+                                    TimeZoneInfo localTimezone = TimeZoneInfo.Local;
+                                    var Hours = localTimezone.BaseUtcOffset.Hours;
+                                    var nowTime = DateTimeOffset.UtcNow;
+                                    if (Hours!=0)
+                                    {
+                                        //有时差
+                                        nowTime =  DateTimeOffset.UtcNow.AddHours(8-Hours);
+                                    }
+                                    image = await _azureStorage.GetBlobContainerClient("0-public").UploadFileByContainer(ms, $"visitCnt/{nowTime.ToString("yyyyMMdd")}", $"{nowTime.ToString("yyyyMMddHH")}.png", false);
+                                    //image = await _azureStorage.GetBlobContainerClient("0-public").UploadFileByContainer(ms, $"visitCnt/{y}{m}{d}", $"{y}{m}{d}{h}.png", false);
                                 }
-                                   
+
                             }
                         }
-                        catch (Exception ex) { 
-                          
+                        catch (Exception ex)
+                        {
+
                         }
-                        await _dingDing.SendBotMarkdown("防火墙日志记录", $"#### 防火墙日志记录(小时)\n> 记录时间:{datetime.AddHours(8).ToString("yyyy-MM-dd HH")}\n> ![screenshot]({image})\n> ###### 发布时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}" +
+                        await _dingDing.SendBotMarkdown("防火墙日志记录", $"#### 防火墙日志记录(小时)\n> 记录时间:{datetime.AddHours(8).ToString("yyyy-MM-dd HH")}\n> ![screenshot]({image})\n> ###### 发布时间:{DateTime.Now.AddHours(8).ToString("yyyy-MM-dd HH:mm:ss")}" +
                             $" [发布地址]({publishUrl}) \n", GroupNames.醍摩豆服務運維群組);
                     }
 
                     //处理昨天的防火墙日志
-                    if (h.Equals("00"))
+                    var yesterday = datetime.AddHours(8).Hour >= 10 ? $"{datetime.AddHours(8).Hour}" : $"0{datetime.AddHours(8).Hour}";
+                    if (yesterday.Equals("00"))
                     {
                         var pastTime = datetime.AddHours(-1);
                         var ptY = pastTime.Year;
                         var ptM = pastTime.Month >= 10 ? $"{pastTime.Month}" : $"0{pastTime.Month}";
                         var ptD = pastTime.Day >= 10 ? $"{pastTime.Day}" : $"0{pastTime.Day}";
 
-                        string dayPath = $"resourceId=/SUBSCRIPTIONS/73B7F9EF-D8B7-4444-9E8D-D80B43BF3CD4/RESOURCEGROUPS/TEAMMODELCHENGDU/PROVIDERS/MICROSOFT.NETWORK/APPLICATIONGATEWAYS/OSFIREWARE/y={ptY}/m={ptM}/d={ptD}";
-                        var retnDay = await BILogAnalyseService.GetPathAnalyse(_azureStorage, _ipSearcher, _dingDing, dayPath, "LogStorage",timeType:"Day");
+                        string dayPath = $"resourceId=/SUBSCRIPTIONS/73B7F9EF-D8B7-4444-9E8D-D80B43BF3CD4/RESOURCEGROUPS/TEAMMODELCHENGDU/PROVIDERS/MICROSOFT.WEB/SITES/TEAMMODELOS/y={ptY}/m={ptM}/d={ptD}";
+                        var retnDay = await BILogAnalyseService.GetPathAnalyse(_azureStorage, _ipSearcher, _dingDing, dayPath, "LogStorage", timeType: "Day");
 
-                        if (retn.recCnts.IsNotEmpty()) 
+                        if (retn.recCnts.IsNotEmpty())
                         {
                             //一天的统计
                             string dayPublishUrl = $"https://teammodelos.blob.core.chinacloudapi.cn/0-public/api-count.html?url={HttpUtility.UrlEncode(retnDay.saveUrls.First(), Encoding.UTF8)}&time={HttpUtility.UrlEncode(pastTime.ToString("yyyy年MM月dd日"), Encoding.UTF8)}";
@@ -133,18 +154,26 @@ namespace TEAMModelOS.FunctionV4.TimeTrigger
                             try
                             {
                                 string dayStr = await _httpClient.CreateClient().GetStringAsync(dayUrl);
-                                if (!string.IsNullOrWhiteSpace(dayStr)) 
+                                if (!string.IsNullOrWhiteSpace(dayStr))
                                 {
                                     JsonElement dayJson = dayStr.ToObject<JsonElement>();
                                     dayJson.TryGetProperty("url", out JsonElement dayBase64);
-                                    using (MemoryStream dayMs = new(Convert.FromBase64String($"{dayBase64}"))) 
+                                    using (MemoryStream dayMs = new(Convert.FromBase64String($"{dayBase64}")))
                                     {
-                                        dayImage = await _azureStorage.GetBlobContainerClient("0-public").UploadFileByContainer(dayMs, $"visitCnt/{ptY}{ptM}{ptD}", "days.png", false);
+                                        TimeZoneInfo localTimezone = TimeZoneInfo.Local;
+                                        var Hours = localTimezone.BaseUtcOffset.Hours;
+                                        var nowTime = DateTimeOffset.UtcNow;
+                                        if (Hours!=0)
+                                        {
+                                            //有时差
+                                            nowTime =  DateTimeOffset.UtcNow.AddHours(8-Hours);
+                                        }
+                                        dayImage = await _azureStorage.GetBlobContainerClient("0-public").UploadFileByContainer(dayMs, $"visitCnt/{nowTime.ToString("yyyyMMdd")}", "days.png", false);
                                     }
                                 }
                             }
                             catch (Exception ex) { }
-                            await _dingDing.SendBotMarkdown("防火墙日志记录", $"#### 防火墙日志记录(天)\n> 记录时间:{pastTime.AddHours(8).ToString("yyyy-MM-dd")}\n> ![screenshot]({dayImage})\n> ###### 发布时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}" +
+                            await _dingDing.SendBotMarkdown("防火墙日志记录", $"#### 防火墙日志记录(天)\n> 记录时间:{pastTime.ToString("yyyy-MM-dd")}\n> ![screenshot]({dayImage})\n> ###### 发布时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}" +
                             $" [发布地址]({dayPublishUrl}) \n", GroupNames.醍摩豆服務運維群組);
                         }
                     }
@@ -156,7 +185,7 @@ namespace TEAMModelOS.FunctionV4.TimeTrigger
             }
             catch (Exception ex)
             {
-               // await _dingDing.SendBotMsg($"FireWallFileLog 防火墙日志记录: {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}\n{ex.Message}\n{ex.StackTrace}", GroupNames.成都开发測試群組);
+                // await _dingDing.SendBotMsg($"FireWallFileLog 防火墙日志记录: {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}\n{ex.Message}\n{ex.StackTrace}", GroupNames.成都开发測試群組);
             }
         }
 
@@ -171,7 +200,7 @@ namespace TEAMModelOS.FunctionV4.TimeTrigger
         //0 1 0 * * * 一天中00的第 1 分钟
         //0 1 * * * * 一天中每小时的第 1 分钟
         //0 */10 * * * *  每五分钟一次
-        public async Task BIStatsDayDefault([TimerTrigger("0 1 0 * * *")] TimerInfo myTimer, ILogger log) 
+        public async Task BIStatsDayDefault([TimerTrigger("0 1 0 * * *")] TimerInfo myTimer, ILogger log)
         {
             try
             {
@@ -179,7 +208,7 @@ namespace TEAMModelOS.FunctionV4.TimeTrigger
             }
             catch (Exception ex)
             {
-                 await _dingDing.SendBotMsg($"BIStatsDayDefault 定时清理每天的数据: {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}\n{ex.Message}\n{ex.StackTrace}", GroupNames.成都开发測試群組);
+                await _dingDing.SendBotMsg($"BIStatsDayDefault 定时清理每天的数据: {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}\n{ex.Message}\n{ex.StackTrace}", GroupNames.成都开发測試群組);
             }
         }
 
@@ -194,9 +223,11 @@ namespace TEAMModelOS.FunctionV4.TimeTrigger
         //5分钟一次
         public async Task CleanUnusedLessonRecord(
             //[TimerTrigger("0 */10 15-23 * * *")] TimerInfo myTimer, ILogger log
-            ) {
+            )
+        {
             string location = Environment.GetEnvironmentVariable("Option:Location");
-            try {
+            try
+            {
                 if (location.Equals("China") || location.Equals("Global"))
                 {
                     bool lockKey = await _azureRedis.GetRedisClient(8).KeyExistsAsync($"LessonRecord:Unused:Lock:{location}");
@@ -256,7 +287,7 @@ namespace TEAMModelOS.FunctionV4.TimeTrigger
                                                               //100-200
                         var count100_200 = schoolKeys.Where(x => x.count >= 100 && x.count <= 200);//至少500条-1000条
                         counts.AddRange(count100_200.Page(10));//5个学校或个人
-                                                              //51-99
+                                                               //51-99
                         var count51_99 = schoolKeys.Where(x => x.count >= 51 && x.count <= 99);//至少500条-990条
                         counts.AddRange(count51_99.Page(10));//10个学校或个人
                                                              //21-50
@@ -320,7 +351,8 @@ namespace TEAMModelOS.FunctionV4.TimeTrigger
                                 if (items.IsNotEmpty())
                                 {
                                     HashSet<string> set = new HashSet<string>();
-                                    items.ForEach(z => {
+                                    items.ForEach(z =>
+                                    {
                                         var uri = z.Split("/");
                                         if (uri.Length > 1)
                                         {
@@ -344,7 +376,8 @@ namespace TEAMModelOS.FunctionV4.TimeTrigger
                                         {
                                             foreach (var not in notin)
                                             {
-                                                if (!string.IsNullOrWhiteSpace(not)) {
+                                                if (!string.IsNullOrWhiteSpace(not))
+                                                {
                                                     string url = $"records/{not}";
                                                     long id = -1;
                                                     long.TryParse(not, out id);
@@ -367,7 +400,8 @@ namespace TEAMModelOS.FunctionV4.TimeTrigger
                                         }
                                     }
                                 }
-                                if (urls.IsNotEmpty()) {
+                                if (urls.IsNotEmpty())
+                                {
                                     deleteUrls.Add(new KeyValuePair<string, List<string>>($"{idCode.name}-{idCode.id}", urls));
                                 }
                             }
@@ -390,7 +424,8 @@ namespace TEAMModelOS.FunctionV4.TimeTrigger
                         List<string> content = new List<string>();
                         foreach (var url in deleteUrls)
                         {
-                            if (url.Value.IsNotEmpty()) {
+                            if (url.Value.IsNotEmpty())
+                            {
                                 content.Add($"{url.Key}\n   {string.Join("  \t\n", url.Value)}");
                             }
                         }
@@ -398,7 +433,7 @@ namespace TEAMModelOS.FunctionV4.TimeTrigger
                         {
                             //锁定15至少小时。
                             await _azureRedis.GetRedisClient(8).StringSetAsync($"LessonRecord:Unused:Lock:Lock", "用于在清理完所有Blob容器后,锁定不再进行多次清理。");
-                            await _azureRedis.GetRedisClient(8).KeyExpireAsync($"LessonRecord:Unused:Lock:Lock",  DateTime.UtcNow.AddHours(15)   );
+                            await _azureRedis.GetRedisClient(8).KeyExpireAsync($"LessonRecord:Unused:Lock:Lock", DateTime.UtcNow.AddHours(15));
                         }
 
                         if (content.IsNotEmpty())
@@ -406,12 +441,15 @@ namespace TEAMModelOS.FunctionV4.TimeTrigger
                             string str = string.Join("\n", content);
                             await _dingDing.SendBotMsg($"{location}-结束第{unusedLock.field}轮清理冗余的课例记录文件...\n结束时间:{edata}\n耗时:{timeStr}\n清理内容:\n{str}", GroupNames.醍摩豆服務運維群組);
                         }
-                        else {
-                          //  await _dingDing.SendBotMsg($"{location}-结束第{unusedLock.field}轮清理冗余的课例记录文件...\n结束时间:{edata}\n耗时:{timeStr}\n清理内容:暂无", GroupNames.醍摩豆服務運維群組);
+                        else
+                        {
+                            //  await _dingDing.SendBotMsg($"{location}-结束第{unusedLock.field}轮清理冗余的课例记录文件...\n结束时间:{edata}\n耗时:{timeStr}\n清理内容:暂无", GroupNames.醍摩豆服務運維群組);
                         }
                     }
                 }
-            } catch (Exception ex) {
+            }
+            catch (Exception ex)
+            {
                 await _dingDing.SendBotMsg($"{location} 清理时容器出现异常。\n异常信息:{ex.Message},{ex.StackTrace}", GroupNames.醍摩豆服務運維群組);
             }
         }
@@ -448,7 +486,8 @@ namespace TEAMModelOS.FunctionV4.TimeTrigger
         //    await ErrorItemsService.cntStuErrorItemsAsync(_azureRedis, _azureCosmosClient, _dingDing);
         //}
 
-        public class UnusedLock { 
+        public class UnusedLock
+        {
             public int field { get; set; }
             /// <summary>
             /// 0默认未被执行,1 正在执行中
@@ -456,6 +495,6 @@ namespace TEAMModelOS.FunctionV4.TimeTrigger
             public int status { get; set; }
             public IEnumerable<IdCodeCount> item { get; set; } = new List<IdCodeCount>();
         }
-       
+
     }
 }

Plik diff jest za duży
+ 42708 - 0
TEAMModelOS.FunctionV4/latlng.json


+ 18 - 6
TEAMModelOS.FunctionV4/local.settings.json

@@ -17,18 +17,23 @@
     "HaBookAuth:CoreService:clientID": "c7317f88-7cea-4e48-ac57-a16071f7b884",
     "HaBookAuth:CoreService:clientSecret": "kguxh:V.PLmxBdaI@jnrTrDSth]A3346",
     "CoreServiceV1:Cosmos:ConnectionString": "AccountEndpoint=https://corecosmosdbcn.documents.azure.cn:443/;AccountKey=WIFUWgnC6HiPb4yYba5iLR4ZghcA2fqrEvnX00YTA5B4sd7o89sxtbvgFDDUqT2VRkeWrWVTWZ8VbApQtwkJKQ==;", //CSV1大陸正式站(唯讀)
-	"CoreServiceV2:Cosmos:ConnectionString": "AccountEndpoint=https://teammodelostest.documents.azure.cn:443/;AccountKey=S5aG4XbGulK3T1LKzNAraeSuNMvXWf5y6j7ptQHlzVL2jHB07FRNUnmKCChkQ5IeLrJzGfSKIXGnAk3gW30UtQ==;", //CSV2大陸測試站(唯讀)
+    "CoreServiceV2:Cosmos:ConnectionString": "AccountEndpoint=https://teammodelostest.documents.azure.cn:443/;AccountKey=S5aG4XbGulK3T1LKzNAraeSuNMvXWf5y6j7ptQHlzVL2jHB07FRNUnmKCChkQ5IeLrJzGfSKIXGnAk3gW30UtQ==;", //CSV2大陸測試站(唯讀)
     "CoreServiceV2:CosmosCnRead:ConnectionString": "AccountEndpoint=https://corecosmosdb.documents.azure.cn:443/;AccountKey=12C5n8IvXFqsPARUb2YBOUWiU9PksohESlLHgH6cAVajxWBBJIJ6chJusgfrYhxCKgQgSHHxVgHsFPRakhvlPw==;", //CSV2大陸正式站(唯讀)
-    "Option:LocationNum": 1
+    "Option:LocationNum": 1,
+    "Azure:CoreService:delnotification": "https://api2.teammodel.cn/service/delnotification",
+    "Azure:CoreService:deviceinfo": "https://api2.teammodel.cn/oauth2/getdeviceinfos",
+    "Azure:CoreService:getnotification": "https://api2.teammodel.cn/service/getnotification",
+    "Azure:CoreService:sendnotification": "https://api2.teammodel.cn/service/sendnotification"
   }
   //"Values": {
+  //  "AzureWebJobsSecretStorageType": "Blob",
   //  "AzureWebJobsStorage": "DefaultEndpointsProtocol=https;AccountName=teammodellog;AccountKey=lxVDrgs+6rKtmASL3k1WrarrEd5Rk42wS1Mu5+sqQlPya1JLSlFDtnZUvMPeHHe7zlESfn/1NY7CZdGviy2UCw==;EndpointSuffix=core.chinacloudapi.cn",
   //  "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
   //  "Azure:Storage:ConnectionString": "DefaultEndpointsProtocol=https;AccountName=teammodelos;AccountKey=Dl04mfZ9hE9cdPVO1UtqTUQYN/kz/dD/p1nGvSq4tUu/4WhiKcNRVdY9tbe8620nPXo/RaXxs+1F9sVrWRo0bg==;EndpointSuffix=core.chinacloudapi.cn",
   //  "Azure:LogStorage:ConnectionString": "DefaultEndpointsProtocol=https;AccountName=teammodellog;AccountKey=lxVDrgs+6rKtmASL3k1WrarrEd5Rk42wS1Mu5+sqQlPya1JLSlFDtnZUvMPeHHe7zlESfn/1NY7CZdGviy2UCw==;EndpointSuffix=core.chinacloudapi.cn",
-  //  "Azure:ServiceBus:ConnectionString": "Endpoint=sb://coreiotservicebuscnpro.servicebus.chinacloudapi.cn/;SharedAccessKeyName=TEAMModelOS;SharedAccessKey=llRPBMDJG9w1Nnifj+pGhV0g4H2REcq0PjvX2qqpcOg=",
-  //  "Azure:Cosmos:ConnectionString": "AccountEndpoint=https://cdhabookdep-free.documents.azure.cn:443/;AccountKey=JTUVk92Gjsx17L0xqxn0X4wX2thDPMKiw4daeTyV1HzPb6JmBeHdtFY1MF1jdctW1ofgzqkDMFOtcqS46by31A==;",
-  //  "Azure:Redis:ConnectionString": "52.130.252.100:6379,password=habook,ssl=false,abortConnect=False,writeBuffer=10240",
+  //  "Azure:ServiceBus:ConnectionString": "Endpoint=sb://coreservicebuscn.servicebus.chinacloudapi.cn/;SharedAccessKeyName=TEAMModelOS;SharedAccessKey=xO8HcvXXuuEkuFI0KlV5uXs8o6vyuVqTR+ASbPGMhHo=",
+  //  "Azure:Cosmos:ConnectionString": "AccountEndpoint=https://teammodelos.documents.azure.cn:443/;AccountKey=clF73GwPECfP1lKZTCvs8gLMMyCZig1HODFbhDUsarsAURO7TcOjVz6ZFfPqr1HzYrfjCXpMuVD5TlEG5bFGGg==;",
+  //  "Azure:Redis:ConnectionString": "CoreRedisCN.redis.cache.chinacloudapi.cn:6380,password=LyJWP1ORJdv+poXWofAF97lhCEQPg1wXWqvtzXGXQuE=,ssl=True,abortConnect=False",
   //  "Azure:ServiceBus:ActiveTask": "active-task",
   //  "Azure:ServiceBus:ItemCondQueue": "itemcond",
   //  "Azure:ServiceBus:GenPdfQueue": "genpdf",
@@ -37,6 +42,13 @@
   //  "HaBookAuth:CoreAPI": "https://api2.teammodel.cn",
   //  "HaBookAuth:CoreService:clientID": "c7317f88-7cea-4e48-ac57-a16071f7b884",
   //  "HaBookAuth:CoreService:clientSecret": "kguxh:V.PLmxBdaI@jnrTrDSth]A3346",
-  //  "Option:LocationNum": 1
+  //  "CoreServiceV1:Cosmos:ConnectionString": "AccountEndpoint=https://corecosmosdbcn.documents.azure.cn:443/;AccountKey=WIFUWgnC6HiPb4yYba5iLR4ZghcA2fqrEvnX00YTA5B4sd7o89sxtbvgFDDUqT2VRkeWrWVTWZ8VbApQtwkJKQ==;", //CSV1大陸正式站(唯讀)
+  //  "CoreServiceV2:Cosmos:ConnectionString": "AccountEndpoint=https://corecosmosdb.documents.azure.cn:443/;AccountKey=12C5n8IvXFqsPARUb2YBOUWiU9PksohESlLHgH6cAVajxWBBJIJ6chJusgfrYhxCKgQgSHHxVgHsFPRakhvlPw==;", //CSV2大陸測試站(唯讀)
+  //  "CoreServiceV2:CosmosCnRead:ConnectionString": "AccountEndpoint=https://corecosmosdb.documents.azure.cn:443/;AccountKey=12C5n8IvXFqsPARUb2YBOUWiU9PksohESlLHgH6cAVajxWBBJIJ6chJusgfrYhxCKgQgSHHxVgHsFPRakhvlPw==;", //CSV2大陸正式站(唯讀)
+  //  "Option:LocationNum": 1,
+  //  "Azure:CoreService:delnotification": "https://api2.teammodel.cn/service/delnotification",
+  //  "Azure:CoreService:deviceinfo": "https://api2.teammodel.cn/oauth2/getdeviceinfos",
+  //  "Azure:CoreService:getnotification": "https://api2.teammodel.cn/service/getnotification",
+  //  "Azure:CoreService:sendnotification": "https://api2.teammodel.cn/service/sendnotification"
   //}
 }

+ 0 - 1
TEAMModelOS.SDK/DI/AzureCosmos/AzureCosmosExtensions.cs

@@ -16,7 +16,6 @@ using System.Text.Json;
 using System.Net;
 using System.Linq.Expressions;
 using TEAMModelOS.SDK;
-using TEAMModelOS.SDK.Helper.Common.JsonHelper;
 using TEAMModelOS.SDK.Extension;
 using TEAMModelOS.SDK.Models;
 using TEAMModelOS.SDK.Models.Cosmos.OpenEntity;

+ 0 - 1
TEAMModelOS.SDK/DI/AzureStorage/Inner/AzureBlobModel.cs

@@ -4,7 +4,6 @@ using Microsoft.Azure.Cosmos.Table;
 using System;
 using System.ComponentModel.DataAnnotations;
 using System.IO;
-using TEAMModelOS.SDK.Helper.Common.DateTimeHelper;
 
 namespace TEAMModelOS.SDK.Module.AzureBlob.Container
 {

+ 10 - 6
TEAMModelOS.SDK/DI/CoreAPI/CoreAPIHttpService.cs

@@ -389,12 +389,16 @@ lang 语系"zh-cn" ,zh-tw", "en-us"
                                 {
                                     urlAction = "https://test.teammodel.cn/core/process-notify";
                                 }
-                                byte[] byts = Encoding.Unicode.GetBytes(replaceData.ToJsonString());
-                                var  rdata=Convert.ToBase64String(byts);
-                                byte[] bs= Convert.FromBase64String(rdata);
-                                string data1 = Encoding.Unicode.GetString(bs);
-                                rdata = HttpUtility.UrlEncode(rdata, Encoding.UTF8);
-                                urlAction = $"{urlAction}?notifyCode={notifyCode}&data={rdata}";
+                                string dataJson = replaceData.ToJsonString();
+
+                                //byte[] byts = Encoding.Unicode.GetBytes(replaceData.ToJsonString());
+                                //var  rdata=Convert.ToBase64String(byts);
+                                //byte[] bs= Convert.FromBase64String(rdata);
+                                //string data1 = Encoding.Unicode.GetString(bs);
+
+
+                                dataJson = HttpUtility.UrlEncode(dataJson, Encoding.UTF8);
+                                urlAction = $"{urlAction}?notifyCode={notifyCode}&data={dataJson}";
                                 if (msgs.Count == 3)
                                 {
                                     if (msgs[2].Contains("{link}"))

+ 30 - 0
TEAMModelOS.SDK/DI/CoreAPI/Region2LongitudeLatitudeTranslator.cs

@@ -0,0 +1,30 @@
+using Newtonsoft.Json.Linq;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TEAMModelOS.SDK.DI
+{
+    public class Region2LongitudeLatitudeTranslator
+    {
+
+        public readonly JArray regionJson;
+        public Region2LongitudeLatitudeTranslator(string configPath)
+        {
+            if (configPath == null) throw new ArgumentNullException(nameof(configPath));
+            StreamReader streamReader = new StreamReader(new FileStream(Path.Combine(configPath , "latlng.json"), FileMode.Open, FileAccess.Read, FileShare.ReadWrite), Encoding.UTF8);
+            StringBuilder stringBuilder = new StringBuilder();
+            string text;
+            while ((text = streamReader.ReadLine()) != null)
+            {
+                stringBuilder.Append(text.ToString());
+            }
+            streamReader.Close();
+            string input = stringBuilder.ToString();
+            regionJson=  JArray.Parse(input);
+        }
+    }
+}

+ 3 - 0
TEAMModelOS.SDK/DI/HttpTrigger/WebHookHttpTrigger.cs

@@ -100,6 +100,7 @@ namespace TEAMModelOS.SDK.DI
             GroupChange data = null;
             if (json.TryGetProperty("data", out JsonElement _data)){
                 data= _data.ToObject<GroupChange>();
+                data.client="web";
             }
             (List<BizConfig> businessConfigs, List<(List<string> urls, string head, string token, BizConfig config)> webhooks ) = await WebHookService.GetRequestData(_data, _azureCosmos,_azureRedis,_azureStorage);
             if (webhooks.IsNotEmpty() && !string.IsNullOrWhiteSpace(data.school))
@@ -110,6 +111,8 @@ namespace TEAMModelOS.SDK.DI
                     {
                         await WebHookService.Send(new
                         {
+                            data.name,
+                            data.client,
                             data.status,
                             data.type,
                             data.listid,

+ 25 - 90
TEAMModelOS.SDK/Helper/Common/DateTimeHelper/DateTimeHelper.cs

@@ -2,20 +2,10 @@ using System;
 using System.Collections.Generic;
 using System.Text;
 
-namespace TEAMModelOS.SDK.Helper.Common.DateTimeHelper
+namespace TEAMModelOS.SDK
 {
     public static class DateTimeHelper
     {
-        /// <summary>
-        /// 日期转换为时间戳(时间戳单位秒)
-        /// </summary>
-        /// <param name="TimeStamp"></param>
-        /// <returns></returns>
-        public static long ConvertToTimeStamp10(DateTime time)
-        {
-            DateTime Jan1st1970 = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
-            return (long)(time.AddHours(-8) - Jan1st1970).TotalMilliseconds / 1000;
-        }
 
         public static DateTime FromUnixTimestamp(this long unixtime)
         {
@@ -23,102 +13,47 @@ namespace TEAMModelOS.SDK.Helper.Common.DateTimeHelper
             return sTime.AddMilliseconds(unixtime);
         }
 
-        public static DateTime FromUnixTimestampOffSet(this long unixtime, int offset)
-        {
-            DateTime sTime = TimeZoneInfo.ConvertTime(new DateTime(1970, 1, 1), TimeZoneInfo.Utc, TimeZoneInfo.Local);
-            int serverOffset = (int)TimeZoneInfo.Local.GetUtcOffset(DateTime.Now).TotalMinutes;
-            int subOffset = offset - serverOffset;
-            return sTime.AddMilliseconds(unixtime).AddMinutes(subOffset);
-        }
+         
         public static long ToUnixTimestamp(this DateTime datetime)
         {
             //DateTime sTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
             DateTime sTime = TimeZoneInfo.ConvertTime(new DateTime(1970, 1, 1), TimeZoneInfo.Utc, TimeZoneInfo.Local);
             return (long)(datetime - sTime).TotalMilliseconds;
         }
+       
         /// <summary>
-        /// 获取当前cpu振荡时间戳 17位数
-        /// </summary>
-        /// <returns></returns>
-        public static long GetCPUMillisecond()
-        {
-            return DateTime.Now.ToUniversalTime().Ticks - 621355968000000000;
-        }
-        /// <summary>
-        /// 获取标准毫秒级时间戳 13位数
-        /// </summary>
-        /// <returns></returns>
-        public static long GetMillisecond()
-        {
-            return (DateTime.Now.ToUniversalTime().Ticks - 621355968000000000) / 10000;
-        }
-        /// <summary>
-        /// 获取当前年
-        /// </summary>
-        /// <returns></returns>
-        public static int GetCurrentYear()
-        {
-            return DateTime.Now.Year;
-        }
-        /// <summary>
-        /// 获取当前月份
-        /// </summary>
-        /// <returns></returns>
-        public static int GetCurrentMonth()
-        {
-            return DateTime.Now.Month;
-        }
-        /// <summary>
-        /// 获取星期几
-        /// </summary>
-        /// <returns></returns>
-        public static DayOfWeek GetCurrentDayOfWeek()
-        {
-            return DateTime.Now.DayOfWeek;
-        }
-        /// <summary>
-        /// 获取本年第几天
-        /// </summary>
-        /// <returns></returns>
-        public static int GetCurrentDayOfYear()
-        {
-            return DateTime.Now.DayOfYear;
-        }
-        /// <summary>
-        /// 获取当前小时
-        /// </summary>
-        /// <returns></returns>
-        public static int GetCurrentHour()
-        {
-            return DateTime.Now.Hour;
-        }
-        /// <summary>
-        /// 获取当前分钟
+        /// 获得 GMT+8 时间
         /// </summary>
         /// <returns></returns>
-        public static int GetCurrentMinute()
+        public static DateTime GetGMTTime(this DateTime dateTime, int GMT = 8)
         {
-            return DateTime.Now.Minute;
+            //SystemTimeZoneById  :  https://learn.microsoft.com/zh-cn/previous-versions/windows/embedded/ms912391(v=winembedded.11)
+            //TimeZoneInfo easternZone = TimeZoneInfo.FindSystemTimeZoneById("China Standard Time");//设置时区
+            //DateTime easternTime = TimeZoneInfo.ConvertTimeFromUtc(dateTime, easternZone);
+            //return easternTime;
+
+
+            //处理UTC时差
+            TimeZoneInfo localTimezone = TimeZoneInfo.Local;
+            var Hours = localTimezone.BaseUtcOffset.Hours;
+            dateTime = dateTime.AddHours(GMT-Hours);
+            return dateTime;
         }
         /// <summary>
-        /// 获取当前秒
+        /// 默认东八区+8
         /// </summary>
+        /// <param name="dateTime"></param>
+        /// <param name="GMT"></param>
         /// <returns></returns>
-        public static int GetCurrentSecond()
+        public static DateTimeOffset GetGMTTime(this DateTimeOffset dateTime,int GMT=+8)
         {
-            return DateTime.Now.Second;
+            //处理UTC时差
+            TimeZoneInfo localTimezone = TimeZoneInfo.Local;
+            var Hours = localTimezone.BaseUtcOffset.Hours;
+            dateTime = dateTime.AddHours(GMT-Hours);
+            return dateTime;
         }
-        /// <summary>
-        /// 获得 GMT+8 时间
-        /// </summary>
-        /// <returns></returns>
-        public static DateTime ChinaTime()
-        {
 
-            TimeZoneInfo easternZone = TimeZoneInfo.FindSystemTimeZoneById("China Standard Time");//设置时区
-            DateTime easternTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, easternZone);
-            return easternTime;
-        }
         public static  int getDays(int year) { 
             int day = 0;
             for (int i = 0;i<=12;i++) {

+ 1 - 2
TEAMModelOS.SDK/Helper/Common/JsonHelper/JsonParser.cs

@@ -22,7 +22,7 @@ namespace TEAMModelOS.SDK.Helper.Common.JsonHelper
         public static T[] As<T>(this JsonPathNode[] nodes, T prototype) { return nodes.Select(node => node.As<T>()).ToArray(); }
     }
 
-    public delegate object JsonPathScriptEvaluator(string script, object value,IJsonPathContext context);
+    public delegate object JsonPathScriptEvaluator(string script, object value, IJsonPathContext context);
 
     public sealed class JsonPathSelection
     {
@@ -1788,4 +1788,3 @@ namespace TEAMModelOS.SDK.Helper.Common.JsonHelper
 // THE SOFTWARE.
 //
 #endregion
- 

+ 1 - 1
TEAMModelOS.SDK/Helper/Common/JsonHelper/JsonPath/IJsonPathValueSystem.cs

@@ -4,7 +4,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Threading.Tasks;
 
-namespace TEAMModelOS.SDK.Helper.Common.JsonHelper.JsonPath
+namespace TEAMModelOS.SDK
 {
     public interface IJsonPathValueSystem
     {

+ 70 - 0
TEAMModelOS.SDK/Helper/Common/JsonHelper/JsonPath/JsonNetValueSystem.cs

@@ -0,0 +1,70 @@
+using Newtonsoft.Json.Linq;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TEAMModelOS.SDK
+{
+   public sealed class JsonNetValueSystem : IJsonPathValueSystem
+    {
+        public bool HasMember(object value, string member)
+        {
+            if (value is JObject)
+                return (value as JObject).Properties().Any(property => property.Name == member);
+            if (value is JArray)
+            {
+                int index = ParseInt(member, -1);
+                return index >= 0 && index < (value as JArray).Count;
+            }
+            return false;
+        }
+
+        public object GetMemberValue(object value, string member)
+        {
+            if (value is JObject)
+            {
+                var memberValue = (value as JObject)[member];
+                return memberValue;
+            }
+            if (value is JArray)
+            {
+                int index = ParseInt(member, -1);
+                return (value as JArray)[index];
+            }
+            return null;
+        }
+
+        public IEnumerable GetMembers(object value)
+        {
+            var jobject = value as JObject;
+            return jobject.Properties().Select(property => property.Name);
+        }
+
+        public bool IsObject(object value)
+        {
+            return value is JObject;
+        }
+
+        public bool IsArray(object value)
+        {
+            return value is JArray;
+        }
+
+        public bool IsPrimitive(object value)
+        {
+            if (value == null)
+                throw new ArgumentNullException("value");
+
+            return value is JObject || value is JArray ? false : true;
+        }
+
+        private int ParseInt(string s, int defaultValue)
+        {
+            int result;
+            return int.TryParse(s, out result) ? result : defaultValue;
+        }
+    }
+}

+ 1 - 1
TEAMModelOS.SDK/Helper/Common/JsonHelper/JsonPath/JsonPathContext.cs

@@ -9,7 +9,7 @@ using System.Text.Json;
 using System.Text.RegularExpressions;
 using TEAMModelOS.SDK;
 
-namespace TEAMModelOS.SDK.Helper.Common.JsonHelper.JsonPath
+namespace TEAMModelOS.SDK
 {
     /// <summary>
     /// 语法文档  https://www.cnblogs.com/aoyihuashao/p/8665873.html

+ 2 - 3
TEAMModelOS.SDK/Helper/Common/JsonHelper/JsonPath/JsonApiValueSystem.cs

@@ -5,9 +5,8 @@ using System.Linq;
 using System.Text.Json;
 using System.Threading.Tasks;
 
-namespace TEAMModelOS.SDK.Helper.Common.JsonHelper.JsonPath
-{
-    public class JsonApiValueSystem : IJsonPathValueSystem
+namespace TEAMModelOS.SDK { 
+    public class JsonTextValueSystem : IJsonPathValueSystem
     {
         public bool HasMember(object value, string member)
         {

+ 9 - 5
TEAMModelOS.SDK/Helper/Network/IP2Region/IPSearcher.cs

@@ -3,6 +3,7 @@ using System.IO;
 using System.Text;
 using System.Threading.Tasks;
 using TEAMModelOS.SDK.IP2Region;
+using TEAMModelOS.SDK.Models;
 
 namespace TEAMModelOS.SDK
 {
@@ -375,16 +376,20 @@ namespace TEAMModelOS.SDK
         }
         public async Task<string> SearchIpAsync( string ip)
         {
+            if (ip.Contains("::"))
+            {
+                ip = "127.0.0.1";
+            }
             try
             {
                 DataBlock block = await MemorySearchAsync(ip);
                 if (block != null)
                 {
                     string region = block.Region.Replace("0|0|0|0|", "").Replace("0|0|0|", "").Replace("|0|0|0|0", "").Replace("|0|0|0|", "").Replace("|0|0|0", "").Replace("|0|0|", "").Replace("|0|0", "").Replace("|0|", "·").Replace("|0", "").Replace("|", "·");
-                    if (!string.IsNullOrWhiteSpace(region))
-                    {
-                        region = region.Replace("中国·", "").Replace("中国", "").Replace("台湾省", "台湾");
-                    }
+                    //if (!string.IsNullOrWhiteSpace(region))
+                   // {
+                       // region = region.Replace("中国·", "").Replace("中国", "").Replace("台湾省", "台湾");
+                   // }
                     return region;
                 }
                 else { return null; }
@@ -397,7 +402,6 @@ namespace TEAMModelOS.SDK
             catch (Exception) {
                 return null;
             }
-           
         }
         /// <summary>
         /// Get the region throught the ip address with b-tree search algorithm.

+ 1 - 0
TEAMModelOS.SDK/Helper/Network/IP2Region/IPSearcherExtensions.cs

@@ -18,5 +18,6 @@ namespace TEAMModelOS.SDK
             services.TryAddSingleton(new IPSearcher(Path.Combine(path, "ip2region.db")));
             return services;
         }
+        
     }
 }

+ 0 - 1
TEAMModelOS.SDK/Helper/Security/RSACrypt/RsaHelper.cs

@@ -4,7 +4,6 @@ using System.Collections.Generic;
 using System.IO;
 using System.Security.Cryptography;
 using System.Text;
-using TEAMModelOS.SDK.Helper.Common.JsonHelper;
 
 namespace TEAMModelOS.SDK.Helper.Security.RSACrypt
 {

+ 8 - 0
TEAMModelOS.SDK/Models/Cosmos/BI/CurrencyModel.cs

@@ -182,6 +182,14 @@ namespace TEAMModelOS.SDK.Models.Cosmos.BI
         public ManyScActStats actStats { get; set; } = new ManyScActStats();
 
         public ScSRStats srStats { get; set; } = new ScSRStats();
+        /// <summary>
+        /// 城市
+        /// </summary>
+        public string city { get; set; }
+        /// <summary>
+        /// 区/县/郡
+        /// </summary>
+        public string dist { get; set; }
     }
 
     /// <summary>

+ 68 - 1
TEAMModelOS.SDK/Models/Cosmos/BI/RecAppGWInfo.cs

@@ -79,7 +79,8 @@ namespace TEAMModelOS.SDK.Models.Cosmos.BI
         public string operationName { get; set; }
         public string time { get; set; }
         public string category { get; set; }
-        public Properties properties { get; set; }
+        public PropertiesWeb properties { get; set; }
+        public string resourceId { get; set; }
     }
 
     /// <summary>
@@ -102,4 +103,70 @@ namespace TEAMModelOS.SDK.Models.Cosmos.BI
         public string hostname { get; set; }
         public string transactionId { get; set; }
     }
+    public record PropertiesWeb
+    {
+        /// <summary>
+        /// 
+        /// </summary>
+        public string CsMethod { get; set; }
+        /// <summary>
+        /// 
+        /// </summary>
+        public string CsUriStem { get; set; }
+        /// <summary>
+        /// 
+        /// </summary>
+        public string SPort { get; set; }
+        /// <summary>
+        /// 
+        /// </summary>
+        public string CIp { get; set; }
+        /// <summary>
+        /// 
+        /// </summary>
+        public string UserAgent { get; set; }
+        /// <summary>
+        /// 
+        /// </summary>
+        public string CsHost { get; set; }
+        /// <summary>
+        /// 
+        /// </summary>
+        public int ScStatus { get; set; }
+        
+        /// <summary>
+        /// 
+        /// </summary>
+        public int ScBytes { get; set; }
+        /// <summary>
+        /// 
+        /// </summary>
+        public int CsBytes { get; set; }
+        /// <summary>
+        /// 
+        /// </summary>
+        public int TimeTaken { get; set; }
+        /// <summary>
+        /// 
+        /// </summary>
+        public string Result { get; set; }
+        /// <summary>
+        /// 
+        /// </summary>
+        public string Cookie { get; set; }
+        /// <summary>
+        /// 
+        /// </summary>
+        public string CsUriQuery { get; set; }
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <summary>
+        /// 
+        /// </summary>
+        public string Referer { get; set; }
+        /// <summary>
+        /// 
+        /// </summary>
+    }
 }

+ 8 - 0
TEAMModelOS.SDK/Models/Cosmos/BI/StatsInfo.cs

@@ -141,6 +141,14 @@ namespace TEAMModelOS.SDK.Models.Cosmos.BI
         /// 研修统计
         /// </summary>
         public StudyStats study { get; set; } = new StudyStats();
+        /// <summary>
+        /// 城市
+        /// </summary>
+        public string city { get; set; }
+        /// <summary>
+        /// 区/县/郡
+        /// </summary>
+        public string dist { get; set; }
     }
 
     /// <summary>

+ 105 - 2
TEAMModelOS.SDK/Models/Cosmos/Common/Activity.cs

@@ -484,6 +484,16 @@ namespace TEAMModelOS.SDK.Models
         /// file  sokrates
         /// </summary>
         public string type { get; set; }
+
+        public  HashSet<string> uploadType { get; set; }= new HashSet<string>();
+        /// <summary>
+        /// 必须上传的类型
+        /// </summary>
+        public HashSet<string> uploadTypeNecessary { get; set; } = new HashSet<string>();
+        /// <summary>
+        /// 上传类型数量限制0 不限制。1 限制任意一种, 当uploadType.count==limit,则表示 所有类型都需要上传。 即 当uploadType[ file,sokrates,lesson],limit==3  则所有类型均要上传。
+        /// </summary>
+        public int limit { get; set; } 
         /// <summary>
         /// ["file"], 提交作品的格式
         /// </summary>
@@ -612,6 +622,7 @@ namespace TEAMModelOS.SDK.Models
         /// </summary>
         public int contestUpload { get; set; }
         public string uploadType { get; set; }
+        public HashSet<string> uploadTypes { get; set; } = new HashSet<string>();
         public long uploadTime { get; set; } = -1;
     }
 
@@ -747,6 +758,7 @@ namespace TEAMModelOS.SDK.Models
         public List<ContestAttachment> files { get; set; } = new List<ContestAttachment>();
         public List<ContestAttachment> lessons { get; set; } = new List<ContestAttachment>();
         public List<ContestSokrates> sokrates { get; set; } = new List<ContestSokrates>();
+        public List<ContestUploadComplex> complexes { get; set; } = new List<ContestUploadComplex>();
         /// <summary>
         /// file  sokrates,lesson
         /// </summary>
@@ -761,6 +773,8 @@ namespace TEAMModelOS.SDK.Models
         public string name { get; set; }
     }
 
+
+
     public class EnrollUpload
     {
 
@@ -769,11 +783,12 @@ namespace TEAMModelOS.SDK.Models
         public List<ContestAttachment> files { get; set; } = new List<ContestAttachment>();
         public List<ContestAttachment> lessons { get; set; } = new List<ContestAttachment>();
         public List<ContestSokrates> sokrates { get; set; } = new List<ContestSokrates>();
-
+        public List<ContestUploadComplex> complexes { get; set; } = new List<ContestUploadComplex>();
         /// <summary>
         /// file  sokrates
         /// </summary>
         public string type { get; set; }
+        public HashSet<string> uploadType { get; set; } = new HashSet<string>();
         /// <summary>
         ///作品id
         /// </summary>
@@ -907,6 +922,8 @@ namespace TEAMModelOS.SDK.Models
         /// null没有上传模块的默认状态,file文件  sokrates 苏格拉底
         /// </summary>
         public string uploadContestType { get; set; }
+
+        public HashSet<string> uploadContestTypes { get; set; } = new HashSet<string>();
         /// <summary>
         /// 作品分数
         /// </summary>
@@ -966,7 +983,7 @@ namespace TEAMModelOS.SDK.Models
         /// <summary>
         /// sokrates 苏格拉底 ,file 作品
         /// </summary>
-        public List<string> uploadTypes { get; set; } = new List<string>();
+        public HashSet<string> uploadTypes { get; set; } = new HashSet<string>();
         /// <summary>
         /// 上传文件和苏格拉底链接的数量
         /// </summary>
@@ -1072,6 +1089,92 @@ namespace TEAMModelOS.SDK.Models
         public List<string> tag { get; set; } = new List<string>();
         public string lessonId { get; set; }
     }
+    public class ContestUploadComplex
+    {
+        /// <summary>
+        ///  file  sokrates,lesson
+        /// </summary>
+        public string type { get; set; }
+
+        /// <summary>
+        /// 课例和苏格拉底作品的复合结构
+        /// </summary>
+        public LessonSokrates lessonSokrates { get; set; }
+        /// <summary>
+        /// 文件类型的作品
+        /// </summary>
+        public ContestAttachment uploadFile { get; set; }
+    }
+
+    public class LessonSokrates : LessonRecord
+    {
+        #region 扩展公共属性
+        /// <summary>
+        /// 课例,苏格拉底的附件,用于存放课件等额外的信息
+        /// </summary>
+        public List<ContestAttachment> attachments { get; set; } = new List<ContestAttachment>();
+        #endregion  扩展公共属性
+
+        #region 苏格拉底属性
+        /// <summary>
+        /// 地址
+        /// </summary>
+        public string url { get; set; }
+
+        /// <summary>
+        /// 苏格拉底报告
+        /// </summary>
+        public string report { get; set; }
+        /// <summary>
+        /// 课例ID
+        /// </summary>
+        public string recordId { get; set; }
+        /// <summary>
+        /// 封面
+        /// </summary>
+        public string poster { get; set; }
+
+        #endregion 苏格拉底属性
+
+        #region 课例属性
+        /// <summary>
+        /// 是否展示课例历程
+        /// </summary>
+        public int showProcess { get; set; }
+        #endregion 课例属性
+        /*
+         #region  苏格拉底和课例类型作品的公共属性
+        /// <summary>
+        /// 课例名称
+        /// </summary>
+        public string name { get; set; }
+        /// <summary>
+        /// 教师id 
+        /// </summary>
+        public string tmdid { get; set; }
+
+        /// <summary>
+        /// 教师醍摩豆id名称
+        /// </summary>
+        public string tmdname { get; set; }
+        /// <summary>
+        /// 教师醍摩豆id名称
+        /// </summary>
+        public string tmdpicture { get; set; }
+
+        /// <summary>
+        ///必填 开始时间(时间戳) 1606393763434
+        /// </summary>
+        public long startTime { get; set; }
+        /// <summary>
+        /// 视频播放时长
+        /// </summary>
+        public double duration { get; set; }
+
+        #endregion 苏格拉底和课例类型作品的公共属性
+         */
+    }
+
 
     public class ContestSokrates
     { 

+ 6 - 1
TEAMModelOS.SDK/Models/Cosmos/Common/ArtEvaluation.cs

@@ -68,7 +68,7 @@ namespace TEAMModelOS.SDK.Models.Cosmos.Common
         //发布层级 0校级,1区级
         public int? publish { get; set; } = 0;
         public List<ArtSubject> subjects { get; set; } = new List<ArtSubject>();
-        public PeriodSimple period { get; set; }
+        public ArtPeriod period { get; set; } = new ArtPeriod();
         public string periodType { get; set; }
         public List<LostStudent> lost { get; set; } = new List<LostStudent>();
         //用来判定是否已经处理过缺考人数逻辑标识
@@ -98,6 +98,11 @@ namespace TEAMModelOS.SDK.Models.Cosmos.Common
         public string id { get; set; }
         public string name { get; set; }
     }
+    public class ArtPeriod
+    {
+        public string id { get; set; }
+        public string name { get; set; }
+    }
     public class Acs {
         [JsonPropertyName("infoId")]
         public string infoId { get; set; }

+ 38 - 0
TEAMModelOS.SDK/Models/Cosmos/Common/GroupList.cs

@@ -56,6 +56,11 @@ namespace TEAMModelOS.SDK.Models
         /// 加入人数200人,学生加入已满200 自动关闭加入。可手动解除限制,开启审核时,关闭人数上限设置机制
         /// </summary>
         public int limitCount { get; set; } = 200;
+
+        /// <summary>
+        /// 自选座号 0 不允许,1 允许
+        /// </summary>
+        public int optNo { get; set; }
         /// <summary>
         /// 二维码 天数
         /// </summary>
@@ -124,6 +129,10 @@ namespace TEAMModelOS.SDK.Models
         /// </summary>
         public int limitCount { get; set; } = 200;
         /// <summary>
+        /// 自选座号 0 不允许,1 运行
+        /// </summary>
+        public int optNo { get; set; }
+        /// <summary>
         /// 二维码过期时间
         /// </summary>
         public long qrcodeExpire { get; set; }
@@ -165,6 +174,10 @@ namespace TEAMModelOS.SDK.Models
         public int graduate { get; set; } = 0;
         public int year { get; set; }= 0;
         public string gender { get; set; }
+        /// <summary>
+        /// 0 自动的座号和irs,1 手动的irs
+        /// </summary>
+        public int manual { get; set; }
     }
 
     /// <summary>
@@ -254,6 +267,10 @@ namespace TEAMModelOS.SDK.Models
         public List<string> groupListIds { get; set; } = new List<string>();
         public int year { get; set; }
         public string periodId { get; set; }
+        /// <summary>
+        /// 0 自动的座号和irs,1 手动的irs
+        /// </summary>
+        public int manual { get; set; }
 
     }
     public class GroupListGrp
@@ -283,6 +300,7 @@ namespace TEAMModelOS.SDK.Models
             this.qrcodeDays = groupList.qrcodeDays;
             this.review=groupList.review;
             this.limitCount = groupList.limitCount;
+            this.optNo=groupList.optNo;
             this.grades = groupList.grades;
 
         }
@@ -310,6 +328,7 @@ namespace TEAMModelOS.SDK.Models
             this.qrcodeDays = groupList.qrcodeDays;
             this.review=groupList.review;
             this.limitCount = groupList.limitCount;
+            this.optNo= groupList.optNo;
             this.grades = groupList.grades;
 
         }
@@ -357,6 +376,10 @@ namespace TEAMModelOS.SDK.Models
         /// </summary>
         public int limitCount { get; set; } = 200;
         /// <summary>
+        /// 自选座号 0 不允许,1 运行
+        /// </summary>
+        public int optNo { get; set; }
+        /// <summary>
         /// 二维码过期时间
         /// </summary>
         public long qrcodeExpire { get; set; }
@@ -401,6 +424,7 @@ namespace TEAMModelOS.SDK.Models
             this.qrcodeDays = groupList.qrcodeDays;
             this.review=groupList.review;
             this.limitCount = groupList.limitCount;
+            this.optNo= groupList.optNo;
             this.grades = groupList.grades;
             this.graduate = groupList.graduate;
         }
@@ -448,6 +472,10 @@ namespace TEAMModelOS.SDK.Models
         /// </summary>
         public int limitCount { get; set; } = 200;
         /// <summary>
+        /// 自选座号 0 不允许,1 运行
+        /// </summary>
+        public int optNo { get; set; }
+        /// <summary>
         /// 二维码过期时间
         /// </summary>
         public long qrcodeExpire { get; set; }
@@ -566,6 +594,10 @@ namespace TEAMModelOS.SDK.Models
         /// </summary>
         public int limitCount { get; set; } = 200;
         /// <summary>
+        /// 自选座号 0 不允许,1 运行
+        /// </summary>
+        public int optNo { get; set; }
+        /// <summary>
         /// 二维码过期时间
         /// </summary>
         public long qrcodeExpire { get; set; }
@@ -576,4 +608,10 @@ namespace TEAMModelOS.SDK.Models
         public HashSet<int> grades { get; set; } = new HashSet<int>();
     }
 
+    public class GroupListWithCourseTaskId
+    {
+        public GroupList groupList { get; set; }
+        public string CourseTaskId { get; set; }
+    }
+
 }

+ 16 - 0
TEAMModelOS.SDK/Models/Cosmos/Common/Inner/BaseItem.cs

@@ -65,5 +65,21 @@ namespace TEAMModelOS.SDK.Models
         /// </summary>
         public int source { get; set; }
         public string tag { get; set; }
+        /// <summary>
+        /// 指定回答類型 (五種, 若沒有 anserType 就是原本默認的型態, client端會以文字為主來做界面)
+        ///  "text", //文字
+        ///  "text_Image",  //畫記
+        ///  "image", //圖片
+        ///  "file", //文檔
+        /// </summary>
+        public string answerType { get; set; } = "text";
+        /// <summary>
+        /// "useAutoScore": false,  //口說自動評分, 
+        /// </summary>
+        public bool useAutoScore {  get; set; }
+        /// <summary>
+        /// "answerLang": "en-US", //指定口說語言類型, 默認是英語, 英語(en-US), 日語(ja-JP), 韓語(ko-KR), 粵語(zh-HK), 普通話(zh) 
+        /// </summary>
+        public string answerLang { get; set; } = "en-US";
     }
 }

+ 5 - 0
TEAMModelOS.SDK/Models/Cosmos/Common/Inner/GroupChange.cs

@@ -56,5 +56,10 @@ namespace TEAMModelOS.SDK.Models
         /// </summary>
         public string status { get; set; } = "upsert";
         public string periodId { get; set; }
+        /// <summary>
+        /// web  hiteach hita
+        /// </summary>
+        public string client { get; set; }
+        public string name { get; set; }
     }
 }

+ 2 - 0
TEAMModelOS.SDK/Models/Cosmos/Common/ItemInfo.cs

@@ -31,5 +31,7 @@ namespace TEAMModelOS.SDK.Models
         public string blob { get; set; }
         //记录试题大小
         public long? size { get; set; } = 0;
+       
+       
     }
 }

+ 2 - 2
TEAMModelOS.SDK/Models/Cosmos/Common/LearnRecord.cs

@@ -31,9 +31,9 @@ namespace TEAMModelOS.SDK.Models.Cosmos.Common
         public string Desc { get; set; }
         // 知識點陣列, 裡面放字串, 相當於關鍵詞
         public List<string> Points { get; set; }
-        // 知識點陣列, 裡面放字串, 相當於關鍵詞
+        //正確答案, 複選可以放多個
         public object Correct { get; set; }
-        // 知識點陣列, 裡面放字串, 相當於關鍵詞
+        //選項內容, 純文字, 沒有就放空的
         public List<ChoicesItem> Choices { get; set; }
         // 考試卷題數, 若無放 null
         public int? ExamQuesQty { get; set; }

+ 4 - 4
TEAMModelOS.SDK/Models/Cosmos/School/Course.cs

@@ -154,18 +154,18 @@ namespace TEAMModelOS.SDK.Models
     }
 
     public class CourseDtoImpt {
-        public List<CourseDto> courses { get; set; } = new List<CourseDto>();
+        public List<CourseOldDto> courses { get; set; } = new List<CourseOldDto>();
     }
-    public class CourseDto
+    public class CourseOldDto
     {
         public string id { get; set; }
         [Required(ErrorMessage = "{0} 课程的名称必须填写")]
         public string name { get; set; }
         public string desc { get; set; }
         public string no { get; set; }
-        [Required(ErrorMessage = "{0} 课程的科目id必须填写"), RegularExpression(@"[0-9a-zA-Z]{8}(-[0-9a-zA-Z]{4}){3}-[0-9a-zA-Z]{12}",ErrorMessage ="科目的uuid格式错误!")]
+        [Required(ErrorMessage = "{0} 课程的科目id必须填写"), RegularExpression(@"[0-9a-zA-Z]{8}(-[0-9a-zA-Z]{4}){3}-[0-9a-zA-Z]{12}", ErrorMessage = "科目的uuid格式错误!")]
         public string subjectId { get; set; }
-   
+
         public string subjectName { get; set; }
         public string periodId { get; set; }
     }

+ 20 - 0
TEAMModelOS.SDK/Models/Cosmos/School/CourseBase.cs

@@ -425,4 +425,24 @@ namespace TEAMModelOS.SDK.Models
         /// </summary>
         public string teacherIdChanged { get; set; }
     }
+    public class CourseStudentDto
+    {
+        public CourseBase courseBase { get; set; }
+        public List<CourseTask> courseTasks { get; set; } = new List<CourseTask>();
+
+    }
+    public class CourseDto
+    {
+        public CourseBase courseBase { get; set; }
+        public List<CourseTaskDto> courseTasks { get; set; } = new List<CourseTaskDto>();
+
+    }
+    public class CourseTaskDto
+    {
+        public CourseTask courseTask { get; set; }
+        /// <summary>
+        /// teacher主任课教师,assistant,协同教师
+        /// </summary>
+        public string type { get; set; }
+    }
 }

+ 1 - 0
TEAMModelOS.SDK/Models/Cosmos/School/Debate.cs

@@ -10,6 +10,7 @@ namespace TEAMModelOS.SDK.Models
     /// </summary>
     public class Debate : CosmosEntity
     {
+        public string scope { get; set; }
         public string userType { get; set; }
         ///id设计  uuid
         /// code 设计  Debate-hbcn

+ 103 - 0
TEAMModelOS.SDK/Models/Dtos/Accumulate.cs

@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TEAMModelOS.SDK.Models.Dtos
+{
+    public class Accumulate
+    {
+
+        /// <summary>
+        /// course,课程
+        /// exam 评测
+        /// homework 作业
+        /// login_school_student
+        /// login_student
+        /// login_school_teacher
+        /// login_teacher
+        /// </summary>
+        public string key { get; set; }
+        /// <summary>
+        /// 教师,学校管理员,系统管理员(包含钉钉)
+        /// </summary>
+        public string target { get; set; }
+        /// <summary>
+        /// 业务id
+        /// </summary>
+        public string id { get; set; }
+        /// <summary>
+        /// 名称
+        /// </summary>
+        public string name { get; set; }
+        /// <summary>
+        /// teacher发送给教师的  school 发送给学校管理员的   system发送给系统管理员的
+        /// </summary>
+        public string scope { get; set; }
+        public int count { get; set; } = 1;
+        /// <summary>
+        /// web,hiteach,hita
+        /// </summary>
+        public string client {  get; set; }
+
+        //Accumulate:Keys:Day
+
+        //Accumulate:Keys:Week
+
+        /*加入的人数
+         * Accumulate:Daily:teacher:grouplist:yyyyddMM:==>1595321354-grouplistId=>count
+        key:grouplist
+        target:1595321354
+        id:grouplistId
+        scope:teacher
+        name:名单名称
+        count:8
+         */
+        /*作业提交人数
+         * Accumulate:Daily:teacher:homework:yyyyddMM:==>1595321354-homeworkId=>count
+        key:homework
+        target:1595321354
+        id:homeworkId
+        scope:teacher
+        name:作业名称
+        count:8
+         */
+        /*评测完成人数
+         * Accumulate:Daily:teacher:exam:yyyyddMM:==>1595321354-examId=>count
+        key:exam
+        target:1595321354
+        id:examId
+        scope:teacher
+        name:作业名称
+        count:8
+         */
+        /*学生登入人数,scope学校的统一发送给管理员scope:system,发送给系统管理员,及钉钉
+        * Accumulate:Daily:school:login_student:yyyyddMM:==>hbcn-hbcn=>count
+        key:login_student
+        target:hbcn,root
+        id:hbcn,ies
+        scope:school,system
+        name:学校名称
+        count:8
+        */
+        /*教师登入人数,scope学校的统一发送给管理员scope:system,发送给系统管理员,及钉钉
+        * Accumulate:Daily:school:login_teacher:yyyyddMM:==>hbcn-hbcn=>count
+        key:login_teacher
+        target:hbcn,root
+        id:hbcn,ies
+        scope:school,system
+        name:学校名称
+        count:8
+        */
+        /*开课数量,scope学校的统一发送给管理员,scope:system,发送给系统管理员,及钉钉
+       * Accumulate:Daily:school:lesson:yyyyddMM:==>hbcn-hbcn=>count
+       key:lesson
+       target:hbcn,root
+       id:hbcn,ies
+       scope:school,system
+       name:学校名称
+       count:8
+       */
+    }
+}

+ 19 - 9
TEAMModelOS.SDK/Models/Service/BI/BILogAnalyseService.cs

@@ -72,7 +72,7 @@ namespace TEAMModelOS.SDK.Models.Service.BI
 
                 RecCnt saveCnts = new();
 
-                List<RecAppGWInfo> recInfo = aGInfos.Select(s => new RecAppGWInfo { hour = cHour, ip = s.properties.clientIp, api = s.properties.requestUri.Split("?").ToList().Count() > 1 ? s.properties.requestUri.Split("?").ToList()[0] : s.properties.requestUri, hostName = s.properties.hostname }).ToList();
+                List<RecAppGWInfo> recInfo = aGInfos.Select(s => new RecAppGWInfo { hour = cHour, ip = s.properties.CIp, api = s.properties.CsUriStem.Split("?").ToList().Count() > 1 ? s.properties.CsUriStem.Split("?").ToList()[0] : s.properties.CsUriStem, hostName = s.properties.CsHost }).ToList();
 
                 List<RecApiCnt> apiCnt = recInfo.GroupBy(a => a.api).Select(g => new RecApiCnt { api = g.Key, count = g.Count(), hour = cHour, hostName = g.Select(h => h.hostName).Distinct().ToList(), ip = g.Select(i => i.ip).Distinct().ToList() }).ToList();
                 saveCnts.apiCnt = apiCnt;
@@ -101,8 +101,15 @@ namespace TEAMModelOS.SDK.Models.Service.BI
          {
             List<RecCnt> recCnts = new();
             List<string> urls = new();
-
+            TimeZoneInfo localTimezone = TimeZoneInfo.Local;
+            var Hours = localTimezone.BaseUtcOffset.Hours;
             DateTimeOffset dtime = DateTimeOffset.UtcNow;
+            if (Hours!=0)
+            {
+                //有时差
+                dtime =  DateTimeOffset.UtcNow.AddHours(8-Hours);
+            }
+          
             string cDay = dtime.ToString("yyyyMMdd");
 
             //天api
@@ -115,7 +122,7 @@ namespace TEAMModelOS.SDK.Models.Service.BI
 
             try 
             {
-                var blobClient = _azureStorage.GetBlobContainerClient($"insights-logs-applicationgatewayfirewalllog", name: connectName);
+                var blobClient = _azureStorage.GetBlobContainerClient($"insights-logs-appservicehttplogs", name: connectName);
                 await foreach (BlobItem blobItem in blobClient.GetBlobsAsync(BlobTraits.None, BlobStates.None, path))
                 {
                     StringBuilder visits = new("[");
@@ -145,12 +152,15 @@ namespace TEAMModelOS.SDK.Models.Service.BI
                     List<AGInfo> tempsert = new List<AGInfo>();
                     List<AGInfo> aGInfos = new List<AGInfo>();
 
-                    tempAinfos.ForEach(item =>
+                    tempAinfos.FindAll(x=>x.properties.CsMethod.Equals("POST"))?.ForEach(item =>
                     {
-                        string requestUri = item.properties.requestUri;
-                        var isType = StaticValue.suffixName.Where(k => requestUri.Contains(k)).ToList();
-                        if (isType.Count == 0)
-                            aGInfos.Add(item);
+                        string requestUri = item.properties.CsUriStem;
+                        if (!string.IsNullOrWhiteSpace(requestUri)) {
+                            var isType = StaticValue.suffixName.Where(k => requestUri.Contains(k)).ToList();
+                            if (isType.Count == 0)
+                                aGInfos.Add(item);
+                        }
+                        
                     });
 
                     //foreach (var item in tempAinfos)
@@ -172,7 +182,7 @@ namespace TEAMModelOS.SDK.Models.Service.BI
 
                     RecCnt saveCnts = new();
 
-                    List<RecAppGWInfo> recInfo = aGInfos.Select(s => new RecAppGWInfo { hour = cHour, ip = s.properties.clientIp, api = s.properties.requestUri.Split("?").ToList().Count() > 1 ? s.properties.requestUri.Split("?").ToList()[0] : s.properties.requestUri, hostName = s.properties.hostname,minute = DateTimeOffset.Parse(s.time).ToString("mm")}).ToList();
+                    List<RecAppGWInfo> recInfo = aGInfos.Select(s => new RecAppGWInfo { hour = cHour, ip = s.properties.CIp, api = s.properties.CsUriStem.Split("?").ToList().Count() > 1 ? s.properties.CsUriStem.Split("?").ToList()[0] : s.properties.CsUriStem, hostName = s.properties.CsHost,minute = DateTimeOffset.Parse(s.time).ToString("mm")}).ToList();
 
                     if (timeType.Equals("Hour"))
                     {

+ 24 - 2
TEAMModelOS.SDK/Models/Service/Common/ActivityService.cs

@@ -291,6 +291,11 @@ namespace TEAMModelOS.SDK
                         {
                             count += leader.upload.lessons.Count;
                         }
+
+                        if (leader?.upload!=null  && leader.upload.complexes.IsNotEmpty()) 
+                        {
+                            count+=leader.upload.complexes.Count();
+                        }
                         if (count <= 0)
                         {
                             uploadId = enroll.contest.cipher;
@@ -314,7 +319,8 @@ namespace TEAMModelOS.SDK
                         {
                             uploadId = leader.upload?.uploadId,
                             name = $"{leader.schoolName}-{name}",
-                            uploadTypes = new List<string> { leader?.upload.type },
+                           // uploadTypes = new List<string> { leader?.upload.type },
+                            uploadTypes =leader.upload!=null ? new HashSet<string>() { leader.upload.type } : leader.upload.uploadType,
                             count = count,
                             cipher = leader.contest?.cipher,
                             type = 1,
@@ -378,6 +384,10 @@ namespace TEAMModelOS.SDK
                         {
                             count += enroll.upload.lessons.Count;
                         }
+                        if (enroll.upload != null  && enroll.upload.complexes.IsNotEmpty())
+                        {
+                            count+=enroll.upload.complexes.Count();
+                        }
                         if (count <= 0)
                         {
                             available = 5;
@@ -401,7 +411,8 @@ namespace TEAMModelOS.SDK
                         {
                             uploadId = uploadId,
                             name = $"{enroll.schoolName}-{name}",
-                            uploadTypes = new List<string> { enroll.upload?.type },
+                            //uploadTypes = new List<string> { enroll.upload?.type },
+                            uploadTypes =enroll.upload!=null ? new HashSet<string>() { enroll.upload.type } :enroll.upload.uploadType,
                             count = count,
                             cipher = enroll.contest?.cipher,
                             type = 1,
@@ -855,12 +866,23 @@ namespace TEAMModelOS.SDK
                             activityDto.contestType=enroll.contest.type;
 
                         }
+
+
                         if (enroll.upload!=null  && (enroll.upload.files.IsNotEmpty()  ||  enroll.upload.sokrates.IsNotEmpty() ||  enroll.upload.lessons.IsNotEmpty())  )
                         {
                             activityDto.contestUpload=1;
                             activityDto.uploadTime=enroll.upload.uploadTime;
                             activityDto.uploadType=enroll.upload.type;
                         }
+
+
+
+                        if (enroll.upload!=null  &&enroll.upload.complexes.IsNotEmpty())
+                        {
+                            activityDto.contestUpload=1;
+                            activityDto.uploadTime=enroll.upload.uploadTime;
+                            activityDto.uploadTypes=enroll.upload.uploadType;
+                        }
                     }
                 }
             }

+ 0 - 1
TEAMModelOS.SDK/Models/Service/FixDataService.cs

@@ -16,7 +16,6 @@ using System.Dynamic;
 using Newtonsoft.Json;
 using TEAMModelOS.Models;
 using Azure.Storage.Blobs.Models;
-using TEAMModelOS.SDK.Helper.Common.DateTimeHelper;
 using DinkToPdf.Contracts;
 
 namespace TEAMModelOS.SDK.Models.Service

+ 300 - 12
TEAMModelOS.SDK/Models/Service/GroupListService.cs

@@ -16,6 +16,8 @@ using TEAMModelOS.SDK.Models;
 using System.Net.Http;
 using DocumentFormat.OpenXml.Drawing.Charts;
 using System.Reflection;
+using TEAMModelOS.SDK.Models.Service;
+using OpenXmlPowerTools;
 
 
 namespace TEAMModelOS.SDK
@@ -23,11 +25,239 @@ namespace TEAMModelOS.SDK
     public class GroupListService
     {
 
+        /// <summary>
+        /// 根据教师id获取执教的名单及协同身份的名单
+        /// </summary>
+        /// <param name="_coreAPIHttpService"></param>
+        /// <param name="client"></param>
+        /// <param name="_dingDing"></param>
+        /// <param name="tmdid"></param>
+        /// <param name="school"> 可选,不填则只获取个人课程的名单</param>
+        /// <param name="periodId">school,传递,则 periodId必传 </param>
+        /// <param name="time"> 时间,以便于获取某一学期的数据</param>
+        /// <returns></returns>
+        public static async Task<List<CourseGroupList>> GetTeacherTeachGroupList(CoreAPIHttpService _coreAPIHttpService, CosmosClient client, DingDing _dingDing, string tmdid, string school, string periodId,  long time = -1)
+        {
+            string sql = $"SELECT distinct value c FROM c  join b in c.schedules where c.pk='CourseTask' and (ARRAY_CONTAINS(b.assistants,'{tmdid}')or  b.teacherId  ='{tmdid}' )";
+            List<KeyValuePair<string, CourseTask>> schoolTeacherTask = new List<KeyValuePair<string, CourseTask>>();
+            List<KeyValuePair<string, CourseTask>> schoolAssistantTask = new List<KeyValuePair<string, CourseTask>>();
+
+            List<KeyValuePair<string, CourseTask>> privateTeacherTask = new List<KeyValuePair<string, CourseTask>>();
+            List<KeyValuePair<string, CourseTask>> privateAssistantTask = new List<KeyValuePair<string, CourseTask>>();
+            List<CourseDto> schoolCourses = new List<CourseDto>();
+            List<CourseDto> teahcerCourses = new List<CourseDto>();
+            List<string> groupIds = new List<string>();
+            if (!string.IsNullOrWhiteSpace(school)   && !string.IsNullOrWhiteSpace(periodId)) 
+            {
+              
+                School schoolBase = await client.GetContainer(Constant.TEAMModelOS, Constant.School).ReadItemAsync<School>(school, new PartitionKey("Base"));
+                var data =  SchoolService.GetSemester(schoolBase.period.Find(x => x.id.Equals(periodId)), time);
+                sql = $"{sql} and  c.year={data.studyYear} and c.semesterId='{data.currSemester.id}'";
+                HashSet<string> courseIds = new HashSet<string>();
+                var resultSchool = await client.GetContainer(Constant.TEAMModelOS, Constant.School).GetList<CourseTask>(sql, $"CourseTask-{school}");
+                if (resultSchool.list.IsNotEmpty())
+                {
+                    resultSchool.list.ForEach(x => {
+                        var schedulesTeacher = x.schedules.Where(z => !string.IsNullOrWhiteSpace(z.teacherId)   && z.teacherId.Equals(tmdid));
+                        if (schedulesTeacher.Any())
+                        {
+                            courseIds.Add(x.courseId);
+                            CourseTask courseTask = x.ToJsonString().ToObject<CourseTask>();
+                            courseTask.schedules=schedulesTeacher.ToList();
+                            schoolTeacherTask.Add(new KeyValuePair<string, CourseTask>(x.courseId, courseTask));
+                            groupIds.AddRange(schedulesTeacher.Where(z => !string.IsNullOrWhiteSpace(z.groupId)).Select(x => x.groupId));
+                        }
+                        var schedulesAssistant = x.schedules.Where(z => z.assistants.Contains(tmdid));
+                        if (schedulesAssistant.Any())
+                        {
+                            courseIds.Add(x.courseId);
+                            CourseTask courseTask = x.ToJsonString().ToObject<CourseTask>();
+                            courseTask.schedules=schedulesAssistant.ToList();
+                            schoolAssistantTask.Add(new KeyValuePair<string, CourseTask>(x.courseId, courseTask));
+                            groupIds.AddRange(schedulesAssistant.Where(z => !string.IsNullOrWhiteSpace(z.groupId)).Select(x => x.groupId));
+                        }
+                    });
+                }
+                if (courseIds.Any())
+                {
+                    string sqlCourse = $"select value c from c where c.id in ({string.Join(",", courseIds.Select(b => $"'{b}'"))})";
+                    var result = await client.GetContainer(Constant.TEAMModelOS, Constant.School).GetList<CourseBase>(sqlCourse, $"CourseBase-{school}");
+                    if (result.list.IsNotEmpty())
+                    {
+                        foreach (var item in result.list)
+                        {
+                            List<CourseTaskDto> courseTaskDtos = new List<CourseTaskDto>();
+                            var teacher = schoolTeacherTask.Where(x => x.Key.Equals(item.id)).Select(z => new CourseTaskDto { courseTask=z.Value, type="teacher" });
+                            if (teacher.Any())
+                            {
+                                courseTaskDtos.AddRange(teacher.ToList());
+                            }
+                            var assistant = schoolAssistantTask.Where(x => x.Key.Equals(item.id)).Select(z => new CourseTaskDto { courseTask=z.Value, type="assistant" });
+                            if (assistant.Any())
+                            {
+                                courseTaskDtos.AddRange(assistant.ToList());
+                            }
+                            schoolCourses.Add(new CourseDto { courseBase=item, courseTasks=courseTaskDtos });
+                        }
+                    }
+                }
+            }
+
+            {
+                HashSet<string> courseIds = new HashSet<string>();
+                string sqlCoursePrivate = $"select value c.id from c where  c.creatorId='{tmdid}'";
+                var resultCourseBasePrivate = await client.GetContainer(Constant.TEAMModelOS, Constant.Teacher).GetList<string>(sqlCoursePrivate, $"CourseBase");
+                if (resultCourseBasePrivate.list.IsNotEmpty())
+                {
+                    courseIds=new HashSet<string>(resultCourseBasePrivate.list);
+                }
+                string sqlprivate = $"SELECT distinct value c FROM c  join b in c.schedules where c.pk='CourseTask' and (ARRAY_CONTAINS(b.assistants,'{tmdid}')or  b.teacherId  ='{tmdid}' )";
+                var resultTeacher = await client.GetContainer(Constant.TEAMModelOS, Constant.Teacher).GetList<CourseTask>(sqlprivate, $"CourseTask");
+                if (resultTeacher.list.IsNotEmpty())
+                {
 
-        public static async Task DeleteGrouplistEvent(string  id , string code,string  tbname ,CosmosClient client, IConfiguration _configuration, AzureServiceBusFactory _serviceBus)
+                    resultTeacher.list.ForEach(x => {
+                        var schedulesTeacher = x.schedules.Where(z => !string.IsNullOrWhiteSpace(z.teacherId)  &&  z.teacherId.Equals(tmdid));
+                        if (schedulesTeacher.Any())
+                        {
+                            courseIds.Add(x.courseId);
+                            CourseTask courseTask = x.ToJsonString().ToObject<CourseTask>();
+                            courseTask.schedules=schedulesTeacher.ToList();
+                            privateTeacherTask.Add(new KeyValuePair<string, CourseTask>(x.courseId, courseTask));
+                            groupIds.AddRange(schedulesTeacher.Where(z => !string.IsNullOrWhiteSpace(z.groupId)).Select(x => x.groupId));
+                        }
+                        var schedulesAssistant = x.schedules.Where(z => z.assistants.Contains(tmdid));
+                        if (schedulesAssistant.Any())
+                        {
+                            courseIds.Add(x.courseId);
+                            CourseTask courseTask = x.ToJsonString().ToObject<CourseTask>();
+                            courseTask.schedules=schedulesAssistant.ToList();
+                            privateAssistantTask.Add(new KeyValuePair<string, CourseTask>(x.courseId, courseTask));
+                            groupIds.AddRange(schedulesAssistant.Where(z => !string.IsNullOrWhiteSpace(z.groupId)).Select(x => x.groupId));
+                        }
+                    });
+
+                }
+                if (courseIds.Any())
+                {
+                    string sqlCourse = $"select value c from c where    c.id in ({string.Join(",", courseIds.Select(b => $"'{b}'"))})";
+                    var result = await client.GetContainer(Constant.TEAMModelOS, Constant.Teacher).GetList<CourseBase>(sqlCourse, $"CourseBase");
+                    if (result.list.IsNotEmpty())
+                    {
+                        foreach (var item in result.list)
+
+                        {
+                            List<CourseTaskDto> courseTaskDtos = new List<CourseTaskDto>();
+                            var teacher = privateTeacherTask.Where(x => x.Key.Equals(item.id)).Select(z => new CourseTaskDto { courseTask=z.Value, type="teacher" });
+                            if (teacher.Any())
+                            {
+                                courseTaskDtos.AddRange(teacher.ToList());
+                            }
+                            var assistant = privateAssistantTask.Where(x => x.Key.Equals(item.id)).Select(z => new CourseTaskDto { courseTask=z.Value, type="assistant" });
+                            if (assistant.Any())
+                            {
+                                courseTaskDtos.AddRange(assistant.ToList());
+                            }
+                            teahcerCourses.Add(new CourseDto { courseBase=item, courseTasks=courseTaskDtos });
+                        }
+                    }
+                }
+                List<CourseGroupList> courseGroupLists = new List<CourseGroupList>();
+                _coreAPIHttpService.check=false;
+                groupIds= groupIds.ToHashSet().ToList();
+                var groupListDatas = await GroupListService.GetMemberByListids(_coreAPIHttpService, client, _dingDing, groupIds.ToHashSet().ToList(), school);
+                string privateGrouplistSQL = $"select value c from c where c.creatorId={tmdid} ";
+                if (groupIds.IsNotEmpty()) {
+                      privateGrouplistSQL = $"select value c from c where c.creatorId={tmdid} and c.id not in ({string.Join(",", groupIds.Select(x=>$"'{x}'"))})";
+                }
+                var resultRGroupList = await client.GetContainer(Constant.TEAMModelOS, Constant.Teacher).GetList<RGroupList>(privateGrouplistSQL, "GroupList");
+                if (resultRGroupList.list.IsNotEmpty())
+                {
+                    CourseGroupList courseGroupList = new CourseGroupList
+                    {
+                        scope="private",
+                        id="default",
+                        name="default"
+                    };
+                    foreach (var data in resultRGroupList.list)
+                    {
+                        HashSet<string> groupName = data.members.Where(x => !string.IsNullOrEmpty(x.groupName)).Select(y => y.groupName).ToHashSet();
+                        GroupListGrp groupListGrp = new GroupListGrp(data, groupName);
+                        groupListGrp.teachType="teacher";
+                        courseGroupList.groups.Add(groupListGrp);
+                    }
+                }
+                foreach (var z in teahcerCourses)
+                {
+                    CourseGroupList courseGroupList = new CourseGroupList
+                    {
+                        scope="private",
+                        id=z.courseBase.id,
+                        name=z.courseBase.name,
+                        periodId=z.courseBase.period?.id,
+                        period=z.courseBase.period?.name,
+                        subject=z.courseBase.subject?.name,
+                        subjectId= z.courseBase.subject?.id
+                    };
+                    foreach (var x in z.courseTasks)
+                    {
+                        foreach (var y in x.courseTask.schedules)
+                        {
+                            var data = groupListDatas.groups.Find(m => m.id.Equals(y.groupId));
+                            if (data!= null)
+                            {
+                                HashSet<string> groupName = data.members.Where(x => !string.IsNullOrEmpty(x.groupName)).Select(y => y.groupName).ToHashSet();
+                                GroupListGrp groupListGrp = new GroupListGrp(data, groupName);
+                                groupListGrp.teachType=x.type;
+                                courseGroupList.groups.Add(groupListGrp);
+                            }
+                        }
+                    }
+                    courseGroupLists.Add(courseGroupList);
+                }
+                foreach (var z in schoolCourses)
+                {
+                    CourseGroupList courseGroupList = new CourseGroupList
+                    {
+                        scope="school",
+                        id=z.courseBase.id,
+                        name=z.courseBase.name,
+                        periodId=z.courseBase.period?.id,
+                        period=z.courseBase.period?.name,
+                        subject=z.courseBase.subject?.name,
+                        subjectId= z.courseBase.subject?.id
+                    };
+                    foreach (var x in z.courseTasks)
+                    {
+                        foreach (var y in x.courseTask.schedules)
+                        {
+                            var data = groupListDatas.groups.Find(m => m.id.Equals(y.groupId));
+                            if (data!= null)
+                            {
+                                HashSet<string> groupName = data.members.Where(x => !string.IsNullOrEmpty(x.groupName)).Select(y => y.groupName).ToHashSet();
+                                GroupListGrp groupListGrp = new GroupListGrp(data, groupName);
+                                groupListGrp.teachType=x.type;
+                                courseGroupList.groups.Add(groupListGrp);
+                            }
+                        }
+                    }
+                    courseGroupLists.Add(courseGroupList);
+                }
+                long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+                foreach (var groups in courseGroupLists)
+                {
+                    groups.groups.RemoveAll(z => z.expire>0 && z.expire<now);
+                }
+
+                return courseGroupLists;
+            }
+        }
+
+        public static async Task DeleteGrouplistEvent(string  id , string code,string  tbname ,CosmosClient client, IConfiguration _configuration, AzureServiceBusFactory _serviceBus,string _client)
         {
-            GroupChange change = new GroupChange();
+            
             GroupList groupList = await client.GetContainer(Constant.TEAMModelOS, tbname).ReadItemAsync<GroupList>(id.ToString(), new PartitionKey(code));
+            GroupChange change = new GroupChange() { client=_client,name=groupList.name};
             var tleave = groupList.members.FindAll(x => x.type == 1);
             if (tleave.IsNotEmpty())
             {
@@ -132,7 +362,7 @@ namespace TEAMModelOS.SDK
                     }
                 }
             }
-            string SummarySql = " c.id,c.code,c.name,c.no,c.periodId,c.scope,c.school,c.creatorId,c.type,c.year,c.tcount,c.scount,c.leader  ,c.froms ,c.joinLock ,c.review,c.limitCount ,c.expire,c.qrcodeExpire ,c.qrcodeDays ,c.grades  ";
+            string SummarySql = " c.id,c.code,c.name,c.no,c.periodId,c.scope,c.school,c.creatorId,c.type,c.year,c.tcount,c.scount,c.leader  ,c.froms ,c.joinLock ,c.review,c.limitCount,c.optNo ,c.expire,c.qrcodeExpire ,c.qrcodeDays ,c.grades  ";
             if (groupTypes.IsEmpty() || groupTypes.Contains("teach")) {
                 //教学班
                 string teachsql="";
@@ -221,6 +451,7 @@ namespace TEAMModelOS.SDK
                                 qrcodeDays = x.qrcodeDays,
                                 review=x.review,
                                 limitCount = x.limitCount,
+                                optNo = x.optNo,
                                 grades = x.grades,
                             });
                         }
@@ -239,7 +470,7 @@ namespace TEAMModelOS.SDK
         /// <param name="type"></param>
         /// <param name="school"></param>
         /// <returns></returns>
-        public static async Task<(int status, GroupList stuList,Member member)> CodeJoinList(CosmosClient client,AzureRedisFactory _azureRedis, string _stuListNo, string userid, int type, string school,int year,string name ,string picture,string lang,string courseId= null)
+        public static async Task<(int status, GroupList stuList,Member member)> CodeJoinList(CosmosClient client,AzureRedisFactory _azureRedis, string _stuListNo, string userid, int type, string school,int year,string name ,string picture,string lang,int seatNo, string courseId= null,string _client="web")
         {
             var queryNo = $"SELECT  value(c)  FROM c where  c.no ='{_stuListNo}'";
             (int status, GroupList stuList,Member member) data = (-1, null,null);
@@ -248,7 +479,7 @@ namespace TEAMModelOS.SDK
                 await foreach (var item in client.GetContainer(Constant.TEAMModelOS, "School").GetItemQueryIterator<GroupList>(queryText: queryNo,
                 requestOptions: new QueryRequestOptions() { PartitionKey = new Azure.Cosmos.PartitionKey($"GroupList-{school}") }))
                 {
-                    data = JoinList(item, userid, type, school,year);
+                     data = await JoinList(_azureRedis, item, userid, type, school,year,seatNo,_client:_client);
                     break;
                 }
             }
@@ -263,6 +494,10 @@ namespace TEAMModelOS.SDK
                if (item.review ==1)
                 {
 
+                    var mb =  item.members.FindAll(x => x.id.Equals(userid)  && x.type==type);
+                    if (mb.IsNotEmpty()) {
+                        return (2, item, null);
+                    }
                     //状态=6 ,人数已满,需要审核通过再加入。
 
                     long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
@@ -287,7 +522,8 @@ namespace TEAMModelOS.SDK
                             lang = lang,
                             qrcodeExpire=item.qrcodeExpire,
                             groupName= item.name,
-                            applyTime=now
+                            applyTime=now,
+                            seatNo=seatNo
                         };
                         string key = $"GroupList:GroupWaitingList:{item.scope}:{item.id}";
                         string filed = !string.IsNullOrWhiteSpace(school) ? $"{school}_{userid}" : userid;
@@ -307,7 +543,7 @@ namespace TEAMModelOS.SDK
                     //状态=5 ,人数已满
                     return (5, item, null);
                 }
-                data = JoinList(item, userid, type, school,year);
+                data =await JoinList(_azureRedis, item, userid, type, school,year,seatNo, _client: _client);
                 //TODO 需要考虑已经通过别的途径加入名单,但是缓存数据仍记录数据的问题。 还要考虑手动加入过的。或者在获取名未加入名单成员的临时缓存数据的时候过滤已经加入的。
                 break;
             }
@@ -337,13 +573,16 @@ namespace TEAMModelOS.SDK
             public string groupName { get; set; }
             public long applyTime{ get; set; }
             /// <summary>
-            /// 申请状态,-1 申请中,0 通过,1 拒绝,2 已过期。 
+            /// 申请状态,-1 申请中,0 通过,1 拒绝,2 已过期,3已经加入
             /// </summary>
             public int status { get; set; } = -1;
+            public int seatNo { get; set; }
            public List<IdName> courses  { get; set; }= new List<IdName>();
         }
-        public static (int status, GroupList stuList, Member member) JoinList(GroupList stuList, string userid, int type, string school,int year)
+        public static async Task<(int status, GroupList stuList, Member member)> JoinList(AzureRedisFactory azureRedis, GroupList stuList, string userid, int type, string school, int year, int seatNo = 0,string _client="web")
         {
+            
+            Member member = null;
             int status = -1;
             if (!string.IsNullOrWhiteSpace(stuList.school) && !string.IsNullOrWhiteSpace(school))
             {
@@ -353,6 +592,45 @@ namespace TEAMModelOS.SDK
                     return (status, stuList,null);
                 }
             }
+
+            if (seatNo>0) {
+                var mb=  stuList.members.Find(x => x.irs.Equals($"{seatNo}"));
+                if (mb==null) {
+                    if (type == 1)
+                    {
+                        member = stuList.members.Find(x => x.type == 1 && x.id.Equals(userid));
+                        if (member != null)
+                        {
+                            //重复加入
+                            status = 2;
+                        }
+                        else
+                        {
+                            //加入成功
+                            status = 0;
+                            member = new Member { id = userid, type = type, irs = $"{seatNo}", no =  $"{seatNo}", year=year,manual=1 };
+                            stuList.members.Add(member);
+                           
+                        }
+                    }
+                    else if (type == 2)
+                    {
+                        member = stuList.members.Find(x => x.type == 2 && x.id.Equals(userid) && x.code.Equals(school));
+                        if (member != null)
+                        {
+                            //重复加入
+                            status = 2;
+                        }
+                        else
+                        {
+                            status = 0;
+                            member = new Member { id = userid, code = school, type = type, irs =  $"{seatNo}", no =  $"{seatNo}", year = year, manual=1 };
+                            stuList.members.Add(member);
+                        }
+                    }
+                    return (status, stuList, member);
+                }
+            }
             string irs = string.Empty;
             List<string> irsOrder = stuList.members.Select(x => x.irs)?.Where(y => !string.IsNullOrEmpty(y) && Regex.IsMatch(y, @"^\d*$")).OrderBy(x => int.Parse(x)).ToList();
             if (!irsOrder.Contains("0"))
@@ -379,7 +657,7 @@ namespace TEAMModelOS.SDK
                     }
                 }
             }
-            Member member = null;
+        
             if (string.IsNullOrEmpty($"{userid}"))
             {
                 //加入学生或醍摩豆ID为空
@@ -422,7 +700,7 @@ namespace TEAMModelOS.SDK
             return (status, stuList, member);
         }
 
-        public static async Task<GroupList> UpsertList(GroupList list, AzureCosmosFactory _azureCosmos, IConfiguration _configuration, AzureServiceBusFactory _serviceBus)
+        public static async Task<GroupList> UpsertList(GroupList list, AzureCosmosFactory _azureCosmos, IConfiguration _configuration, AzureServiceBusFactory _serviceBus,string _client )
         {
             bool isnew = false;
             var client = _azureCosmos.GetCosmosClient();
@@ -468,6 +746,8 @@ namespace TEAMModelOS.SDK
             {
                 GroupChange change = new GroupChange()
                 {
+                    name=list.name,
+                    client=_client,
                     type = list.type,
                     listid = list.id,
                     scope = list.scope,
@@ -669,7 +949,7 @@ namespace TEAMModelOS.SDK
         /// <param name="graduate">毕业类型0在校,1毕业 , -1查全部。</param>
         /// <returns></returns>
         public static async Task<List<GroupListDto>> GetGroupListByListids(CosmosClient client, DingDing _dingDing, List<string> classes, string school,
-            string SummarySql = " c.id,c.code,c.name,c.no,c.periodId,c.scope,c.school,c.creatorId,c.type,c.year,c.tcount,c.scount,c.leader ,c.froms ,c.joinLock ,c.review,c.limitCount ,c.expire ,c.qrcodeExpire,c.qrcodeDays  ,c.grades ", int graduate = -1,long time=-1)
+            string SummarySql = " c.id,c.code,c.name,c.no,c.periodId,c.scope,c.school,c.creatorId,c.type,c.year,c.tcount,c.scount,c.leader ,c.froms ,c.joinLock ,c.review,c.limitCount ,c.optNo,c.expire ,c.qrcodeExpire,c.qrcodeDays  ,c.grades ", int graduate = -1,long time=-1)
         {
             List<GroupListDto> groupLists = new List<GroupListDto>();
             try
@@ -879,6 +1159,7 @@ namespace TEAMModelOS.SDK
                         joinLock=z.joinLock,
                         graduate=z.graduate,
                         review=z.review,
+                        optNo=z.optNo,
                         limitCount=z.limitCount,
                         qrcodeDays=z.qrcodeDays,
                         qrcodeExpire=z.qrcodeExpire,
@@ -905,6 +1186,7 @@ namespace TEAMModelOS.SDK
                         joinLock=z.joinLock,
                         graduate=z.graduate,
                         review=z.review,
+                        optNo=z.optNo,
                         limitCount=z.limitCount,
                         qrcodeDays=z.qrcodeDays,
                         qrcodeExpire=z.qrcodeExpire,
@@ -977,6 +1259,10 @@ namespace TEAMModelOS.SDK
             else
             {
                 var semesterGroupList = await GetGroupListSemester(client, classes, school,type:null,periodId:null ,no:null, time);
+                if (semesterGroupList.rgroupList.IsNotEmpty()) {
+                    groupLists.AddRange(semesterGroupList.rgroupList);
+                    members.AddRange(semesterGroupList.rmembers);
+                }
                 classes = classes.Except(semesterGroupList.rgroupList.Select(y => y.id)).ToList();
                 if (classes.IsNotEmpty()) {
                     Dictionary<string, List<RGroupList>> groups = new Dictionary<string, List<RGroupList>>();
@@ -1144,6 +1430,7 @@ namespace TEAMModelOS.SDK
                                     graduate = y.graduate,
                                     gender= y.gender,
                                     periodId=y.periodId,
+                                    
                                 }).ToList();
                             members.AddRange(smembers);
 
@@ -1362,6 +1649,7 @@ namespace TEAMModelOS.SDK
                                         froms = x.froms,
                                         joinLock = x.joinLock,
                                         qrcodeExpire = x.qrcodeExpire,
+                                        optNo = x.optNo,
                                         qrcodeDays = x.qrcodeDays,
                                         review=x.review,
                                         limitCount = x.limitCount,

+ 0 - 1
TEAMModelOS.SDK/Models/Service/LessonService.cs

@@ -17,7 +17,6 @@ using System.Text.Json;
 using System.Threading.Tasks;
 using TEAMModelOS.SDK.DI;
 using TEAMModelOS.SDK.Extension;
-using TEAMModelOS.SDK.Helper.Common.DateTimeHelper;
 using TEAMModelOS.SDK.Models.Cosmos.Common;
 using TEAMModelOS.SDK.Models.Cosmos.OpenEntity;
 using TEAMModelOS.SDK.Services;

+ 10 - 0
TEAMModelOS.SDK/Models/Service/OpenApiService.cs

@@ -45,6 +45,7 @@ using TEAMModelOS.Models.ShanDa;
 using System.Runtime.ConstrainedExecution;
 using DocumentFormat.OpenXml.Wordprocessing;
 using DocumentFormat.OpenXml.Office2010.Excel;
+using DocumentFormat.OpenXml.Office2016.Excel;
 
 namespace TEAMModelOS.SDK
 {
@@ -173,7 +174,16 @@ namespace TEAMModelOS.SDK
             {
                 sql_status_managePage = "";
             }
+            long now = DateTimeOffset.Now.ToUnixTimeMilliseconds();
             cosmosDbQuery.QueryText = cosmosDbQuery.QueryText.Replace("where", $" where {sql_status_managePage} array_length(c.groupIds)>0 {sqlPrivate} {sqlShow}  and  ");
+            if (json.TryGetProperty("singleGreen", out JsonElement doubleGreen) && doubleGreen.GetBoolean())
+            {
+                cosmosDbQuery.QueryText = cosmosDbQuery.QueryText.Replace("where", $" where  ((c.tLevel=2 and c.pLevel<2 )or(c.tLevel<2 and c.pLevel=2 )) and   ");
+            }
+            if (json.TryGetProperty("expire", out JsonElement expire) && expire.ValueKind.Equals(JsonValueKind.True))
+            {
+                cosmosDbQuery.QueryText = cosmosDbQuery.QueryText.Replace("where", $" where c.expire>{now} and   ");
+            }
             List<LessonRecord> records = new List<LessonRecord>();
             await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, tbname).GetItemQueryIterator<LessonRecord>(queryDefinition: cosmosDbQuery.CosmosQueryDefinition, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey(code) }))
             {

+ 32 - 5
TEAMModelOS.SDK/Models/Service/StudentService.cs

@@ -2,6 +2,7 @@
 using Azure.Cosmos;
 using Azure.Messaging.ServiceBus;
 using DocumentFormat.OpenXml.Drawing;
+using DocumentFormat.OpenXml.Drawing.Charts;
 using DocumentFormat.OpenXml.Office2010.Excel;
 using DocumentFormat.OpenXml.Spreadsheet;
 using DocumentFormat.OpenXml.VariantTypes;
@@ -557,6 +558,8 @@ namespace TEAMModelOS.SDK
                             {
                                 GroupChange change = new GroupChange
                                 {
+
+                                    client="web",
                                     scope = "school",
                                     school = schoolId,
                                     type = "student",
@@ -592,6 +595,7 @@ namespace TEAMModelOS.SDK
                             {
                                 GroupChange change = new GroupChange
                                 {
+                                    client="web",
                                     scope = "school",
                                     school = schoolId,
                                     type = "student",
@@ -630,6 +634,7 @@ namespace TEAMModelOS.SDK
                                 {
                                     GroupChange change = new GroupChange
                                     {
+                                        client="web",
                                         scope = "school",
                                         school = schoolId,
                                         type = "student",
@@ -661,6 +666,7 @@ namespace TEAMModelOS.SDK
                                 {
                                     GroupChange change = new GroupChange
                                     {
+                                        client="web",
                                         scope = "school",
                                         school = schoolId,
                                         type = "student",
@@ -708,6 +714,7 @@ namespace TEAMModelOS.SDK
                             {
                                 GroupChange change = new GroupChange
                                 {
+                                    client="web",
                                     scope = "school",
                                     school = schoolId,
                                     type = "student",
@@ -752,7 +759,8 @@ namespace TEAMModelOS.SDK
                             else
                             {
                                 GroupChange change = new GroupChange
-                                {
+                                {   
+                                    client = "web",
                                     scope = "school",
                                     school = schoolId,
                                     type = "student",
@@ -788,6 +796,7 @@ namespace TEAMModelOS.SDK
                             {
                                 GroupChange change = new GroupChange
                                 {
+                                    client="web",
                                     scope = "school",
                                     school = schoolId,
                                     type = "student",
@@ -826,6 +835,7 @@ namespace TEAMModelOS.SDK
                                 {
                                     GroupChange change = new GroupChange
                                     {
+                                        client="web",
                                         scope = "school",
                                         school = schoolId,
                                         type = "student",
@@ -857,6 +867,7 @@ namespace TEAMModelOS.SDK
                                 {
                                     GroupChange change = new GroupChange
                                     {
+                                        client="web",
                                         scope = "school",
                                         school = schoolId,
                                         type = "student",
@@ -904,6 +915,7 @@ namespace TEAMModelOS.SDK
                             {
                                 GroupChange change = new GroupChange
                                 {
+                                    client="web",
                                     scope = "school",
                                     school = schoolId,
                                     type = "student",
@@ -925,15 +937,30 @@ namespace TEAMModelOS.SDK
                     }
                 }
             }
+            List<Class> classes = new List<Class>();
+            if (dictChange.Keys.Count>0)
+            {
+                string insql = string.Join(",", dictChange.Keys.Select(x => $"'{x}'"));
+                var result = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, "School").GetList<Class>($"select     c.id,c.name   from c where c.id in ({insql})", $"Class-{schoolId}");
+                if (result.list.IsNotEmpty())
+                {
+                    classes.AddRange(result.list);
+                }
+            }
             foreach (var changed in dictChange.Keys)
             {
                 var change = dictChange[changed];
                 if (change.stujoin.Count != 0 || change.stuleave.Count != 0)
                 {
-                    var messageChange = new ServiceBusMessage(change.ToJsonString());
-                    messageChange.ApplicationProperties.Add("name", "GroupChange");
-                    var ActiveTask = _configuration.GetValue<string>("Azure:ServiceBus:ActiveTask");
-                    await _serviceBus.GetServiceBusClient().SendMessageAsync(ActiveTask, messageChange);
+                   var clazz=  classes.Find(x => x.id.Equals(changed));
+                    if (clazz!=null) {
+                        change.name=clazz.name;
+                        var messageChange = new ServiceBusMessage(change.ToJsonString());
+                        messageChange.ApplicationProperties.Add("name", "GroupChange");
+                        var ActiveTask = _configuration.GetValue<string>("Azure:ServiceBus:ActiveTask");
+                        await _serviceBus.GetServiceBusClient().SendMessageAsync(ActiveTask, messageChange);
+                    }
+                    
                 }
             }
 

Plik diff jest za duży
+ 1793 - 0
TEAMModelOS.SDK/Models/Service/SystemService.cs


+ 3 - 3
TEAMModelOS.SDK/Models/Service/Third/ThirdService.cs

@@ -151,14 +151,14 @@ namespace TEAMModelOS.SDK.Models
                                 if (meber == null || !meber.Any())
                                 {
                                     yxtrain[0].members.Add(new Member { id = teacher.id, type = 1, groupId = groupId, groupName = groupName,nickname= nickname });
-                                    await GroupListService.UpsertList(yxtrain[0], _azureCosmos, _configuration, _serviceBus);
+                                    await GroupListService.UpsertList(yxtrain[0], _azureCosmos, _configuration, _serviceBus,"web");
                                 }
                                 else
                                 {
                                     if (string.IsNullOrEmpty(meber.First().groupId) || string.IsNullOrEmpty(meber.First().groupName))
                                     {
                                         meber.ToList().ForEach(x => { x.groupId = groupId; x.groupName = groupName;x.nickname = string.IsNullOrWhiteSpace(x.nickname)? nickname:x.nickname; });
-                                        await GroupListService.UpsertList(yxtrain[0], _azureCosmos, _configuration, _serviceBus);
+                                        await GroupListService.UpsertList(yxtrain[0], _azureCosmos, _configuration, _serviceBus, "web");
                                     }
                                 }
                             }
@@ -191,7 +191,7 @@ namespace TEAMModelOS.SDK.Models
                                     pk = "GroupList",
                                     ttl = -1
                                 };
-                                await GroupListService.UpsertList(groupList, _azureCosmos, _configuration, _serviceBus);
+                                await GroupListService.UpsertList(groupList, _azureCosmos, _configuration, _serviceBus, "web");
                             }
                         }
                     }

+ 46 - 46
TEAMModelOS.SDK/TEAMModelOS.SDK.csproj

@@ -1,53 +1,53 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
-  <PropertyGroup>
-    <TargetFramework>net6.0</TargetFramework>
-    <Version>5.2401.31</Version>
-    <AssemblyVersion>5.2401.31.1</AssemblyVersion>
-    <FileVersion>5.2401.31.1</FileVersion>
-    <PackageReleaseNotes>发版</PackageReleaseNotes>
-  </PropertyGroup>
+	<PropertyGroup>
+		<TargetFramework>net6.0</TargetFramework>
+		<Version>5.2404.03</Version>
+		<AssemblyVersion>5.2404.03.1</AssemblyVersion>
+		<FileVersion>5.2404.03.1</FileVersion>
+		<PackageReleaseNotes>发版</PackageReleaseNotes>
+	</PropertyGroup>
 
-  <ItemGroup>
-    <PackageReference Include="CHTCHSConv" Version="1.0.0" />
-    <PackageReference Include="AspectCore.Extensions.Reflection" Version="2.2.0" />
-    <PackageReference Include="Azure.Cosmos" Version="4.0.0-preview3" />
-    <PackageReference Include="Azure.Identity" Version="1.5.0" />
-    <PackageReference Include="Azure.Messaging.ServiceBus" Version="7.7.0" />
-    <PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.2.0" />
-    <PackageReference Include="Azure.Storage.Blobs.Batch" Version="12.8.0" />
-    <PackageReference Include="Azure.Storage.Queues" Version="12.9.0" />
-    <PackageReference Include="ClouDASLibx" Version="1.3.2" />
-    <PackageReference Include="DinkToPdf" Version="1.0.8" />
-    <PackageReference Include="DocumentFormat.OpenXml" Version="2.15.0" />
-    <PackageReference Include="HTEXLib" Version="5.2401.1024" />
-    <PackageReference Include="HtmlAgilityPack" Version="1.11.42" />
-    <PackageReference Include="Lib.AspNetCore.ServerSentEvents" Version="8.2.0" />
-    <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.3" />
-    <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
-    <PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="6.0.3" />
-    <PackageReference Include="Microsoft.Azure.SignalR.Management" Version="1.18.0" />
-    <PackageReference Include="Microsoft.Extensions.Logging.AzureAppServices" Version="7.0.5" />
-    <PackageReference Include="Microsoft.Identity.Client" Version="4.39.0" />
-    <PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
-    <PackageReference Include="NUnit" Version="3.13.2" />
-    <PackageReference Include="PinYinConverterCore" Version="1.0.2" />
-    <PackageReference Include="StackExchange.Redis" Version="2.6.45" />
-    <PackageReference Include="SvgNet" Version="2.2.2" />
-    <PackageReference Include="System.Drawing.Common" Version="6.0.0" />
-    <PackageReference Include="Microsoft.Azure.Cosmos.Table" Version="2.0.0-preview" />
-    <PackageReference Include="System.Net.Http.Json" Version="6.0.0" />
-    <PackageReference Include="NPinyin.Core" Version="3.0.0" />
-    <PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
-    <PackageReference Include="VueCliMiddleware" Version="6.0.0" />
-	<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.10.0" />
-	<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Abstractions" Version="1.1.0" />
-	<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.0.13" />
-  </ItemGroup>
+	<ItemGroup>
+		<PackageReference Include="CHTCHSConv" Version="1.0.0" />
+		<PackageReference Include="AspectCore.Extensions.Reflection" Version="2.2.0" />
+		<PackageReference Include="Azure.Cosmos" Version="4.0.0-preview3" />
+		<PackageReference Include="Azure.Identity" Version="1.5.0" />
+		<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.7.0" />
+		<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.2.0" />
+		<PackageReference Include="Azure.Storage.Blobs.Batch" Version="12.8.0" />
+		<PackageReference Include="Azure.Storage.Queues" Version="12.9.0" />
+		<PackageReference Include="ClouDASLibx" Version="1.3.2" />
+		<PackageReference Include="DinkToPdf" Version="1.0.8" />
+		<PackageReference Include="DocumentFormat.OpenXml" Version="2.15.0" />
+		<PackageReference Include="HTEXLib" Version="5.2401.1024" />
+		<PackageReference Include="HtmlAgilityPack" Version="1.11.42" />
+		<PackageReference Include="Lib.AspNetCore.ServerSentEvents" Version="8.2.0" />
+		<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.3" />
+		<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
+		<PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="6.0.3" />
+		<PackageReference Include="Microsoft.Azure.SignalR.Management" Version="1.18.0" />
+		<PackageReference Include="Microsoft.Extensions.Logging.AzureAppServices" Version="7.0.5" />
+		<PackageReference Include="Microsoft.Identity.Client" Version="4.39.0" />
+		<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
+		<PackageReference Include="NUnit" Version="3.13.2" />
+		<PackageReference Include="PinYinConverterCore" Version="1.0.2" />
+		<PackageReference Include="StackExchange.Redis" Version="2.6.45" />
+		<PackageReference Include="SvgNet" Version="2.2.2" />
+		<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
+		<PackageReference Include="Microsoft.Azure.Cosmos.Table" Version="2.0.0-preview" />
+		<PackageReference Include="System.Net.Http.Json" Version="6.0.0" />
+		<PackageReference Include="NPinyin.Core" Version="3.0.0" />
+		<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
+		<PackageReference Include="VueCliMiddleware" Version="6.0.0" />
+		<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.10.0" />
+		<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Abstractions" Version="1.1.0" />
+		<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.0.13" />
+	</ItemGroup>
 
 
 
-  <ItemGroup>
-    <Folder Include="DI\BBAPI\" />
-  </ItemGroup>
+	<ItemGroup>
+		<Folder Include="DI\BBAPI\" />
+	</ItemGroup>
 </Project>

+ 74 - 0
TEAMModelOS.TEST/Program.cs

@@ -1,5 +1,8 @@
 using HTEXLib.COMM.Helpers;
+using Newtonsoft.Json;
+using System.Security.Policy;
 using System.Text;
+using System.Web;
 using TEAMModelOS.SDK.Extension;
 using TEAMModelOS.SDK.Models;
 using TEAMModelOS.SDK.Models.Cosmos.OpenEntity;
@@ -11,6 +14,45 @@ namespace TEAMModelOS.TEST
     {
         static void Main(string[] args)
         {
+
+            //
+            var uri = HttpUtility.UrlDecode("https://teammodeltest.blob.core.chinacloudapi.cn/hbcn/art/e9a5ec36-7299-45dc-9517-7457960346c4/report/202106005.pdf");
+            var paths = uri.Split("/art/");
+            if (paths.Length == 2)
+            {
+                var ps = paths[1].Split("/");
+                if (ps.Length == 3)
+                {
+                    Uri uris = new Uri(paths[0]);
+
+                    // 获取URL的Segments属性,这是一个String数组,包含URL中的每个部分
+                    string[] segments = uris.Segments;
+                    string key = $"ArtPDF:{ps[0]}";
+                    // 确保segments数组至少有一个元素
+                    if (segments.Length > 0)
+                    {
+                        // 获取数组中的最后一个元素,即最后一个'/'之后的部分
+                        string lastSegment = segments[segments.Length - 1];
+                        key = $"ArtPDF:{ps[0]}:{lastSegment}";
+                    }
+                }
+            }
+
+        List<Dictionary<string, object>> jsonData = new List<Dictionary<string, object>>
+        {
+            new Dictionary<string, object> { { "user", "A" }, { "time", 1711414573130 } },
+            new Dictionary<string, object> { { "user", "A" }, { "time", 1711414673130 } },
+            new Dictionary<string, object> { { "user", "A" }, { "time", 1711415673130 } },
+            new Dictionary<string, object> { { "user", "A" }, { "time", 1711435873130 } },
+            new Dictionary<string, object> { { "user", "A" }, { "time", 1711436873130 } },
+            new Dictionary<string, object> { { "user", "A" }, { "time", 1711455873130 } },
+            new Dictionary<string, object> { { "user", "A" }, { "time", 1711495873130 } },
+            new Dictionary<string, object> { { "user", "A" }, { "time", 1711495973130 } }
+        };
+
+            double totalDuration = GetUserDuration(jsonData);
+            Console.WriteLine("用户使用时长: " + totalDuration + "小时");
+
             // 作品数据
             var works = new List<Work>
         {
@@ -69,6 +111,30 @@ namespace TEAMModelOS.TEST
             }
         }
 
+        static double GetUserDuration(List<Dictionary<string, object>> jsonData)
+        {
+            double totalDuration = 0;
+          //  DateTime lastTime = DateTimeOffset.FromUnixTimeMilliseconds((long)jsonData[0]["time"]).UtcDateTime;
+            long ltime = (long)jsonData[0]["time"];
+            for (int i = 1; i < jsonData.Count; i++)
+            {
+                long ctime = (long)jsonData[i]["time"];
+                //DateTime currentTime = DateTimeOffset.FromUnixTimeMilliseconds((long)jsonData[i]["time"]).UtcDateTime;
+                long  timeDifference = ctime - ltime;
+
+                if (timeDifference < 3600000)
+                {
+                    totalDuration += timeDifference;
+                }
+                else
+                {
+                    totalDuration += 1;
+                }
+
+                ltime = ctime;
+            }
+            return totalDuration;
+        }
         static List<Assignment> AssignWorksToExperts(List<Work> works, List<ExpertS> experts, int N)
         {
             var assignments = new List<Assignment>();
@@ -227,5 +293,13 @@ namespace TEAMModelOS.TEST
         /// </summary>
         public double interactScore { get; set; }
     }
+    // 用于反序列化JSON数据的类
+    public class UsageRecord
+    {
+        [JsonProperty("user")]
+        public string user { get; set; }
 
+        [JsonProperty("time")]
+        public long time { get; set; }
+    }
 }

+ 1 - 0
TEAMModelOS/ClientApp/package.json

@@ -38,6 +38,7 @@
     "i18next": "^20.3.1",
     "imports-loader": "^0.8.0",
     "increase-memory-limit": "^1.0.7",
+    "js-audio-recorder": "^1.0.7",
     "js-md5": "^0.7.3",
     "js-sha1": "^0.6.0",
     "json-markup": "^1.1.3",

Plik diff jest za duży
+ 1438 - 0
TEAMModelOS/ClientApp/public/bill.html


+ 329 - 116
TEAMModelOS/ClientApp/public/lang/en-US.js

@@ -364,7 +364,7 @@ const LANG_EN_US = {
         isShowLines: 'Sealing line',
         objective: 'Objective Question',
         complete: 'Cloze Question',
-        subjective: 'Writing Question',
+        subjective: 'Subjective Question',
         compositionZh: 'Essay(Language)',
         compositionEn: 'Essay(English)',
         tip1: 'Question Number',
@@ -575,6 +575,9 @@ const LANG_EN_US = {
         pdName5: 'Data Storage Service Space',
         pdName6: 'Haboard Smarter Touchscreen',
         pdName7: 'AI Lecture Observation Lounge',
+        pdName8: 'Sokrates',
+        pdName9: 'Art Assessment Service',
+        pdName10: 'All-round Quality Dashboard',
         authDate: 'Expiry Date:',
         fuAuth: 'Authorized Function:',
         IRSnumber: 'Web IRS No.:',
@@ -866,6 +869,14 @@ const LANG_EN_US = {
         tips1: 'Modify Information'
     },
     newCusMgt: {
+        irs: {
+            batchSetting: 'Batch set seat No. (IRS No.)',
+            batchText: 'Save',
+            cancel: 'Cancel',
+            repeatTip: 'These seat No. (IRS No.) are duplicated, please check and try again:',
+            allowPick: 'Allow students to choose their seat No.',
+            pickTip: 'If the seat No. you selected is already used, it will be automatically assigned by the system'
+        },
         configCourse: 'Assigned courses this semester',
         recommendFiles: 'Lesson Backup data',
         sort: 'Sort by',
@@ -1073,11 +1084,12 @@ const LANG_EN_US = {
         schoolType: 'In-school Account',
         tmIDType: 'ID',
         qrCodeText: 'Invitation Code:',
-        inviteUrl: 'Copy description with URL',
+        inviteUrl: 'Copy URL with description',
         inviteShortUrl: 'Copy URL only',
         copyUrl: 'Copy Link',
         joinMessage1: "Allowed to join the course",
         joinMessage2: "Prohibited to join the course",
+        defaultDesc:'Personal Course on the IES TEAM Model Cloud',
         inviteInfo1: 'Invite to join the IES Teacher Personal Course on the TEAM Model Cloud',
         inviteInfo2: 'Course Name:',
         inviteInfo3: 'Course List:',
@@ -1090,6 +1102,7 @@ const LANG_EN_US = {
         createTips2: 'Note: You (not yet a member of a school) can allow students to join the course by entering the course invitation code, scanning the course QR code, or using the invitation link.',
         renameListTitle: 'Edit List Name',
         selectListTips: 'Please select a list',
+        renameRepeat: 'A list with the same name already exists',
         alreadyExist: ' already existed',
         listAPIErr: 'Failed to get the list',
         rmvListTips: 'Note: The current list is also being used in other courses',
@@ -1345,7 +1358,7 @@ const LANG_EN_US = {
         //MgtStuList.vue
         nameList: 'Name list',
         remvStu: 'Remove Student',
-        editStu: 'Set IRS number',
+        editStu: 'Auto-set IRS number',
         goBack: 'Click to go back',
         delListTitle: 'Delete Customized Class',
         notSet: 'Not set',
@@ -1491,6 +1504,7 @@ const LANG_EN_US = {
             filter4: 'Interaction',
             filter5: 'Test',
             filter6: 'Smart Rating',
+            filter7: 'Collaboration',
             evt1: 'Buzz-in:',
             evt2: 'Smarter Classroom Assessment Data',
             evt3: 'Pick-out:',
@@ -1511,6 +1525,12 @@ const LANG_EN_US = {
             personMutual: 'All Items for All',
             randomMutual: 'Random Assign',
             anonymousSub: 'Anonymous Submission',
+            coworkAll: 'Whole Class Collaboration',
+            coworkGroup: 'Groups Collaboration',
+            coworkdiff: 'Differentiated Collaboration',
+            contribution: 'Contribution',
+            personContri: 'Personal Contribution',
+            groupContri: 'Group Contribution'
         }
     },
     result: {
@@ -1648,7 +1668,7 @@ const LANG_EN_US = {
         multiple: 'Multiple Answers',
         judge: 'True-False',
         complete: 'Cloze',
-        subjective: 'Writing',
+        subjective: 'Subjective',
         connector: 'Matching',
         correct: 'Correcting',
         compose: 'Question Set',
@@ -1689,6 +1709,21 @@ const LANG_EN_US = {
         cancelSuc: 'Cancel successfully',
         addSuc: 'Add successfully',
         newExercise: {
+            answerType:{
+                types:'Answer Type',
+                text:'Text',
+                textImage:'Draw Notes',
+                image:'Picture',
+                file:'File',
+                audio: 'Audio',
+                autoScore: 'Automatic Rating',
+                answerLang: 'Language',
+                us: 'English',
+                zh: 'Mandarin',
+                jp: 'Japanese',
+                kr: 'Korean',
+                hk: 'Cantonese'
+            },
             newSchoolItem: 'Create School Question',
             newPrivateItem: 'Create Personal Question',
             backToBank: 'Back to question bank',
@@ -1820,8 +1855,9 @@ const LANG_EN_US = {
             choosePaper: 'Select exam file',
             choosed: 'Deselect',
             searchPaper: 'Enter exam file name to search...',
-            copyTip1: 'Use ',
-            copyTip2: 'Duplicate'
+            copyTip1: 'Are you sure you want to duplicate ',
+            copyTip2: 'to create a new exam file',
+            copyTip3: 'Duplicate',
         },
         importFile: {
             uploadSuc: 'Exam file uploaded and parsed successfully!',
@@ -1831,7 +1867,7 @@ const LANG_EN_US = {
             importTips: 'Import Notice',
             tips1: ' Click on the upload icon above to select a file',
             tips2: ' Only support ".docx, .xlsx, .xls" format file import, please refer to the template format to import',
-            tips3: ' For now, only single answer, multiple answers, true-false, cloze, writing, matching, correcting, and question set are supported for import',
+            tips3: ' For now, only single answer, multiple answers, true-false, cloze, subjective, matching, correcting, and question set are supported for import',
             tips4: ' Please keep the template language the same as the current browser language',
             tips5: ' For more details, please refer to the template making instruction',
             tips8: '',
@@ -2005,7 +2041,7 @@ const LANG_EN_US = {
             value: "complete"
         },
         {
-            label: "Writing",
+            label: "Subjective",
             value: "subjective"
         },
         {
@@ -2989,7 +3025,7 @@ const LANG_EN_US = {
             calcing: 'Result data are being processed now.,',
             clickFresh: 'Click here to refresh',
             inCalc: 'The data is being processed, please check later',
-            total: 'Total Score ',
+            total: 'Total Score',
             avgScore1: 'Total Average Score',
             totalLabel: 'Total Quantity',
             scoreMat: 'Score Segment',
@@ -3793,7 +3829,7 @@ const LANG_EN_US = {
         scType1: 'K–12',
         scType2: 'Higher Education Institutions (HEIs)',
         openLessonRecord: 'Lesson Record Management',
-        getLoginUrl:'Student Login Portal',
+        getLoginUrl: 'Student Login Portal',
         goNormalLogin: 'Switch to Standard Login Page',
         proSetting: 'Professional Subject',
         curSemSta: 'Current Status:',
@@ -4676,7 +4712,7 @@ const LANG_EN_US = {
             value: "complete"
         },
         {
-            label: "Writing",
+            label: "Subjective",
             value: "subjective"
         },
         {
@@ -5101,7 +5137,7 @@ const LANG_EN_US = {
                 multiple: 'Multiple Answers Question',
                 judge: 'True-False Question',
                 complete: 'Cloze Question',
-                subjective: 'Writing Question',
+                subjective: 'Subjective Question',
                 compose: 'Question Set',
                 correct: 'Correcting Question',
                 connector: 'Matching Question'
@@ -5581,7 +5617,7 @@ const LANG_EN_US = {
     },
     // 问卷调查
     survey: {
-        pickTip: 'To comply with the survey activity question template, we only keep the single answer, multiple answers, true-false, and writing questions from the exam file. Are you sure you want to continue?',
+        pickTip: 'To comply with the survey activity question template, we only keep the single answer, multiple answers, true-false, and subjective questions from the exam file. Are you sure you want to continue?',
         pickTip2: 'Please select an exam file first',
         pickPaper: 'Select questions from the question bank',
         noItemTip: 'The content of the survey cannot be empty',
@@ -5600,7 +5636,7 @@ const LANG_EN_US = {
         single: 'Single Answer Question',
         multiple: 'Multiple Answers Question',
         judge: 'True-False Question',
-        subjective: 'Writing Question',
+        subjective: 'Subjective Question',
         defaultName: 'Default Survey Name',
         isExistTip: 'Unsaved survey activity already exists!',
         getDataFailTip: 'Failed to obtain data!',
@@ -5656,7 +5692,7 @@ const LANG_EN_US = {
             single: 'Single Answer',
             multiple: 'Multiple Answers',
             judge: 'True-False',
-            subjective: 'Writing',
+            subjective: 'Subjective',
             noCompleteTip: 'Please fill in completely',
             confirmTitle: 'Friendly Reminder',
             confirmText: 'Are you sure to delete this question?',
@@ -6184,6 +6220,8 @@ const LANG_EN_US = {
         tableC6: 'Upload Date',
         searchText: 'Enter keywords to search',
         btnUpload: 'Upload Resources',
+        tips9: 'Set carousel picture',
+        setCarousel: 'Are you sure to set the current picture as a carousel picture on the dashboard?',
         tips1: 'Large Icons',
         tips2: 'List',
         tips3: 'Download File',
@@ -6826,7 +6864,7 @@ const LANG_EN_US = {
         ql_text3: 'Multiple Answers Question',
         ql_text4: 'True-False Question',
         ql_text5: 'Cloze Question',
-        ql_text6: 'Writing Question',
+        ql_text6: 'Subjective Question',
         ql_text7: 'Question Set',
         ql_text8: ' points',
         ql_text9: 'View Answer and Explanation',
@@ -6929,7 +6967,7 @@ const LANG_EN_US = {
             quType1: 'Single Answer',
             quType2: 'Multiple Answer',
             quType3: 'True-false',
-            quType4: 'Writing',
+            quType4: 'Subjective',
             trainType1: 'Demonstration and sharing of IT teaching cases',
             trainType2: 'Expert Specialized Training',
             trainType3: 'Homogeneous Pedagogy',
@@ -7851,121 +7889,296 @@ const LANG_EN_US = {
     },
     activity: {
         scoreWord: {
-            only: '按评审分数',
-            avg: '按平均分',
-            top: '按最高分',
-            rmLowAvg: '去掉最低分的平均分',
-            rmTopAvg: '去掉最高分的平均分',
-            rmLowTopAvg: '去掉最高分和最低分的平均分',
+            only: 'By Assessment Score:',
+            avg: 'By Average Score:',
+            top: 'By Highest Score:',
+            rmLowAvg: 'Average score excluding the lowest score:',
+            rmTopAvg: 'Average score excluding the highest score:',
+            rmLowTopAvg: 'Average score excluding the highest and lowest scores:',
         },
         distributeWord: {
-            none: '不需要匹配',
-            period: '只匹配学段',
-            subject: '只匹配学科',
-            periodAndSubject: '同时匹配学科和学段',
+            none: 'No need to match:',
+            period: 'Match by school system only:',
+            subject: 'Match by subject only:',
+            periodAndSubject: 'Match by subject and school system:',
         },
-        basicInfo: '基本信息',
-        moduleConfig: '模块配置',
+        basicInfo: 'Basic Information:',
+        moduleConfig: 'Module Settings:',
         placeholder: {
-            field1: '请输入活动主题',
-            field2: '请输入活动简介',
-            field3: '请输入活动地点',
-            field4: '请输入主办单位名称',
-            field5: '请输入承办单位名称',
-            field6: '请输入免责申明',
-            field7: '上传主题图片',
-            field8: '上传活动文件',
-            field9: '不限格式',
-            field10: '请输入作品要求',
-            field11: '自定义填报的信息',
-            field12: '请输入选项内容',
-            field13: '仅作展示',
-        },
-        profile: '简介',
-        host: '主办',
-        undertake: '承办',
+            field1: 'Please enter an activity topic:',
+            field2: 'Please enter a brief description of the activity:',
+            field3: 'Please enter the location of the activity:',
+            field4: 'Please enter the name of the organizer:',
+            field5: 'Please enter the name of the undertaker:',
+            field6: 'Please enter a disclaimer:',
+            field7: 'Upload Topic Image:',
+            field8: 'Upload Activity File:',
+            field9: 'Unlimited Format:',
+            field10: 'Please enter your entry requirements:',
+            field11: 'Customized Information:',
+            field12: 'Please enter options:',
+            field13: 'For display only:',
+            field14: 'No path at this time:',
+            field15: 'Please enter level label:',
+            field16: 'Please enter a suggested score:',
+            field17: 'Please enter a rule name:',
+            field18: 'Please enter a description of the rule:',
+            field19: 'Describe the details:',
+        },
+        profile: 'Introduction:',
+        host: 'Organizer:',
+        undertake: 'Undertaker:',
         scope: {
-            open: '公开',
-            area: '区级',
-            school: '校级',
+            open: 'Public:',
+            area: 'District Level:',
+            school: 'School Level:',
         },
         prompt: {
-            field1: '不选学校则表示所有学校都可参加该活动',
-            field2: '发布活动后,需学校先确认参与活动,老师才可报名参与',
-            field3: '由学校选择老师参与活动',
-            field4: '请选择本次活动需要添加的模块',
-            field5: '0:默认不限制',
-            field6: '个人上传作品',
-            field7: '由队长统一上传',
-            field8: '当前活动使用',
-            field9: '请先勾选作品评审',
-            field10: '报名时展示的填报格式',
-            field11: '报名时可供选择的内容',
-            field12: '报名时展示的内容',
-        },
-        enrollLabel: '报名',
-        enroll: '报名制',
-        invite: '邀请制',
-        disclaimer: '免责声明',
+            field1: 'If no school is selected, all schools are eligible to participate in the program.:',
+            field2: 'After the activity is announced, schools must first confirm their participation before teachers can sign up.:',
+            field3: 'Teachers will be selected by the school to participate in the activity.:',
+            field4: 'Please select the modules that need to be added for this activity.:',
+            field5: '0: Unlimited by default:',
+            field6: 'Upload by team individual:',
+            field7: 'Upload by team leader:',
+            field8: 'Currently used in this activity:',
+            field9: 'Please select judge first:',
+            field10: 'Format to be displayed during registration:',
+            field11: 'Contents to be selected during registration:',
+            field12: 'Contents displayed during registration:',
+            field13: 'Personalized view of own score:',
+            field14: 'Only display the total score:',
+            field15: 'Display detailed scores:',
+            field16: 'Instructions for releasing the results:',
+            field17: 'Entry Statistic Announcement Method:',
+            field18: 'By Score:',
+            field19: 'By Grade:',
+            field20: 'Statistical Ranking:',
+            field21: 'If not filled in, all works will be announced.:',
+            field22: 'Ranking Method:',
+            field23: 'By Score Range:',
+            field24: 'By Level Number:',
+            field25: 'Level Score Setting:',
+            field26: 'The minimum value of each level should be the same as the maximum value of the previous level, otherwise it will fail to save.:',
+            field27: 'Statistical Announcement of Score Range:',
+            field28: 'Judging has begun:',
+            field29: 'The account is invalid:',
+            field30: 'The account is not registered:',
+            field31: 'Time to announce the result, please do it on time.:',
+            field32: 'Results are now announced.:',
+            field33: "Based on experts' scores:",
+            field34: 'Based on recommended scores:',
+            field35: 'The entries are pre-assigned to the experts. If there is no additional adjustment, please save it so they will be assigned to the experts.:',
+            field36: 'School system and subjects filled in need to be matched with the expert information in order to qualify for success:',
+            field37: 'The template will be saved only after the activity has been successfully created.:',
+        },
+        enrollLabel: 'Registration:',
+        enroll: 'By Registration:',
+        invite: 'By Invitation:',
+        disclaimer: 'Disclaimer:',
         timeList: {
-            enroll: '报名时间',
-            upload: '上传时间',
-            review: '评审时间',
-            score: '公示时间',
-        },
-        enrollNum: '报名人数',
-        fillInfo: '填报信息',
-        entryType: '参赛方式',
-        workType: '作品类型',
-        fileDes: '文件说明',
-        uploadType: '上传方式',
-        reviewTitle: '作品评审',
-        reviewRule: '评审规则',
-        scoreTitle: '成绩公示',
+            enroll: 'Registration Time:',
+            upload: 'Upload Time:',
+            review: 'Judging Time:',
+            score: 'Announce Time:',
+        },
+        enrollNum: 'Number of Applications:',
+        fillInfo: 'Required Information:',
+        entryType: 'How to Enter:',
+        workType: 'Entry Type:',
+        fileDes: 'File Description:',
+        uploadType: 'Upload Method:',
+        reviewTitle: 'Entry Judge:',
+        reviewRule: 'Judging Rules:',
+        reviewRuleInfo: {
+            name: 'Rule Name:',
+            desc: 'Description:',
+            itemize: 'Sub-item:',
+            subName: 'Sub-item Name:',
+            addChild: 'Add Sub-item:',
+            addItem: 'Add Sub-item:',
+        },
+        scoreTitle: 'Grade Announcement:',
         module: {
-            title: '模块',
-            Contest: '赛课活动',
-            Training: '教培活动',
-            Research: '教研活动',
-        },
-        customFields: '自定义填报信息',
-        customTitle: '信息名称',
-        word: '文本',
-        select: '选择器',
+            title: 'Module:',
+            Contest: 'Lecture Contest:',
+            Training: 'Training Activity:',
+            Research: 'Teaching Activity:',
+        },
+        customFields: 'Customized Information:',
+        customTitle: 'Information Name:',
+        word: 'Text:',
+        select: 'Selector:',
         ruleInfo: {
-            rule: '规则',
+            rule: 'Rules:',
         },
         joinType: {
-            person: '个人赛',
-            team: '团队赛',
+            person: 'Individual Contest:',
+            team: 'Team Contest:',
         },
         message: {
-            field1: '活动名称不能为空',
-            field2: '活动主题不能为空',
-            field3: '请选择类型',
-            field4: '请选择活动时间',
-            field5: '请选择报名方式',
-            field6: '请选择填报信息',
-            field7: '请选择评审规则',
-            field8: '创建成功',
-            field9: '填报信息未配置学段、学科,无法进行作品分配',
-            field10: '请完善基本信息',
-            field11: '请完善上传信息',
-            field12: '请完善评审信息',
-            field13: '请完善公示信息',
-            field14: '请先选择一个模块',
-            field15: '请完善信息',
-            field16: '请先设置报名时间',
-            field17: '上传结束时间不能早于报名时间',
-            field18: '请先设置报名时间、上传时间',
-            field19: '评审必须在报名、上传结束后进行',
-            field20: '请先设置评审时间',
-            field21: '公示必须在评审结束后进行',
-            field22: '存在相同文件,请重新上传',
-            field23: '该信息已存在',
+            field1: 'Activity name cannot be empty:',
+            field2: 'Topic cannot be empty:',
+            field3: 'Please select type:',
+            field4: 'Please select the time of the activity:',
+            field5: 'Please select registration method:',
+            field6: 'Please select required information:',
+            field7: 'Please select judging rules:',
+            field8: 'Created successfully:',
+            field9: 'The information provided does not include the school system and subject, and cannot be used for work assignment.:',
+            field10: 'Please complete the basic information:',
+            field11: 'Please complete the upload information:',
+            field12: 'Please complete the judging information:',
+            field13: 'Please complete the announcement information:',
+            field14: 'Please select a module first:',
+            field15: 'Please complete the information:',
+            field16: 'Please set the registration time first:',
+            field17: 'The end time of uploading cannot be earlier than the registration time.:',
+            field18: 'Please set the registration time and upload time first.:',
+            field19: 'Judging must be after registration and uploading.:',
+            field20: 'Please set the judging time first:',
+            field21: 'Announcement must be made after judging:',
+            field22: 'The same file exists, please re-upload:',
+            field23: 'The information already exists:',
+            field24: 'Please select a school first:',
+            field25: 'Maximum grade must be greater than minimum grade.:',
+            field26: 'Level score overlap, please reset.:',
+            field27: 'The level scores do not fully cover all the scores, please add or modify them.:',
+            field28: 'Cannot assign entries if no experts have been added:',
+            field29: 'The number of experts is less than the number of assignments.:',
+            field30: 'Insufficient number of experts for school system match:',
+            field31: 'Insufficient number of experts for subject match:',
+            field32: 'It is not yet time for judging:',
+            field33: 'Insufficient number of experts for school system and subject match:',
+            field34: 'Auto-assign is complete, check for errors, and save.:',
+            field35: 'Failed to assign:',
+            field36: 'Assignment result saved successfully:',
+            field37: 'Failed to save, please re-assign the entries:',
+            field38: 'Judging start successfully:',
+            field39: 'Failed to start judging:',
+            field40: 'No registration information yet:',
+            field41: 'No experts added:',
+            field42: 'Expert account is invalid:',
+            field43: 'Assigned to the expert:',
+            field44: 'Expert does not exist:',
+            field45: 'Expert not changed:',
+            field46: 'Assign successfully:',
+            field47: 'Entries have been judged:',
+            field48: 'Applicants cannot be deleted for team contest:',
+            field49: 'Not within the time of announcement:',
+            field50: 'Failed to start announcement:',
+            field51: 'Failed to edit:',
+            field52: 'Judging has begun and cannot be modified.:',
+        },
+        buttonInfo: {
+            text1: 'Add Judging Expert:',
+            text2: 'Auto-assign Entries:',
+            text3: 'Start Judging:',
+            text4: 'Grade Statistic Setting:',
+            text5: 'Announce Grade:',
+            text6: 'Confirm Announcement:',
+            text7: 'For this activity only:',
+            text8: 'Save as template:',
         },
         modal: {
-            text1: '删除评审规则',
+            text1: 'Delete Judging Rules:',
+            text2: 'Participating Schools:',
+            text3: 'Edit activity basic information:',
+            text4: 'Edit contest information:',
+            text5: 'Add a School:',
+            text6: 'Are you sure you want to delete this activity?:',
+            text7: 'Teacher Registration Information:',
+            text8: 'Experts need to judge the entry:',
+            text9: 'Invite Judging Experts:',
+            text10: 'Manual Assign Entries:',
+            text11: 'Grade Level Setting:',
+            text12: 'Adjust Parameters - Entry Score:',
+            text13: 'Adjust Parameters - After confirming the start of the judging stage, invited experts cannot be deleted, judging rules cannot be adjusted, and the experts can judge the entries, do you confirm the start?:',
+            text14: 'Delete Expert:',
+            text15: 'After confirming the results, participants will be able to view their rankings and individual results.:',
+            text16: 'Do you want to modify the template at the same time:',
+            text17: 'If you confirm, the template will be modified after the activity is saved.:',
+        },
+        website: 'Substation Management:',
+        websiteRoute: 'Sub-station Path:',
+        authorized: 'Authorized Unit/Organization:',
+        change: 'Edit:',
+        addBanner: 'Add Home Page Display:',
+        builtBanner: 'Add Home Page Display:',
+        subtitle: 'Sub-title:',
+        jumpAddress: 'Jump Address:',
+        displayTime: 'Display Time:',
+        displayImg: 'Display Picture:',
+        listSelect: 'Activity List Selection:',
+        publishAct: 'Announce public activity:',
+        unconfirmed: 'Not Confirmed:',
+        beConfirmed: 'To be confirmed:',
+        returnList: 'Back to Activity List:',
+        unrestricted: 'No school restricted:',
+        unrestricted1: 'No restriction:',
+        uploadWork: 'Uploaded Entries:',
+        workRequire: 'Entry Requirements:',
+        signUpload: 'Registration - Upload:',
+        publicity: 'Announcement:',
+        duties: 'Duty:',
+        gradeName: 'Level Name:',
+        top: 'Name:',
+        signData: 'Application Data:',
+        registered: 'Registered:',
+        reviewed: 'Judged:',
+        inviteTea: 'Invited Teachers:',
+        invited: 'Invited:',
+        notSign: 'Unregistered:',
+        notSignTime: 'Not yet available:',
+        allocation: 'Assign:',
+        reviewManage: 'Judging Management:',
+        reviewNum: 'Number of Judging:',
+        workAllocation: 'Entry:',
+        reviewMethod: {
+            title: 'Judging Method:',
+            text1: 'Scoring for sub-item:',
+            text2: 'Total Score Only:',
+        },
+        scoreRule: 'Uniform Scoring Rules:',
+        adjust: 'Adjustment:',
+        secrecy: 'Confidentiality:',
+        teamName: 'Team Name:',
+        teamCipher: 'Team ID:',
+        assignExpert: 'Assigned Experts:',
+        notAlloacte: 'Not yet assigned:',
+        notUpload: 'Not uploaded yet:',
+        works: 'Entry::',
+        showScore: {
+            title: 'Scores displayed::',
+            text1: "Based on experts' scores:",
+            text2: 'Based on recommended scores:',
+            text3: 'Recommended Score::',
+        },
+        joinStatus: 'Status:',
+        tableColumn: {
+            reviewSubject: 'Subject of Judge:',
+            reviewNum: 'Number of Judge:',
+            reviewPro: 'Judging Progress:',
+            nameGroup: 'Name/Team Name:',
+            uploadName: 'Entry Title:',
+            expertScore: "Expert's Score:",
+            maskScore: 'Recommended Score:',
+            showScore: 'Score Display:',
+            scoreLevel: 'Preset Awards:',
+        },
+        expertExcel: {
+            importTem: 'Import Expert Template:',
+            name: 'name: mandatory, expert name:',
+            title: "title: optional, expert's title:",
+            mobile: "mobile: optional, expert's cell phone number (At least one of mobile, email, or tmdid should be filled in.):",
+            email: "email: optional, expert's email (At least one of mobile, email, or tmdid should be filled in.):",
+            tmdid: "tmdid: optional, expert's TEAM Model ID (At least one of mobile, email, or tmdid should be filled in.):",
+            modules: 'modules: mandatory, the modules to be evaluated by the expert ("Contest" means lecture contest):',
+            subjects: 'subjects: mandatory, subject to be evaluated by the expert (Enter by the format "school system-subject". If there is more than one subject, separate by comma):',
+            perSub: 'Download the default school system-subject comparison table:',
+            review: 'Judging Module:',
+            subject: 'Subject:',
+            excelName: 'School System-Subject Comparison Table:',
         },
     },
 }

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

@@ -575,6 +575,9 @@ const LANG_ZH_CN = {
         pdName5: '数据储存服务空间',
         pdName6: 'Haboard醍摩豆智慧大屏',
         pdName7: '教研中心模组',
+        pdName8: '苏格拉底',
+        pdName9: '艺术评测服务',
+        pdName10: '五育看板',
         authDate: '有效期:',
         fuAuth: '功能权限:',
         IRSnumber: 'IRS链接数:',
@@ -866,6 +869,14 @@ const LANG_ZH_CN = {
         tips1: '修改信息'
     },
     newCusMgt: {
+        irs:{
+            batchSetting:'批量设置IRS',
+            batchText:'批量修改',
+            cancel:'取消批量',
+            repeatTip:'这些IRS号码存在重复,请检查后再尝试:',
+            allowPick:'允许自选座号',
+            pickTip:'如果您自选的IRS座号已被占用,会由系统自动分配新的IRS编号'
+        },
         configCourse: '本学期已配置课程',
         recommendFiles: '课程备用资料',
         sort: '排序方式',
@@ -1078,6 +1089,7 @@ const LANG_ZH_CN = {
         copyUrl: '复制链接',
         joinMessage1: "允许加入课程",
         joinMessage2: "禁止加入课程",
+        defaultDesc:'云端个人课程',
         inviteInfo1: '邀请加入醍摩豆云平台IES教师个人课程',
         inviteInfo2: '课程名称:',
         inviteInfo3: '课程名单:',
@@ -1090,6 +1102,7 @@ const LANG_ZH_CN = {
         createTips2: '温馨提示:您(暂未加入学校)可让学生通过输入课程邀请码、扫描课程二维码、课程链接方式主动加入课程。',
         renameListTitle: '修改名称',
         selectListTips: '请选择名单',
+        renameRepeat: '名单名称重复',
         alreadyExist: '已在课程名单',
         listAPIErr: '名单列表获取失败',
         rmvListTips: '温馨提示:当前名单在其他课程也在使用',
@@ -1345,7 +1358,7 @@ const LANG_ZH_CN = {
         //MgtStuList.vue
         nameList: '名单',
         remvStu: '移除学生',
-        editStu: '置IRS',
+        editStu: '置IRS',
         goBack: '返回上级',
         delListTitle: '删除自定义名单',
         notSet: '未设置',
@@ -1490,6 +1503,7 @@ const LANG_ZH_CN = {
             filter4: '互动',
             filter5: '测验',
             filter6: '智慧评分',
+            filter7: '协作',
             evt1: '抢权:',
             evt2: '课中评测数据',
             evt3: '挑人:',
@@ -1510,6 +1524,12 @@ const LANG_ZH_CN = {
             personMutual: '每人多件评分',
             randomMutual: '随机分配互评',
             anonymousSub: '匿名提交',
+            coworkAll: '全体协作',
+            coworkGroup: '分组协作',
+            coworkdiff: '差异化协作',
+            contribution: '贡献度',
+            personContri: '个人贡献度',
+            groupContri: '小组贡献度'
         }
     },
     result: {
@@ -1688,6 +1708,21 @@ const LANG_ZH_CN = {
         cancelSuc: '取消成功',
         addSuc: '添加成功',
         newExercise: {
+            answerType:{
+                types:'作答方式',
+                text:'文本',
+                textImage:'标记',
+                image:'图片',
+                file:'文档',
+                audio:'音频',
+                autoScore:'自动评分',
+                answerLang:'语言类型',
+                us:'英语',
+                zh:'普通话',
+                jp:'日语',
+                kr:'韩语',
+                hk:'粤语'
+            },
             newSchoolItem: '新建学校题目',
             newPrivateItem: '新建个人题目',
             backToBank: '返回题库',
@@ -1820,7 +1855,8 @@ const LANG_ZH_CN = {
             choosed: '取消选择',
             searchPaper: '输入试卷名称进行搜索...',
             copyTip1: '确认以',
-            copyTip2: '创建副本'
+            copyTip2: '创建副本',
+            copyTip3: '创建副本'
         },
         importFile: {
             uploadSuc: '文件上传解析成功!',
@@ -6190,6 +6226,8 @@ const LANG_ZH_CN = {
         tips6: '重命名',
         tips7: '删除文件',
         tips8: '您的浏览器不支持 video 标签。',
+        tips9: '设置轮播图',
+        setCarousel: '确认将当前图片设为五育看板的轮播图吗?',
         props1: '确认删除',
         props2: '文件删除成功!',
         props3: '文件删除失败!',
@@ -7586,7 +7624,7 @@ const LANG_ZH_CN = {
             subjectP: '科目占比'
         },
         class: {
-            total: '今年总数据',
+            total: '今年',
             lastWeek: '上周',
             nowMonth: '本月',
             vitality: '活跃度',
@@ -7882,6 +7920,12 @@ const LANG_ZH_CN = {
             field11: '自定义填报的信息',
             field12: '请输入选项内容',
             field13: '仅作展示',
+            field14: '暂无路由',
+            field15: '请输入等级标签',
+            field16: '请输入建议分数',
+            field17: '请输入规则名称',
+            field18: '请输入规则描述',
+            field19: '对分项内容进行描述',
         },
         profile: '简介',
         host: '主办',
@@ -7904,6 +7948,31 @@ const LANG_ZH_CN = {
             field10: '报名时展示的填报格式',
             field11: '报名时可供选择的内容',
             field12: '报名时展示的内容',
+            field13: '个人查看自己成绩',
+            field14: '只展示总分',
+            field15: '展示细项得分',
+            field16: '成绩公布说明',
+            field17: '统计作品公布方式',
+            field18: '按分数排名公布',
+            field19: '按等级分布',
+            field20: '统计公布排名',
+            field21: '不填则表示公布所有',
+            field22: '等级排序方式',
+            field23: '按照分数范围',
+            field24: '按照等级数量',
+            field25: '等级分数设置',
+            field26: '各等级的最小值要与上一级的最大值一致,否则会保存失败',
+            field27: '统计公布成绩范围',
+            field28: '评审已开始',
+            field29: '该账号无效',
+            field30: '该账号未注册',
+            field31: '已到成绩公示阶段,请及时发布成绩',
+            field32: '已开始公示成绩',
+            field33: '专家评分为准',
+            field34: '建议分数为准',
+            field35: '已为专家预分配作品,若无调整,请保存分配结果,作品才会真正分配给专家',
+            field36: '填报信息的学段、学科需与专家信息一致才能匹配成功',
+            field37: '活动创建成功后才会正式保存为模板',
         },
         enrollLabel: '报名',
         enroll: '报名制',
@@ -7923,6 +7992,14 @@ const LANG_ZH_CN = {
         uploadType: '上传方式',
         reviewTitle: '作品评审',
         reviewRule: '评审规则',
+        reviewRuleInfo: {
+            name: '规则名称',
+            desc: '规则描述',
+            itemize: '分项',
+            subName: '分项名称',
+            addChild: '添加子项',
+            addItem: '添加分项',
+        },
         scoreTitle: '成绩公示',
         module: {
             title: '模块',
@@ -7965,9 +8042,145 @@ const LANG_ZH_CN = {
             field21: '公示必须在评审结束后进行',
             field22: '存在相同文件,请重新上传',
             field23: '该信息已存在',
+            field24: '请先选择学校',
+            field25: '等级分数最大值必须大于最小值',
+            field26: '等级分数重叠,请重新设置',
+            field27: '等级分数未完全覆盖,请添加或修改',
+            field28: '未添加专家,无法分配作品',
+            field29: '评审专家人数少于作品分配次数',
+            field30: '学段匹配的专家数量不足',
+            field31: '学科匹配的专家数量不足',
+            field32: '未到评审时间',
+            field33: '学段和学科匹配的专家数量不足',
+            field34: '自动分配完成,检查无误后请保存',
+            field35: '分配失败',
+            field36: '成功保存分配结果',
+            field37: '保存失败,请重新分配作品',
+            field38: '成功开启评审',
+            field39: '开启评审失败',
+            field40: '暂无报名信息',
+            field41: '未添加专家',
+            field42: '专家帐号无效',
+            field43: '已分配给该专家',
+            field44: '评审专家不存在',
+            field45: '未变更专家',
+            field46: '分配成功',
+            field47: '该作品已评审',
+            field48: '团队赛不可删除报名选手',
+            field49: '不在公示时间范围内',
+            field50: '开启公示失败',
+            field51: '编辑失败',
+            field52: '评审已开始,无法修改',
+        },
+        buttonInfo: {
+            text1: '添加评审专家',
+            text2: '自动分配评审作品',
+            text3: '开始评审',
+            text4: '统计成绩设置',
+            text5: '公示成绩',
+            text6: '确认公示',
+            text7: '仅本次活动使用',
+            text8: '保存为模板',
         },
         modal: {
             text1: '删除评审规则',
+            text2: '参与本次活动的学校',
+            text3: '编辑活动基本信息',
+            text4: '编辑赛课活动信息',
+            text5: '添加学校',
+            text6: '确定删除本次活动吗?',
+            text7: '教师报名信息',
+            text8: '专家需评审作品',
+            text9: '邀请评审专家',
+            text10: '手动分配作品',
+            text11: '成绩等级设置',
+            text12: '调整参数作品分数',
+            text13: '调整参数确认开始评审后,不可删除已邀请的专家,不可调整评审规则,同时专家可以对作品进行评审,是否确认开始评审?',
+            text14: '删除专家',
+            text15: '确认公示成绩后,参赛者将可以查看成绩排名和个人成绩',
+            text16: '是否同时修改模板内容',
+            text17: '如果确认修改,会在活动保存后同步修改模板内容',
+        },
+        website: '分站管理',
+        websiteRoute: '分站路由',
+        authorized: '授权单位/组织',
+        change: '更改',
+        addBanner: '添加首页展示',
+        builtBanner: '新建首页展示',
+        subtitle: '副标题',
+        jumpAddress: '跳转地址',
+        displayTime: '展示时间',
+        displayImg: '展示图片',
+        listSelect: '活动列表选择',
+        publishAct: '发布公开活动',
+        unconfirmed: '未确认',
+        beConfirmed: '待确认',
+        returnList: '返回活动列表',
+        unrestricted: '未限制学校',
+        unrestricted1: '不限制',
+        uploadWork: '上传作品',
+        workRequire: '作品要求',
+        signUpload: '报名-上传',
+        publicity: '公示',
+        duties: '职务',
+        gradeName: '等级名称',
+        top: '名',
+        signData: '报名数据',
+        registered: '已报名',
+        reviewed: '已评审',
+        inviteTea: '邀请老师',
+        invited: '已邀请',
+        notSign: '未报名',
+        notSignTime: '未到报名时间',
+        allocation: '分配',
+        reviewManage: '评审管理',
+        reviewNum: '评审次数',
+        workAllocation: '作品分配',
+        reviewMethod: {
+            title: '评审方式',
+            text1: '对细项评分',
+            text2: '只打总分',
+        },
+        scoreRule: '统分规则',
+        adjust: '调整',
+        secrecy: '保密',
+        teamName: '团队名称',
+        teamCipher: '团队编码',
+        assignExpert: '分配专家',
+        notAlloacte: '暂未分配',
+        notUpload: '暂未上传',
+        works: '作品:',
+        showScore: {
+            title: '分数展示:',
+            text1: '专家评分为准',
+            text2: '建议分数为准',
+            text3: '建议分数:',
+        },
+        joinStatus: '参赛状态',
+        tableColumn: {
+            reviewSubject: '评审科目',
+            reviewNum: '评审数量',
+            reviewPro: '评审进度',
+            nameGroup: '姓名/组名',
+            uploadName: '作品名称',
+            expertScore: '专家评分',
+            maskScore: '建议分数',
+            showScore: '分数展示',
+            scoreLevel: '预设奖项',
+        },
+        expertExcel: {
+            importTem: '导入专家范本',
+            name: 'name: 必填,专家名称',
+            title: 'title: 选填,专家职称',
+            mobile: 'mobile: 选填,专家手机号(mobiel, email, tmdid至少填写一个)',
+            email: 'email: 选填,专家邮箱号码(mobiel, email, tmdid至少填写一个)',
+            tmdid: 'tmdid: 选填,专家醍摩豆ID(mobiel, email, tmdid至少填写一个)',
+            modules: 'modules: 必填,专家需评审的模块(Contest表示赛课活动)',
+            subjects: 'subjects: 必填,专家评审科目(采用“学段-科目”格式,多个科目用逗号隔开)',
+            perSub: '下载填报信息时预设的学段-学科对照表',
+            review: '评审模块',
+            subject: '科目',
+            excelName: '学段-学科对照表',
         },
     },
 }

+ 448 - 238
TEAMModelOS/ClientApp/public/lang/zh-TW.js

@@ -575,6 +575,9 @@ const LANG_ZH_TW = {
         pdName5: '數據儲存服務空間',
         pdName6: 'Haboard醍摩豆智慧大屏',
         pdName7: '教研中心模組',
+        pdName8: '蘇格拉底',
+        pdName9: '藝術評量服務',
+        pdName10: '五育看板',
         authDate: '有效期:',
         fuAuth: '功能權限:',
         IRSnumber: '學生連線數:',
@@ -866,6 +869,14 @@ const LANG_ZH_TW = {
         tips1: '修改資訊'
     },
     newCusMgt: {
+        irs: {
+            batchSetting: '批次設定座號(IRS號碼)',
+            batchText: '批次修改',
+            cancel: '取消批次設定',
+            repeatTip: '這些座號(IRS號碼)有重複,請檢查後再嘗試:',
+            allowPick: '允許自選座號',
+            pickTip: '如果您自選的座號已被佔用,會由系統自動分配'
+        },
         configCourse: '本學期已配置課程',
         recommendFiles: '課程備用數據',
         sort: '排序方式',
@@ -1078,6 +1089,7 @@ const LANG_ZH_TW = {
         copyUrl: '複製網址',
         joinMessage1: "允許加入課程",
         joinMessage2: "禁止加入課程",
+        defaultDesc: '雲端個人課程',
         inviteInfo1: '邀請加入醍摩豆雲平臺IES教師個人課程',
         inviteInfo2: '課程名稱:',
         inviteInfo3: '課程名單:',
@@ -1090,6 +1102,7 @@ const LANG_ZH_TW = {
         createTips2: '溫馨提示:您(暫未加入學校)可讓學生透過輸入課程邀請碼、掃描課程QR Code、課程網址方式主動加入課程。 ',
         renameListTitle: '修改名稱',
         selectListTips: '請選擇名單',
+        renameRepeat: '名單名稱重複',
         alreadyExist: '已在課程名單',
         listAPIErr: '名單列表獲取失敗',
         rmvListTips: '溫馨提示:當前名單在其他課程也在使用',
@@ -1492,6 +1505,7 @@ const LANG_ZH_TW = {
             filter4: '互動',
             filter5: '測驗',
             filter6: '智慧評分',
+            filter7: '協作',
             evt1: '搶權:',
             evt2: '課中評量數據',
             evt3: '挑人:',
@@ -1513,6 +1527,12 @@ const LANG_ZH_TW = {
             personMutual: '每人多件評分',
             randomMutual: '隨機分配互評',
             anonymousSub: '匿名提交',
+            coworkAll: '全體協作',
+            coworkGroup: '分組協作',
+            coworkdiff: '差異化協作',
+            contribution: '貢獻度',
+            personContri: '個人貢獻度',
+            groupContri: '小組貢獻度'
         }
     },
     result: {
@@ -1691,6 +1711,21 @@ const LANG_ZH_TW = {
         cancelSuc: '取消成功',
         addSuc: '新增成功',
         newExercise: {
+            answerType: {
+                types: '作答方式',
+                text: '文字',
+                textImage: '畫記',
+                image: '圖片',
+                file: '檔案',
+                audio: '錄音',
+                autoScore: '自動評分',
+                answerLang: '語言類型',
+                us: '英語',
+                zh: '國語',
+                jp: '日語',
+                kr: '韓語',
+                hk: '粵語'
+            },
             newSchoolItem: '新增學校題目',
             newPrivateItem: '新增個人題目',
             backToBank: '返回題庫',
@@ -1823,7 +1858,8 @@ const LANG_ZH_TW = {
             choosed: '取消選擇',
             searchPaper: '輸入試卷名稱進行搜尋…',
             copyTip1: '確認以',
-            copyTip2: '複製建立試卷'
+            copyTip2: '複製建立試卷',
+            copyTip3: '複製建立試卷'
         },
         importFile: {
             uploadSuc: '試卷檔案上傳解析成功!',
@@ -2531,7 +2567,7 @@ const LANG_ZH_TW = {
             placeholder1: "請輸入關鍵字",
             placeholder2: "新的話題",
             placeholder3: "請選擇",
-            placeholder4: "請輸入正文",
+            placeholder4: "請編輯內容",
             success1: "發表成功",
             error1: "發表失敗",
             message1: "切換至我的話題",
@@ -3447,7 +3483,7 @@ const LANG_ZH_TW = {
         },
         registModal: {
             title: '驗證手機',
-            form : {
+            form: {
                 placeholder: {
                     phone: '請輸入手機',
                     pindCode: '請輸入驗證碼'
@@ -3649,7 +3685,7 @@ const LANG_ZH_TW = {
         delete: '刪除',
         view: '查看',
         edit: '編輯',
-        tableIsNaN:'尚無成績表',
+        tableIsNaN: '尚無成績表',
         noNotify: '暫無公告',
         notifyDetail: '公告詳情',
         delNotifyTitle: '刪除公告',
@@ -6193,6 +6229,8 @@ const LANG_ZH_TW = {
         tips6: '重命名',
         tips7: '刪除檔案',
         tips8: '您的瀏覽器不支援video標籤。',
+        tips9: '設定輪播圖',
+        setCarousel: '確定將當前的圖片設成五育儀表的輪播圖嗎?',
         props1: '確認刪除',
         props2: '檔案刪除成功!',
         props3: '檔案刪除失敗!',
@@ -7588,7 +7626,7 @@ const LANG_ZH_TW = {
             subjectP: '科目百分比'
         },
         class: {
-            total: '今年總數據',
+            total: '今年',
             lastWeek: '上週',
             nowMonth: '本月',
             vitality: '活躍度',
@@ -7709,267 +7747,439 @@ const LANG_ZH_TW = {
             totalTime: '總時間數'
         }
     },
-    scoreCalc:{
-        creatDate:'建立日期',
-        customInput:'手動輸入',
-        onlineClass:'線上課堂',
-        onlineHomeWork:'線上作業',
-        IRSnumber:'IRS',
-        addTableInfo:'請先新增成績表',
-        newTable:'新成績統計表',
-        newProject:'新項目',
-        subItem:'子項目',
-        notRated:'未評分',
-        tableIsNaN:'尚無成績表',
-        isNaN:'內容為空',
-        description1:'底線字',
-        description1_2:'可點擊編輯細項',
-        blueWord:'藍字',
-        canModifyable:'可修改',
-        description2:'可修改內容,(括弧)的數字為分數 x 百分比%的結果',
-        simple_complete:'精簡模式/完整模式',  
-        title:'標題',              
-        name:'姓名',
-        id:'學號',
-        class:'課堂',
-        th_homework:'作業',
-        th_exam:'評量',
-        totalScore:'總評分',
-        PRScore:'PR值',
-        gradesInClass:'課堂細項成績',    
-        attendType: "出席狀態",    
-        attend:'出席',//英文版Attendance
-        point:'記分板',
-        interactiveScore:'互動分',
-        attend2:'出席',//英文版Attend
-        leave:'請假',//英文版leave
-        absence:'缺席',//英文版Abs
-        sickLeave:'病假',//英文版SL
-        personalLeave:'事假',//英文版PL
-        publicLeave:'公假',//英文版PH
-        absence2:'缺席',//英文版Absence
-        sickLeave2:'病假',//英文版Sick leave
-        personalLeave2:'事假',//英文版Personal leave
-        publicLeave2:'公假',//英文版Public holiday
-        isSave:'已存檔',
-        notSave:'未存檔',
-        delete:'刪除',
-        export:'匯出',
-        print:'列印',
-        save:'儲存',
-        scoreCalculation:'成績計算',
-        simpleAttendanceCalculation:'簡單出席計算法',
-        attendanceCalculationMethod:'出席率計算法',
-        customCalculation:'自訂計算',
-        percentageScore:'標準化評分',
-        standardizedsScoring:'百分比評分',
-        customScoring:'自訂評分',
-        calculationDescription1:'教師根據學生的出席情況,直接給予分數。通常是以全勤為標準,缺席一次扣一定分數。',
-        calculationDescription2:'請輸入一次缺席扣多少分數',
-        marks:'分',
-        example:'範例:',
-        attendance:'出席成績',
-        fullMarksAttend:'出席滿分',
-        attendPercentage:'出席百分比',
-        absences:'缺席次數',
-        calculationDescription3:'缺席一次扣 ',
-        calculationFormula:'計算公式',
-        calculationDescription4:'勾選即為缺席狀態:',                   
-        checkOk:'確定',
-        cancel:'取消',
-        calculationDescription5:'根據學生的出席次數計算出席率,再以此作為出席分數的依據。出席率以百分比表示,例如出席率超過90%可得滿分,每低於一個百分點扣一定分數。',
-        calculationDescription6:'每缺席1% 扣',
-        attendanceOver:'出席率超過',
-        isFullMark:'% 為滿分',
-        classGrades:'課堂成績:',
-        totalNumberClasses:'課堂總次數',
-        attendQwen:'出席次數', 
-        formula:'公式:',
-        calculationDescription7:'請依照下列變數填寫計算公式:',
-        numberOfSick:'病假次數',
-        numberOfPerson:'事假次數',
-        numberOfPublic:'公假次數',
-        calculationDescription8:'公式範例(以下直接以出席率為出席分數):',
-        calculationDescription9:'※可使用excel公式寫!',
-        instantPreview:'算式寫法即時預覽',
-        calculationDescription10:'使用標準化方法將學生的得分轉換為標準分數。',
-        gravityCalculationScore:'比重計算分數',
-        averageScore:'平均分數',
-        standardDeviation:'標準差',
-        standardScoreZFormula:'標準分數(Z)公式:',
-        derivedStandardScoreTFormula:'衍生標準分數(T)公式:',
-        calculationDescription11:'(通常用於解決小數及負數問題)',
-        indicatesAverageScore:'表示平均分數',
-        calculationDescription12:'將學生的得分除以活動的最高分數。然後乘以100,即可計算出學生的百分比評分。',
-        highestScore:'最高分數',
-        calculationDescription13:'請依照下列變數填寫計算公式:',        
-        classQwen:'課堂總數',
-        totalScoreClass:'全班總分',        
-        totalNumberStudents:'學生總數',
-        formulaExample:'公式範例:',
-        writtenas:'寫法為:',
-        scoreboardScore:'記分板成績',          
+    scoreCalc: {
+        creatDate: '建立日期',
+        customInput: '手動輸入',
+        onlineClass: '線上課堂',
+        onlineHomeWork: '線上作業',
+        IRSnumber: 'IRS',
+        addTableInfo: '請先新增成績表',
+        newTable: '新成績統計表',
+        newProject: '新項目',
+        subItem: '子項目',
+        notRated: '未評分',
+        tableIsNaN: '尚無成績表',
+        isNaN: '內容為空',
+        description1: '底線字',
+        description1_2: '可點擊編輯細項',
+        blueWord: '藍字',
+        canModifyable: '可修改',
+        description2: '可修改內容,(括弧)的數字為分數 x 百分比%的結果',
+        simple_complete: '精簡模式/完整模式',
+        title: '標題',
+        name: '姓名',
+        id: '學號',
+        class: '課堂',
+        th_homework: '作業',
+        th_exam: '評量',
+        totalScore: '總評分',
+        PRScore: 'PR值',
+        gradesInClass: '課堂細項成績',
+        attendType: "出席狀態",
+        attend: '出席',//英文版Attendance
+        point: '記分板',
+        interactiveScore: '互動分',
+        attend2: '出席',//英文版Attend
+        leave: '請假',//英文版leave
+        absence: '缺席',//英文版Abs
+        sickLeave: '病假',//英文版SL
+        personalLeave: '事假',//英文版PL
+        publicLeave: '公假',//英文版PH
+        absence2: '缺席',//英文版Absence
+        sickLeave2: '病假',//英文版Sick leave
+        personalLeave2: '事假',//英文版Personal leave
+        publicLeave2: '公假',//英文版Public holiday
+        isSave: '已存檔',
+        notSave: '未存檔',
+        delete: '刪除',
+        export: '匯出',
+        print: '列印',
+        save: '儲存',
+        scoreCalculation: '成績計算',
+        simpleAttendanceCalculation: '簡單出席計算法',
+        attendanceCalculationMethod: '出席率計算法',
+        customCalculation: '自訂計算',
+        percentageScore: '標準化評分',
+        standardizedsScoring: '百分比評分',
+        customScoring: '自訂評分',
+        calculationDescription1: '教師根據學生的出席情況,直接給予分數。通常是以全勤為標準,缺席一次扣一定分數。',
+        calculationDescription2: '請輸入一次缺席扣多少分數',
+        marks: '分',
+        example: '範例:',
+        attendance: '出席成績',
+        fullMarksAttend: '出席滿分',
+        attendPercentage: '出席百分比',
+        absences: '缺席次數',
+        calculationDescription3: '缺席一次扣 ',
+        calculationFormula: '計算公式',
+        calculationDescription4: '勾選即為缺席狀態:',
+        checkOk: '確定',
+        cancel: '取消',
+        calculationDescription5: '根據學生的出席次數計算出席率,再以此作為出席分數的依據。出席率以百分比表示,例如出席率超過90%可得滿分,每低於一個百分點扣一定分數。',
+        calculationDescription6: '每缺席1% 扣',
+        attendanceOver: '出席率超過',
+        isFullMark: '% 為滿分',
+        classGrades: '課堂成績:',
+        totalNumberClasses: '課堂總次數',
+        attendQwen: '出席次數',
+        formula: '公式:',
+        calculationDescription7: '請依照下列變數填寫計算公式:',
+        numberOfSick: '病假次數',
+        numberOfPerson: '事假次數',
+        numberOfPublic: '公假次數',
+        calculationDescription8: '公式範例(以下直接以出席率為出席分數):',
+        calculationDescription9: '※可使用excel公式寫!',
+        instantPreview: '算式寫法即時預覽',
+        calculationDescription10: '使用標準化方法將學生的得分轉換為標準分數。',
+        gravityCalculationScore: '比重計算分數',
+        averageScore: '平均分數',
+        standardDeviation: '標準差',
+        standardScoreZFormula: '標準分數(Z)公式:',
+        derivedStandardScoreTFormula: '衍生標準分數(T)公式:',
+        calculationDescription11: '(通常用於解決小數及負數問題)',
+        indicatesAverageScore: '表示平均分數',
+        calculationDescription12: '將學生的得分除以活動的最高分數。然後乘以100,即可計算出學生的百分比評分。',
+        highestScore: '最高分數',
+        calculationDescription13: '請依照下列變數填寫計算公式:',
+        classQwen: '課堂總數',
+        totalScoreClass: '全班總分',
+        totalNumberStudents: '學生總數',
+        formulaExample: '公式範例:',
+        writtenas: '寫法為:',
+        scoreboardScore: '記分板成績',
         lessonRecordTitle: '課堂紀錄',
         homeworkTitle: '作業活動',
-        examTitle: '評量活動',        
-        totalRate:'總加權',
-        itemName:'項目名稱',
-        percentage:'百分比',
-        tickIncludedScore:'↓勾選則計入成績。 ',
-        dropSort:'※拖曳可排序',
-        itemRate:'比重',
-        logStudentScore:'編輯學生分數',
-        average:'平均',
-        loging:'登錄',
-        score:'成績',
-        attendState:'※出席分數:1出席 2缺席 4病假 5事假 6公假',
-        pasteMultiple:' 多筆貼上',
-        pasteEXCEL:' ※可直接從EXCEL貼上',       
-        back:'返回',        
-        warringTotalRate:'總加權不可超過100%',
-        dataIsSave:'資料已儲存',
-        remind:'提醒',
-        remindMessage1:'有未儲存的設定,要繼續切換活動項目嗎?',
-        remindMessage2:'有未儲存的設定,要繼續新增活動項目嗎?',
-        deleteRemind1:'刪除活動子項目',
-        remindMessage3:'活動子項目數據刪除後將無法找回,確認刪除當前活動子項目嗎?',
-        deleteRemind2:'刪除活動項目',
-        remindMessage4:'活動項目已刪除',
-        remindMessage5:'分數登錄完成',
-        remindMessage6:'有未儲存的設定,確定要繼續嗎?',
-        scoreCalc:'成績計算',        
-        modifyContent:'可修改內容       ',
-        ask:'詢問',
-        DelTableQuestion:'成績表數據刪除後將無法找回,確認刪除此成績表嗎?',
-        saveAsk:'是否確定要儲存此資料表所有內容呢?', 
-        addAsk:'是否確定要新增活動項目呢?',
-        APIerr:'API請求失敗',    
-        AddScoreSheet:'新增成績表',
-        chooseAddWay:'請選擇新增成績表的方式',
-        addNewTable:'新增"新"成績表',
-        InheritTable:'沿用成績表',
-        addTableNotice:'注意:新增的成績表成績以當下系統有的成績為準,成績表成績沒有跟其他功能連動',
+        examTitle: '評量活動',
+        totalRate: '總加權',
+        itemName: '項目名稱',
+        percentage: '百分比',
+        tickIncludedScore: '↓勾選則計入成績。 ',
+        dropSort: '※拖曳可排序',
+        itemRate: '比重',
+        logStudentScore: '編輯學生分數',
+        average: '平均',
+        loging: '登錄',
+        score: '成績',
+        attendState: '※出席分數:1出席 2缺席 4病假 5事假 6公假',
+        pasteMultiple: ' 多筆貼上',
+        pasteEXCEL: ' ※可直接從EXCEL貼上',
+        back: '返回',
+        warringTotalRate: '總加權不可超過100%',
+        dataIsSave: '資料已儲存',
+        remind: '提醒',
+        remindMessage1: '有未儲存的設定,要繼續切換活動項目嗎?',
+        remindMessage2: '有未儲存的設定,要繼續新增活動項目嗎?',
+        deleteRemind1: '刪除活動子項目',
+        remindMessage3: '活動子項目數據刪除後將無法找回,確認刪除當前活動子項目嗎?',
+        deleteRemind2: '刪除活動項目',
+        remindMessage4: '活動項目已刪除',
+        remindMessage5: '分數登錄完成',
+        remindMessage6: '有未儲存的設定,確定要繼續嗎?',
+        scoreCalc: '成績計算',
+        modifyContent: '可修改內容       ',
+        ask: '詢問',
+        DelTableQuestion: '成績表數據刪除後將無法找回,確認刪除此成績表嗎?',
+        saveAsk: '是否確定要儲存此資料表所有內容呢?',
+        addAsk: '是否確定要新增活動項目呢?',
+        APIerr: 'API請求失敗',
+        AddScoreSheet: '新增成績表',
+        chooseAddWay: '請選擇新增成績表的方式',
+        addNewTable: '新增"新"成績表',
+        InheritTable: '沿用成績表',
+        addTableNotice: '注意:新增的成績表成績以當下系統有的成績為準,成績表成績沒有跟其他功能連動',
     },
     activity: {
         scoreWord: {
             only: '按評審分數',
-            avg: '按平均分',
+            avg: '依平均分數',
             top: '按最高分',
-            rmLowAvg: '去掉最低分的平均分',
-            rmTopAvg: '去掉最高分的平均分',
+            rmLowAvg: '去掉最低分的平均分',
+            rmTopAvg: '去掉最高分的平均分',
             rmLowTopAvg: '去掉最高分和最低分的平均分',
         },
         distributeWord: {
             none: '不需要匹配',
             period: '只匹配學段',
             subject: '只匹配學科',
-            periodAndSubject: '同匹配學科和學段',
+            periodAndSubject: '同匹配學科和學段',
         },
-        basicInfo: '基本息',
-        moduleConfig: '模块配置',
+        basicInfo: '基本息',
+        moduleConfig: '模組設定',
         placeholder: {
-            field1: '请输入活动主题',
-            field2: '请输入活动简介',
-            field3: '请输入活动地点',
-            field4: '请输入主办单位名称',
-            field5: '请输入承办单位名称',
-            field6: '请输入免责申明',
-            field7: '上传主题图片',
-            field8: '上传活动文件',
+            field1: '請輸入活動主題',
+            field2: '請輸入活動簡介',
+            field3: '請輸入活動地點',
+            field4: '請輸入主辦單位名稱',
+            field5: '請輸入承辦單位名稱',
+            field6: '請輸入免責聲明',
+            field7: '上傳主題圖片',
+            field8: '上傳活動文件',
             field9: '不限格式',
-            field10: '请输入作品要求',
-            field11: '自定义填报的信息',
-            field12: '请输入选项内容',
-            field13: '仅作展示',
-        },
-        profile: '简介',
-        host: '主办',
-        undertake: '承办',
+            field10: '請輸入作品要求',
+            field11: '自訂填報的資訊',
+            field12: '請輸入選項內容',
+            field13: '僅供展示',
+            field14: '暫無路徑',
+            field15: '請輸入等級標籤',
+            field16: '請輸入建議分數',
+            field17: '請輸入規則名稱',
+            field18: '請輸入規則描述',
+            field19: '對細項內容進行描述',
+        },
+        profile: '簡介',
+        host: '主辦',
+        undertake: '承辦',
         scope: {
-            open: '公开',
-            area: '区级',
-            school: '校级',
+            open: '公',
+            area: '區級',
+            school: '校',
         },
         prompt: {
-            field1: '不选学校则表示所有学校都可参加该活动',
-            field2: '发布活动后,需学校先确认参与活动,老师才可报名参与',
-            field3: '由学校选择老师参与活动',
-            field4: '请选择本次活动需要添加的模块',
-            field5: '0:默认不限制',
-            field6: '个人上传作品',
-            field7: '由队长统一上传',
-            field8: '当前活动使用',
-            field9: '请先勾选作品评审',
-            field10: '报名时展示的填报格式',
-            field11: '报名时可供选择的内容',
-            field12: '报名时展示的内容',
-        },
-        enrollLabel: '报名',
-        enroll: '报名制',
-        invite: '邀请制',
-        disclaimer: '免责声明',
+            field1: '不選學校則表示所有學校都可參加該活動',
+            field2: '發布活動後,需學校先確認參與活動,老師方可報名參與',
+            field3: '由學校選出老師參與活動',
+            field4: '請選擇本次活動所需新增的模組',
+            field5: '0:預設不限制',
+            field6: '個人上傳作品',
+            field7: '隊長統一上傳',
+            field8: '目前活動使用',
+            field9: '請先勾選作品評審',
+            field10: '報名時所顯示出的表單格式',
+            field11: '報名時可供選擇的內容',
+            field12: '報名時顯示的內容',
+            field13: '個人查看自己成績',
+            field14: '只展示總分',
+            field15: '顯示細項得分',
+            field16: '成績公佈說明',
+            field17: '統計作品公佈方式',
+            field18: '按分數排名公佈',
+            field19: '按等級分佈',
+            field20: '統計公佈排名',
+            field21: '不填則表示公佈所有',
+            field22: '等級排序方式',
+            field23: '依照分數範圍',
+            field24: '依等級數量',
+            field25: '等級分數設定',
+            field26: '各等級的最小值要與上一級的最大值一致,否則會儲存失敗',
+            field27: '統計公佈成績範圍',
+            field28: '評審已開始',
+            field29: '該帳號無效',
+            field30: '該帳號未註冊',
+            field31: '已到成績公布階段,請及時公布成績',
+            field32: '已開始公佈成績',
+            field33: '專家評分為準',
+            field34: '建議分數為準',
+            field35: '已為專家預先分配作品,若無調整,請儲存分配結果,作品才會真正分配給專家',
+            field36: '填報資訊的學段、學科需與專家資訊一致才能符合成功',
+            field37: '活動創建成功後才會正式儲存為模板',
+        },
+        enrollLabel: '報名',
+        enroll: '報名制',
+        invite: '邀請制',
+        disclaimer: '免責聲明',
         timeList: {
-            enroll: '报名时间',
-            upload: '上传时间',
-            review: '评审时间',
-            score: '公示时间',
-        },
-        enrollNum: '报名人数',
-        fillInfo: '填报信息',
-        entryType: '参赛方式',
-        workType: '作品类型',
-        fileDes: '文件说明',
-        uploadType: '上传方式',
-        reviewTitle: '作品评审',
-        reviewRule: '评审规则',
-        scoreTitle: '成绩公示',
+            enroll: '報名時間',
+            upload: '上傳時間',
+            review: '評審時間',
+            score: '公布時間',
+        },
+        enrollNum: '報名數',
+        fillInfo: '填報資訊',
+        entryType: '參賽方式',
+        workType: '作品類型',
+        fileDes: '文件說明',
+        uploadType: '上傳方式',
+        reviewTitle: '作品評審',
+        reviewRule: '評審規則',
+        reviewRuleInfo: {
+            name: '規則名稱',
+            desc: '規則描述',
+            itemize: '分項',
+            subName: '分項名稱',
+            addChild: '新增子項',
+            addItem: '新增分項',
+        },
+        scoreTitle: '成績公布',
         module: {
-            title: '模块',
-            Contest: '赛课活动',
-            Training: '教培活动',
-            Research: '教研活动',
-        },
-        customFields: '自定义填报信息',
-        customTitle: '信息名称',
-        word: '文本',
-        select: '选择器',
+            title: '模',
+            Contest: '賽課活動',
+            Training: '教培活',
+            Research: '教學活動',
+        },
+        customFields: '自訂填報資訊',
+        customTitle: '資訊名稱',
+        word: '文',
+        select: '選擇器',
         ruleInfo: {
-            rule: '规则',
+            rule: '規則',
         },
         joinType: {
-            person: '个人赛',
-            team: '团队赛',
+            person: '個人賽',
+            team: '團隊賽',
         },
         message: {
-            field1: '活动名称不能为空',
-            field2: '活动主题不能为空',
-            field3: '请选择类型',
-            field4: '请选择活动时间',
-            field5: '请选择报名方式',
-            field6: '请选择填报信息',
-            field7: '请选择评审规则',
-            field8: '创建成功',
-            field9: '填报信息未配置学段、学科,无法进行作品分配',
-            field10: '请完善基本信息',
-            field11: '请完善上传信息',
-            field12: '请完善评审信息',
-            field13: '请完善公示信息',
-            field14: '请先选择一个模块',
-            field15: '请完善信息',
-            field16: '请先设置报名时间',
-            field17: '上传结束时间不能早于报名时间',
-            field18: '请先设置报名时间、上传时间',
-            field19: '评审必须在报名、上传结束后进行',
-            field20: '请先设置评审时间',
-            field21: '公示必须在评审结束后进行',
-            field22: '存在相同文件,请重新上传',
-            field23: '该信息已存在',
+            field1: '活動名稱不可為空',
+            field2: '活動主題不可為空',
+            field3: '請選擇類型',
+            field4: '請選擇活動時間',
+            field5: '請選擇報名方式',
+            field6: '請選擇填報資訊',
+            field7: '請選擇評審規則',
+            field8: '新建成功',
+            field9: '填報資料未配置學段、學科,無法進行作品分配',
+            field10: '請完善基本訊息',
+            field11: '請完善上傳訊息',
+            field12: '請完善評審訊息',
+            field13: '請完善公布資訊',
+            field14: '請先選擇一個模組',
+            field15: '請完善資訊',
+            field16: '請先設定報名時間',
+            field17: '上傳結束時間不能早於報名時間',
+            field18: '請先設定報名時間、上傳時間',
+            field19: '評審必須在報名、上傳結束後進行',
+            field20: '請先設定評審時間',
+            field21: '公布必須在評審結束後進行',
+            field22: '存在相同文件,請重新上傳',
+            field23: '該資訊已存在',
+            field24: '請先選擇學校',
+            field25: '等級分數最大值必須大於最小值',
+            field26: '等級分數重疊,請重新設定',
+            field27: '等級分數未完全覆蓋,請新增或修改',
+            field28: '未新增專家,無法分配作品',
+            field29: '評審專家人數少於作品分配次數',
+            field30: '學段匹配的專家數量不足',
+            field31: '學科配對的專家數量不足',
+            field32: '未到評審時間',
+            field33: '學段和學科匹配的專家數量不足',
+            field34: '自動分配完成,檢查無誤後請儲存',
+            field35: '分配失敗',
+            field36: '成功儲存分配結果',
+            field37: '儲存失敗,請重新分配作品',
+            field38: '成功開啟評審',
+            field39: '開啟評審失敗',
+            field40: '暫無報名訊息',
+            field41: '未新增專家',
+            field42: '專家帳號無效',
+            field43: '已分配給該專家',
+            field44: '評審專家不存在',
+            field45: '未變更專家',
+            field46: '分配成功',
+            field47: '作品已評審',
+            field48: '團隊賽不可刪除報名選手',
+            field49: '不在公布時間範圍內',
+            field50: '開始公布失敗',
+            field51: '編輯失敗',
+            field52: '評審已開始,無法修改',
+        },
+        buttonInfo: {
+            text1: '新增評審專家',
+            text2: '自動分配評審作品',
+            text3: '開始評審',
+            text4: '統計成績設定',
+            text5: '公布成績',
+            text6: '確認公布',
+            text7: '僅本次活動使用',
+            text8: '儲存為範本',
         },
         modal: {
-            text1: '删除评审规则',
+            text1: '刪除評審規則',
+            text2: '參與本次活動的學校',
+            text3: '編輯活動基本訊息',
+            text4: '編輯賽課活動資訊',
+            text5: '添加學校',
+            text6: '確定刪除本次活動嗎?',
+            text7: '教師報名資訊',
+            text8: '專家需評審作品',
+            text9: '邀請評審專家',
+            text10: '手動分配作品',
+            text11: '成績等級設定',
+            text12: '調整參數-作品分數',
+            text13: '調整參數-確認開始評審階段後,不可刪除已邀請的專家,不可調整評審規則,同時專家可以對作品進行評審,是否確認開始?',
+            text14: '刪除專家',
+            text15: '確認公布成績後,參賽者將可查看成績排名和個人成績',
+            text16: '是否同時修改範本內容',
+            text17: '若確認修改,會在活動儲存後同步修改範本內容',
+        },
+        website: '分站管理',
+        websiteRoute: '分站路徑',
+        authorized: '授權單位/組織',
+        change: '變更',
+        addBanner: '新增首頁展示',
+        builtBanner: '新建首頁展示',
+        subtitle: '副標題',
+        jumpAddress: '跳轉地址',
+        displayTime: '展示時間',
+        displayImg: '展示圖片',
+        listSelect: '活動清單選擇',
+        publishAct: '發佈公開活動',
+        unconfirmed: '未確認',
+        beConfirmed: '待確認',
+        returnList: '返回活動列表',
+        unrestricted: '未限制學校',
+        unrestricted1: '不限制',
+        uploadWork: '上傳作品',
+        workRequire: '作品要求',
+        signUpload: '報名-上傳',
+        publicity: '公布',
+        duties: '職務',
+        gradeName: '等級名稱',
+        top: '名',
+        signData: '報名數據',
+        registered: '已報名',
+        reviewed: '已評審',
+        inviteTea: '邀請老師',
+        invited: '已邀請',
+        notSign: '未報名',
+        notSignTime: '未到報名時間',
+        allocation: '分配',
+        reviewManage: '評審管理',
+        reviewNum: '評審次數',
+        workAllocation: '作品分配',
+        reviewMethod: {
+            title: '評審方式',
+            text1: '對細項評分',
+            text2: '只打總分',
+        },
+        scoreRule: '統分規則',
+        adjust: '調整',
+        secrecy: '保密',
+        teamName: '團隊名稱',
+        teamCipher: '團隊編碼',
+        assignExpert: '分配專家',
+        notAlloacte: '暫未分配',
+        notUpload: '暫未上傳',
+        works: '作品:',
+        showScore: {
+            title: '分數顯示:',
+            text1: '專家評分為準',
+            text2: '建議分數為準',
+            text3: '建議分數:',
+        },
+        joinStatus: '參賽狀態',
+        tableColumn: {
+            reviewSubject: '評審科目',
+            reviewNum: '評審數量',
+            reviewPro: '評審進度',
+            nameGroup: '姓名/組名',
+            uploadName: '作品名稱',
+            expertScore: '專家評分',
+            maskScore: '建議分數',
+            showScore: '分數展示',
+            scoreLevel: '預設獎項',
+        },
+        expertExcel: {
+            importTem: '導入專家範本',
+            name: 'name: 必填,專家名稱',
+            title: 'title: 選填,專家職稱',
+            mobile: 'mobile: 選填,專家手機號碼(mobile,email,tmdid至少填寫一個)',
+            email: 'email: 選填,專家信箱號碼(mobile,email,tmdid至少填寫一個)',
+            tmdid: 'tmdid: 選填,專家醍摩豆ID(mobile,email,tmdid至少填寫一個)',
+            modules: 'modules: 必填,專家需評審的模組(Contest表示賽課活動)',
+            subjects: 'subjects: 必填,專家評審科目(採用「學段-科目」格式,多科目以逗號隔開)',
+            perSub: '下載填報資訊時預設的學段-學科對照表',
+            review: '評審模組',
+            subject: '科目',
+            excelName: '學段-學科對照表',
         },
     },
-
-
-
 }

BIN
TEAMModelOS/ClientApp/public/login_bg.jpg


+ 16 - 13
TEAMModelOS/ClientApp/src/api/common.js

@@ -3,64 +3,67 @@ export default {
     /*
      *查询名单列表
      */
-     getGroupList: function (data) {
+    getGroupList: function (data) {
         return post('/grouplist/get-grouplists', data)
     },
     /*
      *查询名单详细信息
      */
-     getGroupInfo: function (data) {
+    getGroupInfo: function (data) {
         return post('/grouplist/get-grouplist-idcode', data)
     },
     /*
      *保存更新名单信息
      */
-     upsertGroupInfo: function (data) {
+    upsertGroupInfo: function (data) {
         return post('/grouplist/upsert-grouplist', data)
     },
     /*
      *根据条件查询名单列表和成员信息
      */
-     getGroupListInfo: function (data) {
+    getGroupListInfo: function (data) {
         return post('/grouplist/get-grouplists-members', data)
     },
     /*
      *根据名单id查询名单详细信息
      */
-     getGroupListByIds: function (data) {
+    getGroupListByIds: function (data) {
         return post('/grouplist/get-members-listids', data)
     },
+    getGroupNosByIds: function (data) {
+        return post('/grouplist/get-grouplist-nos', data)
+    },
     /*
      *查询活动发布对象
      */
-     getActivityTarget: function (data) {
+    getActivityTarget: function (data) {
         return post('/grouplist/get-activity-grouplist', data)
     },
     /*
      *删除名单(单纯的名单操作,主要用于教学班、教研组管理)
      */
-     deleteGroupList: function (data) {
+    deleteGroupList: function (data) {
         return post('/grouplist/delete-grouplist', data)
     },
     /*
      *删除个人名单以及课程关联关系(用于个名单和课程的关联关系的管理)
      */
-     deleteStulistRelCus: function (data) {
+    deleteStulistRelCus: function (data) {
         return post('/school/course/del-stulist-rmv-rel-course', data)
     },
     /*
      *查询行政班名单学生完整信息
      */
-     getClassStudent: function (data) {
+    getClassStudent: function (data) {
         return post('/grouplist/get-classstudents-idcode', data)
     },
-	/* 查询组长信息 */
-	getGroupLeaderTags: function (data) {
+    /* 查询组长信息 */
+    getGroupLeaderTags: function (data) {
         return post('/grouplist/get-grouplist-tags', data)
     },
     /* 生成随机码 */
-	getServerRandom: function (data) {
+    getServerRandom: function (data) {
         return post('core/random-code', data)
     },
-    
+
 }

+ 2 - 0
TEAMModelOS/ClientApp/src/api/http.js

@@ -5,6 +5,7 @@ import config from '@/store/module/config'
 import { app } from '@/boot-app.js'
 // 不需要携带access_token
 const NO_ACCESS_API = [
+    '/grouplist/get-grouplist-nos',
     '/core/system-info',
     '/oauth2/login',
     '/oauth2/token',
@@ -34,6 +35,7 @@ const NO_ACCESS_API = [
 ]
 // 需要携带access_token 不需要携带auth-token
 const NO_AUTH_API = [
+    '/grouplist/get-grouplist-nos',
     '/teacher/init/get-teacher-info',
     '/teacher/init/get-school-info',
     '/tmduser/init/get-tmduser-info',

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

@@ -54,6 +54,30 @@
       <div class="content unicode" style="display: block;">
           <ul class="icon_lists dib-box">
           
+            <li class="dib">
+              <span class="icon iconfont">&#xe832;</span>
+                <div class="name">录音</div>
+                <div class="code-name">&amp;#xe832;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe6b3;</span>
+                <div class="name">刷新</div>
+                <div class="code-name">&amp;#xe6b3;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe6b9;</span>
+                <div class="name">开始</div>
+                <div class="code-name">&amp;#xe6b9;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7dd;</span>
+                <div class="name">录音-暂停</div>
+                <div class="code-name">&amp;#xe7dd;</div>
+              </li>
+          
             <li class="dib">
               <span class="icon iconfont">&#xe75a;</span>
                 <div class="name">点击</div>
@@ -1566,9 +1590,9 @@
 <pre><code class="language-css"
 >@font-face {
   font-family: 'iconfont';
-  src: url('iconfont.woff2?t=1702624481449') format('woff2'),
-       url('iconfont.woff?t=1702624481449') format('woff'),
-       url('iconfont.ttf?t=1702624481449') format('truetype');
+  src: url('iconfont.woff2?t=1711100580448') format('woff2'),
+       url('iconfont.woff?t=1711100580448') format('woff'),
+       url('iconfont.ttf?t=1711100580448') format('truetype');
 }
 </code></pre>
           <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -1594,6 +1618,42 @@
       <div class="content font-class">
         <ul class="icon_lists dib-box">
           
+          <li class="dib">
+            <span class="icon iconfont icon-luyin"></span>
+            <div class="name">
+              录音
+            </div>
+            <div class="code-name">.icon-luyin
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-shuaxin1"></span>
+            <div class="name">
+              刷新
+            </div>
+            <div class="code-name">.icon-shuaxin1
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-start"></span>
+            <div class="name">
+              开始
+            </div>
+            <div class="code-name">.icon-start
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-luyin-zanting"></span>
+            <div class="name">
+              录音-暂停
+            </div>
+            <div class="code-name">.icon-luyin-zanting
+            </div>
+          </li>
+          
           <li class="dib">
             <span class="icon iconfont icon-click"></span>
             <div class="name">
@@ -3862,6 +3922,38 @@
       <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-luyin"></use>
+                </svg>
+                <div class="name">录音</div>
+                <div class="code-name">#icon-luyin</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-shuaxin1"></use>
+                </svg>
+                <div class="name">刷新</div>
+                <div class="code-name">#icon-shuaxin1</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-start"></use>
+                </svg>
+                <div class="name">开始</div>
+                <div class="code-name">#icon-start</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-luyin-zanting"></use>
+                </svg>
+                <div class="name">录音-暂停</div>
+                <div class="code-name">#icon-luyin-zanting</div>
+            </li>
+          
             <li class="dib">
                 <svg class="icon svg-icon" aria-hidden="true">
                   <use xlink:href="#icon-click"></use>

+ 19 - 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=1702624481449') format('woff2'),
-       url('iconfont.woff?t=1702624481449') format('woff'),
-       url('iconfont.ttf?t=1702624481449') format('truetype');
+  src: url('iconfont.woff2?t=1711100580448') format('woff2'),
+       url('iconfont.woff?t=1711100580448') format('woff'),
+       url('iconfont.ttf?t=1711100580448') format('truetype');
 }
 
 .iconfont {
@@ -13,6 +13,22 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.icon-luyin:before {
+  content: "\e832";
+}
+
+.icon-shuaxin1:before {
+  content: "\e6b3";
+}
+
+.icon-start:before {
+  content: "\e6b9";
+}
+
+.icon-luyin-zanting:before {
+  content: "\e7dd";
+}
+
 .icon-click:before {
   content: "\e75a";
 }

Plik diff jest za duży
+ 1 - 1
TEAMModelOS/ClientApp/src/assets/iconfont/iconfont.js


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

@@ -5,6 +5,34 @@
   "css_prefix_text": "icon-",
   "description": "",
   "glyphs": [
+    {
+      "icon_id": "6303286",
+      "name": "录音",
+      "font_class": "luyin",
+      "unicode": "e832",
+      "unicode_decimal": 59442
+    },
+    {
+      "icon_id": "13041211",
+      "name": "刷新",
+      "font_class": "shuaxin1",
+      "unicode": "e6b3",
+      "unicode_decimal": 59059
+    },
+    {
+      "icon_id": "6838115",
+      "name": "开始",
+      "font_class": "start",
+      "unicode": "e6b9",
+      "unicode_decimal": 59065
+    },
+    {
+      "icon_id": "34573507",
+      "name": "录音-暂停",
+      "font_class": "luyin-zanting",
+      "unicode": "e7dd",
+      "unicode_decimal": 59357
+    },
     {
       "icon_id": "2205528",
       "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


+ 1 - 1
TEAMModelOS/ClientApp/src/common/BaseClassSelectPri.vue

@@ -125,7 +125,7 @@ export default {
         res => {
           let list = res.groupLists.map(item => {
               item.groups = item.groups.filter(groups => {
-                  return groups.teachType === 'teacher'
+                  return groups.teachType === 'teacher' || groups.teachType === 'assistant'
               })
               return item
           })

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

@@ -363,6 +363,33 @@
 				let idToken = localStorage.getItem("id_token");
 				return this.$api.login.getCode(idToken);
 			},
+			// 教师个人跳转赛课活动
+			async toPrivActivity() {
+				let areaId = sessionStorage.getItem("areaId") ? sessionStorage.getItem("areaId") : ''
+				let areaRoute = ''
+				if(areaId) {
+					areaRoute = await this.getAreaRoute(areaId)
+				}
+				let url = `${window.location.host === 'localhost:5001' ? 'http://localhost:8081' : 'https://contest-test.teammodel.cn'}/${areaRoute || 'teammodel'}/home/homePage?token=${localStorage.getItem("auth_token")}`
+				window.open(url, "", "noopener");
+			},
+			getAreaRoute(areaId) {
+				return new Promise((r, j) => {
+					let params = {
+						grant_type: 'list',
+						websiteId: areaId
+					}
+					this.$api.areaActivity.websiteManage(params).then(res => {
+						if(res.code === 200) {
+							r(res.websites[0]?.route)
+						} else {
+							j('')
+						}
+					}, err => {
+						j('')
+					})
+				})
+			},
 			//教师个人跳转苏格拉底
 			toPrivSokrate() {
 				this.getLoginCode()
@@ -394,8 +421,8 @@
 						this.toPrivSokrate();
 					} else if (menu.to === "schoolSokrate") {
 						this.toSchoolSokrate();
-					} else if (menu.to === "activity") {
-
+					} else if (menu.to === "privActivity") {
+						this.toPrivActivity()
 					}
 				}
 			},
@@ -1084,7 +1111,7 @@
 											role: "admin",
 											permission: "dashboard-read",
 											menuName: "artDashboard",
-											isShow: true
+											isShow: !this.isQingYangArea
 										},
 										// 校园风采(针对青羊区放出)
 										// {
@@ -1557,6 +1584,20 @@
                 isShow: true
             }, */
 								]
+							},
+							// 活动报名
+							{
+								icon: "iconfont icon-yishu",
+								name: "活动平台",
+								router: "#",
+								tag: "",
+								role: "admin|teacher",
+								permission: "",
+								child: [],
+								menuName: "privActivity",
+								// menuName: 'myCourse',
+								isShow: this.isShowActi,
+								to: "privActivity"
 							}
 					  ];
 				return data;

+ 2 - 1
TEAMModelOS/ClientApp/src/common/BaseSelectSchool.vue

@@ -333,7 +333,8 @@
 			/* 判断过去、现在和未来 */
 			getYearStatus() {
 				return (year) => {
-					return year === this.curYear ? 1 : year < this.curYear ? 0 : 2;
+					let curStudyYear = localStorage.curSemester ? JSON.parse(localStorage.curSemester).year : this.curYear
+					return year === curStudyYear ? 1 : year < curStudyYear ? 0 : 2;
 				};
 			},
 			/* 判断是否当前学期 */

+ 2 - 1
TEAMModelOS/ClientApp/src/common/PrivateTargetSingle.vue

@@ -112,9 +112,10 @@ export default {
             this.$api.common.getActivityTarget(params).then(
                 res => {
                     // 由于空间问题,先不让协同教师发布评测
+                    // 2024.3.22 放开协同教师发布评测
                     this.allList = res.groupLists.map(item => {
                         item.groups = item.groups.filter(groups => {
-                            return groups.teachType === 'teacher'
+                            return groups.teachType === 'teacher' || groups.teachType === 'assistant'
                         })
                         return item
                     })

+ 1 - 1
TEAMModelOS/ClientApp/src/components/dashboard/art/BasePointLineBar.vue

@@ -116,7 +116,7 @@ export default {
                 barBorderRadius: 11,
               }
             },
-            data: data.map(i => i.val),
+            data: data.map(i => Math.min(100,i.val)),
           }]
       };
       myChart.setOption(this.option)

+ 222 - 69
TEAMModelOS/ClientApp/src/components/dashboard/art/LeftBottom.vue

@@ -1,77 +1,230 @@
 <template>
-  <div id="bottomLeft">
-    <p class="dashboard-block-title">
-      <Icon type="md-pulse" />
-      <span>学生落点分析图</span>
-      <dv-decoration-1 class="dv-dec-3" />
-    </p>
-    <div class="bg-color-black">
-      <BaseArtScatter></BaseArtScatter>
-      <!-- <dv-scroll-board :config="config" style="width:96%;margin-left:2%" /> -->
-    </div>
-  </div>
+	<div id="bottomLeft">
+		<p class="dashboard-block-title">
+			<Icon type="md-pulse" />
+			<span>知识点得分率明细</span>
+			<dv-decoration-1 class="dv-dec-3" />
+		</p>
+		<div class="select-wrap">
+			<Cascader v-model="cascaderVal" :data="cascaderConfig" change-on-select @on-change="onSelect" :clearable="false"></Cascader>
+		</div>
+		<div class="bg-color-black">
+			<!-- <BaseArtScatter></BaseArtScatter> -->
+			<!-- <dv-scroll-board :config="config" style="width:96%;margin-left:2%" /> -->
+			<div id="knoBar" style="height: 300px"></div>
+		</div>
+	</div>
 </template>
 
 <script>
-import BaseArtScatter from '@/components/dashboard/art/BaseArtScatter.vue'
-export default {
-  components: {
-    BaseArtScatter
-  },
-  data() {
-    return {
-      config: null
-    }
-  },
-  watch: {
-    '$store.state.dashboard.artDashboard': {
-      deep: true,
-      immediate: true,
-      handler(n, o) {
-        if (n) {
-          this.$nextTick(() => {
-            console.error(n.paperArr)
-            this.config = {
-              header: ["名称", "满分", "试题总数", "使用人数", "有效题", "无效题", "难度"],
-              data: n.paperArr,
-              rowNum: 4, //表格行数
-              headerHeight: 35,
-              headerBGC: "#0f1325", //表头
-              oddRowBGC: "#0f1325", //奇数行
-              evenRowBGC: "#171c33", //偶数行
-              columnWidth: [250],
-            }
-          })
-        }
-      }
-    }
-  }
-}
+	import BaseArtScatter from "@/components/dashboard/art/BaseArtScatter.vue";
+	export default {
+		components: {
+			BaseArtScatter
+		},
+		data() {
+			return {
+				cascaderConfig: [],
+				cascaderVal: [],
+				config: null,
+				option: null,
+				artAnalysisJson: null
+			};
+		},
+		methods: {
+			onSelect(val) {
+				console.log(val);
+				if (val.length === 1) {
+					this.doRender(this.artAnalysisJson.cInfo.find((i) => i.id === val[0]).kno);
+				} else {
+					this.doRender(this.artAnalysisJson.students.find((i) => i.id === val[1]).kno);
+				}
+			},
+			doRender(data) {
+				let myChart = this.$echarts.init(document.getElementById("knoBar"));
+				this.option = {
+					title: {
+						textStyle: {
+							align: "center",
+							color: "#fff",
+							fontSize: 20
+						},
+						top: "5%",
+						left: "center"
+					},
+					tooltip: {
+						trigger: "axis",
+						axisPointer: {
+							type: "shadow"
+						},
+						formatter: function (params) {
+							return params[0].name + "<br>得分率:" + params[0].value + "%";
+						}
+					},
+					grid: {
+						left: "2%",
+						right: "4%",
+						bottom: "8%",
+						top: "10%",
+						containLabel: true
+					},
+					xAxis: {
+						type: "category",
+						data: data.map((i) => i.name),
+						axisLine: {
+							lineStyle: {
+								color: "#eee"
+							}
+						},
+						axisLabel: {
+							interval: 0,
+							margin: 20,
+							color: "#05D5FF",
+							textStyle: {
+								fontSize: 11
+							}
+						}
+					},
+					yAxis: [
+						{
+							type: "value",
+							nameTextStyle: {
+								color: "#A2A5AA",
+								fontSize: 10
+							},
+							splitLine: {
+								show: true,
+								lineStyle: {
+									color: "rgba(255,255,255,0.3)"
+								}
+							},
+							axisLine: {
+								lineStyle: {
+									color: "#6C6F79" //改轴颜色
+								}
+							},
+							axisLabel: {}
+						}
+					],
+					dataZoom: [
+						{
+							show: true,
+							height: 10,
+							xAxisIndex: [0],
+							bottom: 10,
+							start: 0,
+							end: 60,
+							handleIcon: "path://M306.1,413c0,2.2-1.8,4-4,4h-59.8c-2.2,0-4-1.8-4-4V200.8c0-2.2,1.8-4,4-4h59.8c2.2,0,4,1.8,4,4V413z",
+							handleSize: "110%",
+							handleStyle: {
+								color: "#5B3AAE"
+							},
+							textStyle: {
+								color: "rgba(204,187,225,0.5)"
+							},
+							fillerColor: "rgba(67,55,160,0.4)",
+							borderColor: "rgba(204,187,225,0.5)"
+						},
+						{
+							type: "inside",
+							show: true,
+							height: 15,
+							start: 1,
+							end: 35
+						}
+					],
+					series: [
+						{
+							type: "bar",
+							barWidth: "20",
+							itemStyle: {
+								normal: {
+									color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+										{
+											offset: 0,
+											color: "#70cf7e"
+										},
+										{
+											offset: 1,
+											color: "#1cc0ad"
+										}
+									]),
+									barBorderRadius: 11
+								}
+							},
+							data: data.map((i) => ((i.score || i.persent || 0) * 100).toFixed(2))
+						}
+					]
+				};
+				myChart.setOption(this.option);
+				window.addEventListener("resize", function () {
+					myChart.resize();
+				});
+			}
+		},
+		watch: {
+			"$store.state.artDashboard.artAnalysisJson": {
+				deep: true,
+				immediate: true,
+				handler(n, o) {
+					if (n) {
+						this.$nextTick(() => {
+							this.artAnalysisJson = n;
+							n.cInfo.forEach((classItem) => {
+								this.cascaderConfig.push({
+									value: classItem.id,
+									label: classItem.name,
+									children: n.students
+										.filter((i) => i.classId === classItem.id)
+										.map((k) => {
+											return {
+												value: k.id,
+												label: k.name,
+												children: []
+											};
+										})
+								});
+							});
+							this.doRender(n.kno);
+						});
+					}
+				}
+			}
+		}
+	};
 </script>
 
 <style lang="less" scoped>
-@box-height: 100%;
-@box-width: 100%;
-#bottomLeft {
-  padding: 20px 16px;
-  height: @box-height;
-  width: @box-width;
-  display: flex;
-  flex-direction: column;
-  .bg-color-black {
-    height: 91%;
-    padding-top: 10px;
-  }
-  .text {
-    color: #c3cbde;
-  }
-  .chart-box {
-    margin-top: 16px;
-    width: 170px;
-    height: 170px;
-    .active-ring-name {
-      padding-top: 10px;
-    }
-  }
-}
+	@box-height: 100%;
+	@box-width: 100%;
+	#bottomLeft {
+		padding: 20px 16px;
+		height: @box-height;
+		width: @box-width;
+		display: flex;
+		flex-direction: column;
+		.select-wrap {
+			position: absolute;
+			top: 30px;
+			right: 30px;
+			width: 200px;
+			height: 100%;
+			z-index: 1;
+		}
+		.bg-color-black {
+			height: 91%;
+			padding-top: 10px;
+		}
+		.text {
+			color: #c3cbde;
+		}
+		.chart-box {
+			margin-top: 16px;
+			width: 170px;
+			height: 170px;
+			.active-ring-name {
+				padding-top: 10px;
+			}
+		}
+	}
 </style>

+ 2 - 0
TEAMModelOS/ClientApp/src/components/dashboard/art/RightTop.vue

@@ -333,6 +333,8 @@ export default {
             })
 
             this.cascaderConfig = cascaderConfig
+            this.cascaderVal = ['all', cascaderConfig[0].children[0].value]
+            this.onSelect(['all', cascaderConfig[0].children[0].value])
           })
         }
       }

Plik diff jest za duży
+ 662 - 690
TEAMModelOS/ClientApp/src/components/evaluation/ExerciseList.vue


+ 455 - 192
TEAMModelOS/ClientApp/src/components/selflearn/ExerciseList.vue

@@ -3,69 +3,212 @@
         <!-- 筛选部分 -->
         <div class="filter-wrap">
             <div class="filter-item">
-                <span class="filter-title">{{$t('evaluation.filter.origin')}}:</span>
-                <RadioGroup v-model="filterOrigin" type="button" @on-change="filterOriginChange">
-                    <Radio :label="userId">{{$t('evaluation.filter.privateBank')}}</Radio>
-                    <Radio :label="schoolCode" v-if="hasSchool">{{$t('evaluation.filter.schoolBank')}}</Radio>
+                <span class="filter-title"
+                    >{{ $t("evaluation.filter.origin") }}:</span
+                >
+                <RadioGroup
+                    v-model="filterOrigin"
+                    type="button"
+                    @on-change="filterOriginChange"
+                >
+                    <Radio :label="userId">{{
+                        $t("evaluation.filter.privateBank")
+                    }}</Radio>
+                    <Radio :label="schoolCode" v-if="hasSchool">{{
+                        $t("evaluation.filter.schoolBank")
+                    }}</Radio>
                 </RadioGroup>
             </div>
             <div class="filter-item" v-show="filterOrigin == schoolCode">
-                <span class="filter-title">{{$t('evaluation.filter.period')}}:</span>
-                <RadioGroup v-model="filterPeriod" type="button" @on-change="filterPeriodChange">
-                    <Radio v-for="(item, index) in periodList" :key="index" :label="index">{{ item.name }}</Radio>
+                <span class="filter-title"
+                    >{{ $t("evaluation.filter.period") }}:</span
+                >
+                <RadioGroup
+                    v-model="filterPeriod"
+                    type="button"
+                    @on-change="filterPeriodChange"
+                >
+                    <Radio
+                        v-for="(item, index) in periodList"
+                        :key="index"
+                        :label="index"
+                        >{{ item.name }}</Radio
+                    >
                 </RadioGroup>
             </div>
             <div class="filter-item" v-show="filterOrigin == schoolCode">
-                <span class="filter-title">{{$t('evaluation.filter.grade')}}:</span>
-                <CheckboxGroup v-model="filterGrade" border @on-change="filterGradeChange">
-                    <Checkbox lable="all">{{$t('evaluation.filter.all')}}</Checkbox>
-                    <Checkbox v-for="(item, index) in gradeList" :key="index" :label="index">{{ item }}</Checkbox>
+                <span class="filter-title"
+                    >{{ $t("evaluation.filter.grade") }}:</span
+                >
+                <CheckboxGroup
+                    v-model="filterGrade"
+                    border
+                    @on-change="filterGradeChange"
+                >
+                    <Checkbox lable="all">{{
+                        $t("evaluation.filter.all")
+                    }}</Checkbox>
+                    <Checkbox
+                        v-for="(item, index) in gradeList"
+                        :key="index"
+                        :label="index"
+                        >{{ item }}</Checkbox
+                    >
                 </CheckboxGroup>
             </div>
             <div class="filter-item" v-show="filterOrigin == schoolCode">
-                <span class="filter-title">{{$t('evaluation.filter.subject')}}:</span>
-                <CheckboxGroup v-model="filterSubject" border @on-change="filterSubjectChange">
-                    <Checkbox lable="all">{{$t('evaluation.filter.all')}}</Checkbox>
-                    <Checkbox v-for="(item, index) in subjectList" :key="index" :label="item.id">{{ item.name }}</Checkbox>
+                <span class="filter-title"
+                    >{{ $t("evaluation.filter.subject") }}:</span
+                >
+                <CheckboxGroup
+                    v-model="filterSubject"
+                    border
+                    @on-change="filterSubjectChange"
+                >
+                    <Checkbox lable="all">{{
+                        $t("evaluation.filter.all")
+                    }}</Checkbox>
+                    <Checkbox
+                        v-for="(item, index) in subjectList"
+                        :key="index"
+                        :label="item.id"
+                        >{{ item.name }}</Checkbox
+                    >
                 </CheckboxGroup>
             </div>
             <div class="filter-item">
-                <span class="filter-title">{{$t('evaluation.filter.type')}}:</span>
-				<CheckboxGroup v-model="filterType" border @on-change="filterTypeChange">
-					<Checkbox label="all">{{$t('evaluation.filter.all')}}</Checkbox>
-					<Checkbox label="single">{{$t('evaluation.single')}}<span class="filter-count" v-if="filterOrigin === schoolCode"></span></Checkbox>
-					<Checkbox label="multiple">{{$t('evaluation.multiple')}}<span class="filter-count" v-if="filterOrigin === schoolCode"></span></Checkbox>
-					<Checkbox label="judge">{{$t('evaluation.judge')}}<span class="filter-count" v-if="filterOrigin === schoolCode"></span></Checkbox>
-					<Checkbox label="complete">{{$t('evaluation.complete')}}<span class="filter-count" v-if="filterOrigin === schoolCode"></span></Checkbox>
-					<Checkbox label="subjective">{{$t('evaluation.subjective')}}<span class="filter-count" v-if="filterOrigin === schoolCode"></span></Checkbox>
-					<Checkbox label="connector">{{$t('evaluation.connector')}}<span class="filter-count" v-if="filterOrigin === schoolCode"></span></Checkbox>
-					<Checkbox label="correct">{{$t('evaluation.correct')}}<span class="filter-count" v-if="filterOrigin === schoolCode"></span></Checkbox>
-					<Checkbox label="compose">{{$t('evaluation.compose')}}<span class="filter-count" v-if="filterOrigin === schoolCode"></span></Checkbox>
-				</CheckboxGroup>
+                <span class="filter-title"
+                    >{{ $t("evaluation.filter.type") }}:</span
+                >
+                <CheckboxGroup
+                    v-model="filterType"
+                    border
+                    @on-change="filterTypeChange"
+                >
+                    <Checkbox label="all">{{
+                        $t("evaluation.filter.all")
+                    }}</Checkbox>
+                    <Checkbox label="single"
+                        >{{ $t("evaluation.single")
+                        }}<span
+                            class="filter-count"
+                            v-if="filterOrigin === schoolCode"
+                        ></span
+                    ></Checkbox>
+                    <Checkbox label="multiple"
+                        >{{ $t("evaluation.multiple")
+                        }}<span
+                            class="filter-count"
+                            v-if="filterOrigin === schoolCode"
+                        ></span
+                    ></Checkbox>
+                    <Checkbox label="judge"
+                        >{{ $t("evaluation.judge")
+                        }}<span
+                            class="filter-count"
+                            v-if="filterOrigin === schoolCode"
+                        ></span
+                    ></Checkbox>
+                    <Checkbox label="complete"
+                        >{{ $t("evaluation.complete")
+                        }}<span
+                            class="filter-count"
+                            v-if="filterOrigin === schoolCode"
+                        ></span
+                    ></Checkbox>
+                    <Checkbox label="subjective"
+                        >{{ $t("evaluation.subjective")
+                        }}<span
+                            class="filter-count"
+                            v-if="filterOrigin === schoolCode"
+                        ></span
+                    ></Checkbox>
+                    <Checkbox label="connector"
+                        >{{ $t("evaluation.connector")
+                        }}<span
+                            class="filter-count"
+                            v-if="filterOrigin === schoolCode"
+                        ></span
+                    ></Checkbox>
+                    <Checkbox label="correct"
+                        >{{ $t("evaluation.correct")
+                        }}<span
+                            class="filter-count"
+                            v-if="filterOrigin === schoolCode"
+                        ></span
+                    ></Checkbox>
+                    <Checkbox label="compose"
+                        >{{ $t("evaluation.compose")
+                        }}<span
+                            class="filter-count"
+                            v-if="filterOrigin === schoolCode"
+                        ></span
+                    ></Checkbox>
+                </CheckboxGroup>
             </div>
             <div class="filter-item">
-                <span class="filter-title">{{$t('evaluation.filter.diff')}}:</span>
-				<CheckboxGroup v-model="filterDiff" border @on-change="filterDiffChange">
-					<Checkbox label="all">{{$t('evaluation.filter.all')}}</Checkbox>
-                    <Checkbox v-for="(item, index) in exersicesDiff" :key="index" :label="index + 1">{{ item }}</Checkbox>
+                <span class="filter-title"
+                    >{{ $t("evaluation.filter.diff") }}:</span
+                >
+                <CheckboxGroup
+                    v-model="filterDiff"
+                    border
+                    @on-change="filterDiffChange"
+                >
+                    <Checkbox label="all">{{
+                        $t("evaluation.filter.all")
+                    }}</Checkbox>
+                    <Checkbox
+                        v-for="(item, index) in exersicesDiff"
+                        :key="index"
+                        :label="index + 1"
+                        >{{ item }}</Checkbox
+                    >
                 </CheckboxGroup>
             </div>
             <div class="filter-item">
-                <span class="filter-title">{{$t('evaluation.filter.level')}}:</span>
-                <CheckboxGroup v-model="filterField" border @on-change="filterFieldChange">
-                    <Checkbox label="all">{{$t('evaluation.filter.all')}}</Checkbox>
-                    <Checkbox :label="1">{{$t('evaluation.level1')}}</Checkbox>
-                    <Checkbox :label="2">{{$t('evaluation.level2')}}</Checkbox>
-                    <Checkbox :label="3">{{$t('evaluation.level3')}}</Checkbox>
-                    <Checkbox :label="4">{{$t('evaluation.level4')}}</Checkbox>
-                    <Checkbox :label="5">{{$t('evaluation.level5')}}</Checkbox>
-                    <Checkbox :label="6">{{$t('evaluation.level6')}}</Checkbox>
+                <span class="filter-title"
+                    >{{ $t("evaluation.filter.level") }}:</span
+                >
+                <CheckboxGroup
+                    v-model="filterField"
+                    border
+                    @on-change="filterFieldChange"
+                >
+                    <Checkbox label="all">{{
+                        $t("evaluation.filter.all")
+                    }}</Checkbox>
+                    <Checkbox :label="1">{{
+                        $t("evaluation.level1")
+                    }}</Checkbox>
+                    <Checkbox :label="2">{{
+                        $t("evaluation.level2")
+                    }}</Checkbox>
+                    <Checkbox :label="3">{{
+                        $t("evaluation.level3")
+                    }}</Checkbox>
+                    <Checkbox :label="4">{{
+                        $t("evaluation.level4")
+                    }}</Checkbox>
+                    <Checkbox :label="5">{{
+                        $t("evaluation.level5")
+                    }}</Checkbox>
+                    <Checkbox :label="6">{{
+                        $t("evaluation.level6")
+                    }}</Checkbox>
                 </CheckboxGroup>
             </div>
             <div class="filter-item">
-                <span class="filter-title">{{$t('evaluation.filter.sort')}}:</span>
-                <RadioGroup v-model="filterSort" type="button" @on-change="filterSortChange">
-                    <Radio label="createTime">{{$t('evaluation.filter.createTime')}}
+                <span class="filter-title"
+                    >{{ $t("evaluation.filter.sort") }}:</span
+                >
+                <RadioGroup
+                    v-model="filterSort"
+                    type="button"
+                    @on-change="filterSortChange"
+                >
+                    <Radio label="createTime"
+                        >{{ $t("evaluation.filter.createTime") }}
                         <Icon type="md-arrow-round-down" />
                     </Radio>
                     <!-- <Radio label="useCount">
@@ -80,67 +223,148 @@
         <Loading :top="100" v-show="dataLoading" hideMask></Loading>
         <div v-if="exerciseList.length === 0" class="no-data-text">
             <img src="@/assets/icon/no_data_evaluation.png" width="120" />
-            <span style="margin-top: 15px; color: #808080">{{$t('evaluation.noData')}}</span>
+            <span style="margin-top: 15px; color: #808080">{{
+                $t("evaluation.noData")
+            }}</span>
         </div>
         <div class="content-wrap" ref="mathJaxContainer" v-else>
-            <div class="exercise-item" v-for="(item, index) of exerciseList" :key="index" @click="onQuestionToggle(index, item.id, $event)">
+            <div
+                class="exercise-item"
+                v-for="(item, index) of exerciseList"
+                :key="index"
+                @click="onQuestionToggle(index, item.id, $event)"
+            >
                 <!-- 题干部分 -->
                 <div class="item-question">
                     <div>
                         <div class="item-question-order">
                             {{ pageSize * (pageNum - 1) + index + 1 }} :
                         </div>
-                        <div class="item-question-text" v-html="item.question" @click="onRichTextClick($event)"></div>
+                        <div
+                            class="item-question-text"
+                            v-html="item.question"
+                            @click="onRichTextClick($event)"
+                        ></div>
                     </div>
                     <span class="item-btn-toggle">
-                        <Icon :type="
-                              collapseList.indexOf(index)>
-                            -1
-                            ? 'ios-arrow-dropup'
-                            : 'ios-arrow-dropdown'
-                            " />
+                        <Icon
+                            :type="
+                                collapseList.indexOf(index) > -1
+                                    ? 'ios-arrow-dropup'
+                                    : 'ios-arrow-dropdown'
+                            "
+                        />
                     </span>
                 </div>
                 <!-- 选项部分 -->
-                <div v-for="(option, optionIndex) in item.option" :key="optionIndex" class="item-options">
+                <div
+                    v-for="(option, optionIndex) in item.option"
+                    :key="optionIndex"
+                    class="item-options"
+                >
                     <div class="item-option-content">
                         <div class="item-option-order">
-                            {{ String.fromCharCode(64 + parseInt(optionIndex + 1)) }} :
+                            {{
+                                String.fromCharCode(
+                                    64 + parseInt(optionIndex + 1)
+                                )
+                            }}
+                            :
                         </div>
-                        <div class="item-option-text" v-html="option.value" @click="onRichTextClick($event)"></div>
+                        <div
+                            class="item-option-text"
+                            v-html="option.value"
+                            @click="onRichTextClick($event)"
+                        ></div>
                     </div>
                 </div>
                 <transition name="slide">
-                    <div v-show="collapseList.indexOf(exerciseList.indexOf(item)) > -1" class="toggle-area">
+                    <div
+                        v-show="
+                            collapseList.indexOf(exerciseList.indexOf(item)) >
+                            -1
+                        "
+                        class="toggle-area"
+                    >
                         <div v-if="item.type !== 'compose'">
                             <!-- 答案展示部分 -->
                             <div class="item-explain">
-                                <span class="explain-title">【{{$t('evaluation.answer')}}】</span>
+                                <span class="explain-title"
+                                    >【{{ $t("evaluation.answer") }}】</span
+                                >
                                 <div class="item-explain-details">
                                     <!-- 问答题答案 -->
-                                    <div v-if="item.type === 'subjective' || item.type === 'complete'">
-                                        <span v-for="(answer, index) in item.answer" :key="index" v-html="item.answer.length ? answer : '未设置答案'"></span>
+                                    <div
+                                        v-if="
+                                            item.type === 'subjective' ||
+                                            item.type === 'complete'
+                                        "
+                                    >
+                                        <span
+                                            v-for="(
+                                                answer, index
+                                            ) in item.answer"
+                                            :key="index"
+                                            v-html="
+                                                item.answer.length
+                                                    ? answer
+                                                    : '未设置答案'
+                                            "
+                                        ></span>
                                     </div>
                                     <!-- 其余题型答案 -->
                                     <div v-else>
-                                        <span :class="[item.type === 'complete' ? 'item-answer-item' : '',]" v-for="(answer, index) in item.answer" :key="index">{{ answer }}</span>
+                                        <span
+                                            :class="[
+                                                item.type === 'complete'
+                                                    ? 'item-answer-item'
+                                                    : '',
+                                            ]"
+                                            v-for="(
+                                                answer, index
+                                            ) in item.answer"
+                                            :key="index"
+                                            >{{ answer }}</span
+                                        >
                                     </div>
                                 </div>
                             </div>
                             <!-- 解析部分 -->
                             <div class="item-explain">
-                                <span class="explain-title">【{{$t('evaluation.explain')}}】</span>
-                                <div class="item-explain-details" @click="onRichTextClick($event)">
-                                    <span v-html="item.explain || $t('evaluation.noExplain')"></span>
+                                <span class="explain-title"
+                                    >【{{ $t("evaluation.explain") }}】</span
+                                >
+                                <div
+                                    class="item-explain-details"
+                                    @click="onRichTextClick($event)"
+                                >
+                                    <span
+                                        v-html="
+                                            item.explain ||
+                                            $t('evaluation.noExplain')
+                                        "
+                                    ></span>
                                 </div>
                             </div>
                             <!-- 知识点部分 -->
                             <div class="item-explain">
-                                <span class="explain-title">【{{$t('evaluation.knowledgePoints')}}】</span>
+                                <span class="explain-title"
+                                    >【{{
+                                        $t("evaluation.knowledgePoints")
+                                    }}】</span
+                                >
                                 <div class="item-explain-details">
-                                    <span v-if="!item.points">{{$t('evaluation.noPoints')}}</span>
+                                    <span v-if="!item.points">{{
+                                        $t("evaluation.noPoints")
+                                    }}</span>
                                     <div v-else>
-                                        <span v-for="(point, index) in item.points" class="item-point-tag" :key="index">
+                                        <span
+                                            v-for="(
+                                                point, index
+                                            ) in item.points"
+                                            class="item-point-tag"
+                                            :key="index"
+                                        >
                                             {{ point }}
                                         </span>
                                     </div>
@@ -155,29 +379,59 @@
                 </transition>
                 <!-- 底部题目操作栏 -->
                 <div class="item-tools">
-                    <span class="item-tools-info">{{$t('evaluation.filter.type')}}:{{ exersicesType[item.type] }}</span>
-                    <span class="item-tools-info">{{$t('evaluation.filter.diff')}}:{{ exersicesDiff[item.level - 1] }}</span>
+                    <span class="item-tools-info"
+                        >{{ $t("evaluation.filter.type") }}:{{
+                            exersicesType[item.type]
+                        }}</span
+                    >
+                    <span class="item-tools-info"
+                        >{{ $t("evaluation.filter.diff") }}:{{
+                            exersicesDiff[item.level - 1]
+                        }}</span
+                    >
                     <!-- <span class="item-tools-info">{{$t('evaluation.filter.useCount')}}:{{ item.usageCount || 0 }} 次</span> -->
 
-                    <Button type="text" @click.stop="chooseExercise(item)" :icon="ids.indexOf(item.id) > -1 ? 'md-remove' : 'md-add'" style="margin-right: 10px">
-                        {{ids.indexOf(item.id) > -1 ? $t('evaluation.removeItem') : $t('evaluation.selectItem')}}
+                    <Button
+                        type="text"
+                        @click.stop="chooseExercise(item)"
+                        :icon="
+                            ids.indexOf(item.id) > -1 ? 'md-remove' : 'md-add'
+                        "
+                        style="margin-right: 10px"
+                    >
+                        {{
+                            ids.indexOf(item.id) > -1
+                                ? $t("evaluation.removeItem")
+                                : $t("evaluation.selectItem")
+                        }}
                     </Button>
                 </div>
             </div>
         </div>
 
         <!-- 底部分页区域 -->
-        <Page :total="totalNum" show-sizer show-total :page-size="pageSize" :current="pageNum" @on-page-size-change="pageSizeChange" @on-change="pageChange" :page-size-opts="[5, 10, 15, 20]" />
+        <Page
+            :total="totalNum"
+            show-sizer
+            show-total
+            :page-size="pageSize"
+            :current="pageNum"
+            placement="top"
+            transfer
+            @on-page-size-change="pageSizeChange"
+            @on-change="pageChange"
+            :page-size-opts="[5, 10, 15, 20]"
+        />
     </div>
 </template>
 <script>
-import blobTool from "@/utils/blobTool.js";
+import blobTool from "@/utils/blobTool.js"
 
 export default {
     data() {
         return {
-			typeCountArr:[],
-			fieldCountArr:[],
+            typeCountArr: [],
+            fieldCountArr: [],
             userId: "",
             schoolCode: "",
             dataLoading: false,
@@ -212,8 +466,8 @@ export default {
             curAudioName: "",
             curVideoSrc: "",
             curVideoName: "",
-            selectItems: []
-        };
+            selectItems: [],
+        }
     },
     created() {
         this.getSchoolInfo()
@@ -231,19 +485,19 @@ export default {
                     }
                 }
             }
-            this.$emit('chooseQuChange', this.selectItems)
+            this.$emit("chooseQuChange", this.selectItems)
         },
         /* 音频弹窗切换事件 */
         onAudioModalChange(val) {
             if (!val) {
-                this.$refs.audioPlayer.onCloseAudio();
+                this.$refs.audioPlayer.onCloseAudio()
             }
         },
 
         /* 视频弹窗切换事件 */
         onVideoModalChange(val) {
             if (!val) {
-                this.$refs.videoPlayer.onCloseAudio();
+                this.$refs.videoPlayer.onCloseAudio()
             }
         },
 
@@ -252,60 +506,60 @@ export default {
             // e.stopPropagation()
             let sasString = JSON.parse(
                 decodeURIComponent(localStorage.getItem("user_profile"))
-            ).blob_sas;
+            ).blob_sas
             // console.log(JSON.parse(decodeURIComponent(localStorage.getItem('school_profile'))))
             if (e.srcElement.classList[0] === "richText-audio") {
-                this.playAudioModal = true;
-                this.curAudioSrc = e.srcElement.dataset.url;
-                this.curAudioName = e.srcElement.dataset.name;
+                this.playAudioModal = true
+                this.curAudioSrc = e.srcElement.dataset.url
+                this.curAudioName = e.srcElement.dataset.name
             } else if (e.srcElement.classList[0] === "richText-video") {
-                this.playVideoModal = true;
+                this.playVideoModal = true
                 this.curVideoSrc =
-                    e.srcElement.dataset.url.split("?")[0] + "?" + sasString;
-                console.log(this.curVideoSrc);
-                this.curVideoName = e.srcElement.dataset.name;
+                    e.srcElement.dataset.url.split("?")[0] + "?" + sasString
+                console.log(this.curVideoSrc)
+                this.curVideoName = e.srcElement.dataset.name
             }
         },
 
         /** 获取区班校信息 */
         getSchoolInfo() {
-            this.dataLoading = true;
+            this.dataLoading = true
             if (!this.hasSchool) {
-                this.userId = this.$store.state.userInfo.TEAMModelId;
-                this.filterOrigin = this.$store.state.userInfo.TEAMModelId;
-                this.doFilter();
+                this.userId = this.$store.state.userInfo.TEAMModelId
+                this.filterOrigin = this.$store.state.userInfo.TEAMModelId
+                this.doFilter()
             } else {
                 this.$store.dispatch("user/getSchoolProfile").then((res) => {
-                    let schoolBaseInfo = res.school_base;
+                    let schoolBaseInfo = res.school_base
                     if (schoolBaseInfo) {
-                        this.schoolInfo = schoolBaseInfo;
-                        this.schoolCode = schoolBaseInfo.id;
-                        this.userId = this.$store.state.userInfo.TEAMModelId;
-                        this.filterOrigin = this.$store.state.userInfo.TEAMModelId;
-                        this.periodList = schoolBaseInfo.period;
-                        console.log('periodList:', this.periodList)
+                        this.schoolInfo = schoolBaseInfo
+                        this.schoolCode = schoolBaseInfo.id
+                        this.userId = this.$store.state.userInfo.TEAMModelId
+                        this.filterOrigin =
+                            this.$store.state.userInfo.TEAMModelId
+                        this.periodList = schoolBaseInfo.period
+                        console.log("periodList:", this.periodList)
                         if (schoolBaseInfo.period.length) {
-                            this.gradeList = schoolBaseInfo.period[0].grades;
-                            this.subjectList = schoolBaseInfo.period[0].subjects;
+                            this.gradeList = schoolBaseInfo.period[0].grades
+                            this.subjectList = schoolBaseInfo.period[0].subjects
                         }
-                        this.doFilter();
+                        this.doFilter()
                     }
-                });
+                })
             }
-
         },
 
         /* 获取BLOB所有试题LIST */
         async getBlobOrigin() {
             // 获取初始化Blob需要的数据
-            let sasData = await this.$tools.getPrivateSas();
+            let sasData = await this.$tools.getPrivateSas()
             //初始化Blob
             let containerClient = new blobTool(
                 sasData.url,
                 sasData.name,
                 sasData.sas,
                 "private"
-            );
+            )
             // 等待blob的返回结果
             containerClient
                 .listBlob({
@@ -313,33 +567,42 @@ export default {
                 })
                 .then(
                     (res) => {
-                        console.log(res);
+                        console.log(res)
                     },
                     (err) => {
-                        this.$Message.error("API Error");
+                        this.$Message.error("API Error")
                     }
-                );
+                )
         },
 
         /** 执行筛选条件获取数据 */
         doFilter() {
-            this.dataLoading = true;
-            this.collapseList = []; // 所有详情都收起来
+            this.dataLoading = true
+            this.collapseList = [] // 所有详情都收起来
             /** 定义查询接口的参数规格 */
             this.filterParams = {
                 // '@CURRPAGE': this.pageNum,
                 // '@PAGESIZE': this.pageSize,
                 "@DESC": this.filterSort,
                 code: this.filterOrigin,
-                periodId: this.filterOrigin == this.schoolCode ? [this.periodList[this.filterPeriod].id] : [],
-                "gradeIds[*]": this.filterOrigin == this.schoolCode ? this.deleteFalse(this.filterGrade).map(i => i + '') : [],
-                subjectId: this.filterOrigin == this.schoolCode ? this.deleteFalse(this.filterSubject) : [],
+                periodId:
+                    this.filterOrigin == this.schoolCode
+                        ? [this.periodList[this.filterPeriod].id]
+                        : [],
+                "gradeIds[*]":
+                    this.filterOrigin == this.schoolCode
+                        ? this.deleteFalse(this.filterGrade).map((i) => i + "")
+                        : [],
+                subjectId:
+                    this.filterOrigin == this.schoolCode
+                        ? this.deleteFalse(this.filterSubject)
+                        : [],
                 level: this.deleteFalse(this.filterDiff),
                 type: this.deleteFalse(this.filterType),
                 field: this.deleteFalse(this.filterField),
                 scope: this.curScope,
-            };
-            this.getExerciseList(this.filterParams);
+            }
+            this.getExerciseList(this.filterParams)
         },
 
         /**
@@ -347,23 +610,23 @@ export default {
          * @param data
          */
         async getExerciseList(data) {
-            let that = this;
+            let that = this
             this.$api.newEvaluation.FindExerciseList(data).then(async (res) => {
-                let list = res.items;
+                let list = res.items
                 /* 获取试题总数 */
-                this.totalNum = res.items.length;
+                this.totalNum = res.items.length
                 /* 查找当前页面所有知识点ID换名称 */
                 // this.getPointsByIds(this.getPointIds(list)).then(res => {
                 // 	this.allPointList = res
                 // })
-                this.exerciseList = list;
-                this.originData = list;
-                this.pageChange(1);
+                this.exerciseList = list
+                this.originData = list
+                this.pageChange(1)
 
                 setTimeout(() => {
-                    that.dataLoading = false;
-                }, 1000);
-            });
+                    that.dataLoading = false
+                }, 1000)
+            })
         },
 
         /**
@@ -371,11 +634,11 @@ export default {
          * @param arr
          */
         getPointIds(arr) {
-            let ids = [];
+            let ids = []
             arr.forEach((i) => {
-                ids = ids.concat(i.points);
-            });
-            return [...new Set(ids)];
+                ids = ids.concat(i.points)
+            })
+            return [...new Set(ids)]
         },
 
         /**
@@ -384,7 +647,7 @@ export default {
          */
         getPointsByIds(ids) {
             if (typeof ids[0] !== "string") {
-                ids = ids.map((item) => item.id);
+                ids = ids.map((item) => item.id)
             }
             return new Promise((r, j) => {
                 if (ids.length) {
@@ -392,18 +655,18 @@ export default {
                         .FindKnowledgebyId(ids)
                         .then((res) => {
                             if (!res.error && res.result.data.length) {
-                                r(res.result.data);
+                                r(res.result.data)
                             } else {
-                                r([]);
+                                r([])
                             }
                         })
                         .catch((err) => {
-                            j(err);
-                        });
+                            j(err)
+                        })
                 } else {
-                    r([]);
+                    r([])
                 }
-            });
+            })
         },
 
         /**
@@ -412,26 +675,26 @@ export default {
          * @param id
          */
         onQuestionToggle(index, id, e) {
-            let curClassName = e.target.className;
-            console.log(curClassName);
+            let curClassName = e.target.className
+            console.log(curClassName)
             if (
                 curClassName === "item-tools" ||
                 curClassName === "richText-video" ||
                 curClassName === "richText-audio"
             )
-                return;
-            e.stopPropagation();
-            let listIndex = this.collapseList.indexOf(index);
+                return
+            e.stopPropagation()
+            let listIndex = this.collapseList.indexOf(index)
             if (listIndex > -1) {
-                this.collapseList.splice(listIndex, 1);
+                this.collapseList.splice(listIndex, 1)
             } else {
-                this.collapseList.push(index);
+                this.collapseList.push(index)
                 let exerciseItemDom = e.path.filter(
                     (i) => i.className === "exercise-item"
-                );
+                )
             }
 
-            this.$emit("toggleChange", this.collapseList);
+            this.$emit("toggleChange", this.collapseList)
         },
 
         /**
@@ -440,11 +703,11 @@ export default {
          */
         filterPeriodChange(val) {
             // this.filterPeriod = this.periodList[val].periodCode
-            this.gradeList = this.schoolInfo.period[val].grades;
-            this.subjectList = this.schoolInfo.period[val].subjects;
-            this.filterGrade = [false];
-            this.filterSubject = [false];
-            this.doFilter();
+            this.gradeList = this.schoolInfo.period[val].grades
+            this.subjectList = this.schoolInfo.period[val].subjects
+            this.filterGrade = [false]
+            this.filterSubject = [false]
+            this.doFilter()
         },
 
         /**
@@ -453,11 +716,11 @@ export default {
          */
         filterGradeChange(val) {
             if (val.length > 1 && val.indexOf(false) === 0) {
-                this.filterGrade.splice(val.indexOf(false), 1);
+                this.filterGrade.splice(val.indexOf(false), 1)
             } else if (val.indexOf(false) > -1 || val.length === 0) {
-                this.filterGrade = [false];
+                this.filterGrade = [false]
             }
-            this.doFilter();
+            this.doFilter()
         },
 
         /**
@@ -466,11 +729,11 @@ export default {
          */
         filterSubjectChange(val) {
             if (val.length > 1 && val.indexOf(false) === 0) {
-                this.filterSubject.splice(val.indexOf(false), 1);
+                this.filterSubject.splice(val.indexOf(false), 1)
             } else if (val.indexOf(false) > -1 || val.length === 0) {
-                this.filterSubject = [false];
+                this.filterSubject = [false]
             }
-            this.doFilter();
+            this.doFilter()
         },
 
         /**
@@ -478,8 +741,8 @@ export default {
          * @param val
          */
         filterOriginChange(origin) {
-            this.filterOrigin = origin;
-            this.doFilter();
+            this.filterOrigin = origin
+            this.doFilter()
         },
 
         /**
@@ -488,11 +751,11 @@ export default {
          */
         filterTypeChange(val) {
             if (val.length > 1 && val.indexOf("all") === 0) {
-                this.filterType.splice(val.indexOf("all"), 1);
+                this.filterType.splice(val.indexOf("all"), 1)
             } else if (val.indexOf("all") > -1 || val.length === 0) {
-                this.filterType = ["all"];
+                this.filterType = ["all"]
             }
-            this.doFilter();
+            this.doFilter()
         },
 
         /**
@@ -501,11 +764,11 @@ export default {
          */
         filterDiffChange(val) {
             if (val.length > 1 && val.indexOf("all") === 0) {
-                this.filterDiff.splice(val.indexOf("all"), 1);
+                this.filterDiff.splice(val.indexOf("all"), 1)
             } else if (val.indexOf("all") > -1 || val.length === 0) {
-                this.filterDiff = ["all"];
+                this.filterDiff = ["all"]
             }
-            this.doFilter();
+            this.doFilter()
         },
 
         /**
@@ -514,11 +777,11 @@ export default {
          */
         filterFieldChange(val) {
             if (val.length > 1 && val.indexOf("all") === 0) {
-                this.filterField.splice(val.indexOf("all"), 1);
+                this.filterField.splice(val.indexOf("all"), 1)
             } else if (val.indexOf("all") > -1 || val.length === 0) {
-                this.filterField = ["all"];
+                this.filterField = ["all"]
             }
-            this.doFilter();
+            this.doFilter()
         },
 
         /**
@@ -526,7 +789,7 @@ export default {
          * @param val
          */
         filterSortChange(val) {
-            this.doFilter();
+            this.doFilter()
         },
 
         /**
@@ -534,11 +797,12 @@ export default {
          * @param arr
          */
         deleteFalse(arr) {
-            let list = JSON.parse(JSON.stringify(arr));
+            let list = JSON.parse(JSON.stringify(arr))
             list.forEach((item, index) => {
-                if ((!item || item === "all") && item !== 0) list.splice(index, 1);
-            });
-            return list;
+                if ((!item || item === "all") && item !== 0)
+                    list.splice(index, 1)
+            })
+            return list
         },
 
         /**
@@ -546,17 +810,17 @@ export default {
          * @param page
          */
         async pageChange(page) {
-            this.pageNum = page;
-            let start = this.pageSize * (page - 1);
-            let end = this.pageSize * page;
+            this.pageNum = page
+            let start = this.pageSize * (page - 1)
+            let end = this.pageSize * page
             // 拿到当前页码需要展示的数据
-            let simpleList = this.originData.slice(start, end);
+            let simpleList = this.originData.slice(start, end)
             try {
                 // 执行试题换取完整JSON数据
-                this.exerciseList = await this.$evTools.getFullItem(simpleList);
-                this.currentPage = page;
+                this.exerciseList = await this.$evTools.getFullItem(simpleList)
+                this.currentPage = page
             } catch (e) {
-                console.log(e);
+                console.log(e)
             }
         },
 
@@ -565,10 +829,9 @@ export default {
          * @param val
          */
         pageSizeChange(val) {
-            this.pageSize = val;
-            this.pageChange(1);
+            this.pageSize = val
+            this.pageChange(1)
         },
-
     },
     mounted() {
         // 公式渲染
@@ -578,30 +841,30 @@ export default {
             //     MathJax.Hub,
             //     this.$refs.mathJaxContainer,
             // ]);
-        });
+        })
     },
     computed: {
         headers() {
-            let hd = {};
-            hd["Authorization"] = "Bearer " + localStorage.getItem("token");
-            return hd;
+            let hd = {}
+            hd["Authorization"] = "Bearer " + localStorage.getItem("token")
+            return hd
         },
         curScope() {
-            return this.filterOrigin === this.$store.state.userInfo.schoolCode ?
-                "school" :
-                "private";
+            return this.filterOrigin === this.$store.state.userInfo.schoolCode
+                ? "school"
+                : "private"
         },
         hasSchool() {
-            return this.$store.state.userInfo.hasSchool;
+            return this.$store.state.userInfo.hasSchool
         },
         //已选题目的id
         ids() {
-            return this.selectItems.map(item => {
+            return this.selectItems.map((item) => {
                 return item.id
             })
-        }
+        },
     },
-};
+}
 </script>
 <style scoped lang="less">
 @import "./ExerciseList.less";

+ 117 - 0
TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/Cowork.vue

@@ -0,0 +1,117 @@
+<template>
+    <div>
+        <div>
+            <p class="action-type">{{ cowork.coworkType === 'All' ? $t('cusMgt.rcd.coworkAll') : (cowork.coworkType === 'Group' ? $t('cusMgt.rcd.coworkGroup') : $t('cusMgt.rcd.coworkdiff')) }}</p>
+            <div class="cowork-box">
+                <div v-for="(item, index) in coworkData" :key="index" style="">
+                    <img :src="item.snapshotUrl" class="receive-img" alt="" @click="viewImage(item.snapshotUrl)">
+                    <p>
+                        {{ item.title }}
+                        <Tooltip placement="bottom" v-show="cowork.coworkType != 'All'">
+                            <Icon type="ios-contacts" size="20" color="#2D8CF0" />
+                            <template #content>
+                                <span v-for="(stu, sIndex) in item.stuList" :key="sIndex">
+                                    <span v-if="stu">
+                                        {{ stu.name }}
+                                        <span v-show="sIndex != (item.stuList.length - 1)">、</span>
+                                    </span>
+                                </span>
+                            </template>
+                        </Tooltip>
+                    </p>
+                </div>
+            </div>
+        </div>
+        <StudentClient class="receive-student"></StudentClient>
+    </div>
+</template>
+
+<script>
+import StudentClient from '@/view/classrecord/eventchart/StudentClient.vue'
+export default {
+    components: {
+        StudentClient
+    },
+    props: {
+        cowork: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        },
+        students: {
+            type: Array,
+            default: () => {
+                return []
+            }
+        },
+    },
+    data () {
+        return {
+        }
+    },
+    created () {
+    },
+    computed: {
+        coworkData() {
+            if(this.cowork.coworkType === 'All') {
+                return this.cowork.coworkGroupInfoList
+            } else {
+                let data = []
+                let owner = this.students.find(stu => stu.id === this.$store.state.userInfo.sub)
+                data = this.cowork.coworkGroupInfoList.filter(item => {
+                    return (item.members.filter(member => member == owner.seatID)).length
+                })
+                return data
+            }
+        },
+    },
+    methods: {
+        viewImage(url) {
+            this.$hevueImgPreview(url)
+        },
+    },
+    watch: {
+        cowork: {
+            handler(n, o) {
+            },
+            immediate: true,
+            deep: true
+        },
+    }
+}
+</script>
+
+<style lang="less" scoped>
+.action-type {
+    text-align: right;
+    font-weight: bold;
+    margin: 0 10px 5px 0;
+}
+.cowork-box {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: flex-end;
+
+    &>div {
+        margin: 0 10px 15px;
+        text-align: center;
+    }
+
+    .receive-img {
+        max-width: 150px;
+        max-height: 150px;
+        border: 1px solid #eeeeee;
+        cursor: pointer;
+    }
+}
+.cowork-group{
+    width: 400px;
+    height: 300px;
+}
+.cowork-student {
+    width: 300px;
+    height: 300px;
+    margin-right: 10px;
+}
+</style>

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

@@ -252,9 +252,10 @@ export default {
             this.dataLoading = true
             let requestData = {
                 id: this.examDetaiInfo.id,
-                code: this.recordInfo.scope == 'school' ? this.$store.state.userInfo.schoolCode : this.$store.state.userInfo.TEAMModelId,
+                code: this.recordInfo.scope == 'school' ? this.$store.state.userInfo.schoolCode : this.examDetaiInfo.code.substr(this.examDetaiInfo.code.lastIndexOf("-") + 1),
                 subjectId: this.recordInfo.subjectId,
                 classId: this.recordInfo.groupIds[0],
+				startTime: this.examInfo.startTime,
             };
             this.$api.learnActivity.FindAllStudent(requestData).then(
                 (res) => {

+ 51 - 4
TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/RecordView.vue

@@ -134,6 +134,7 @@
                                     <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>
                                     <Button type="warning" :disabled="!filtertype.smart" @click="filterFn('smart')">{{ $t('cusMgt.rcd.filter6') }}<!-- ({{ filtertype.exam }}) --></Button>
+                                    <Button type="warning" :disabled="!filtertype.cowork" @click="filterFn('cowork')">{{ $t('cusMgt.rcd.filter7') }}<!-- ({{ filtertype.exam }}) --></Button>
                                 </div>
                             </div>
                         </div>
@@ -158,7 +159,7 @@
                                                             <!-- 即问即答 -->
                                                             <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') && baseData" :evtType="event.Event" :irsData="event.data" :students="baseData.student"></PopQues>
+                                                                <PopQues class="event-item" v-if="(event.Event === 'PopQuesLoad' || event.Event === 'ReAtmpAnsStrt') && baseData && event.data.quesType != 'subjective'" :evtType="event.Event" :irsData="event.data" :students="baseData.student"></PopQues>
                                                             </div>
                                                             <!-- 抢权 -->
                                                             <Buzr class="event-item student-event" v-if="event.Event === 'BuzrAns' && baseData" :buzrData="event.data" :students="baseData.student"></Buzr>
@@ -174,6 +175,8 @@
                                                             <!-- 课中评测 -->
                                                             <Exam class="student-event event-item" :examInfo="event.data" :recordInfo="recordInfo" v-if="event.Event === 'SPQStrt'"></Exam>
                                                             <SmartRating class="event-item student-event" :recordInfo="recordInfo" :smartRate="event.data" :students="baseData.student" :vote="event.vote" :sas="sasRecd" v-else-if="event.Event === 'RatingStart'"></SmartRating>
+                                                            <!-- 协作 -->
+                                                            <Cowork class="event-item student-event" :cowork="event.data" :students="baseData.student" v-else-if="event.Event === 'CoworkLoad'"></Cowork>
                                                         </div>
                                                     </template>
                                                 </div>
@@ -220,12 +223,13 @@ import Pick from './Pick.vue';
 import Exam from './Exam.vue';
 import myWorks from './myWorks.vue';
 import SmartRating from './SmartRating.vue';
+import Cowork from './Cowork.vue';
 
 export default {
     components: {
         RcdPoster,
         Loading,
-        DataCount, ShowQues, PopQues, Buzr, Push, SmartRating,
+        DataCount, ShowQues, PopQues, Buzr, Push, SmartRating, Cowork,
         StuReceive, ReceiveBack, Pick, Exam, myWorks,
     },
     data () {
@@ -289,6 +293,7 @@ export default {
             irsData: [], //irs.json
             taskData: [], //task.json
             smartData: [],//smartRating.json
+            coworkData: [],//Cowork.json
             fnEvents: [], //功能事件
             events: [], //事件ID
             hiTeachEvent: [], //需要解析的事件信息
@@ -305,6 +310,7 @@ export default {
                 irs: 0, //互动
                 exam: 0, //测验
                 smart: 0, //智慧评分
+                cowork: 0, //协作
             },
             myWorks: [],
             haveInteraction: true,
@@ -501,7 +507,7 @@ export default {
             this.taskData = []
             this.smartData = []
             this.baseData = undefined
-            this.filtertype = {push: 0, task: 0, irs: 0, exam: 0, smart: 0}
+            this.filtertype = {push: 0, task: 0, irs: 0, exam: 0, smart: 0, cowork: 0}
             let sas = await this.$tools.getBlobSas(this.recordInfo.scope === 'school' ? this.recordInfo.school : this.recordInfo.tmdid)
             this.sasRecd = sas
             this.recordInfo.eNote = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/Note.pdf?${sas.sas}`
@@ -567,6 +573,21 @@ export default {
             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) || '[]')
+                this.irsData.forEach(item => {
+                    item.quesType = item.question?.exercise?.type || ''
+                    item.ansType = item.question?.exercise?.answerType || ''
+                    if(item.quesType === 'subjective' && item.ansType != 'text' && item.ansType != 'text_Image') {
+                        for (const key in item.clientAnswers) {
+                            item.clientAnswers[key] = item.clientAnswers[key].map(ans => {
+                                ans = ans.map(info => {
+                                    info = `${sas.url}/${sas.name}/records/${this.recordInfo.id}${ans}?${sas.sas}`
+                                    return info
+                                })
+                                return ans
+                            })
+                        }
+                    }
+                })
             } catch (e) {
                 this.irsData = []
             }
@@ -600,6 +621,21 @@ export default {
             } catch (e) {
                 this.smartData = []
             }
+            try {
+                let coworkUrl = `${sas.url}/${sas.name}/records/${this.recordInfo.id}/IES/Cowork.json?${sas.sas}`
+                this.coworkData = JSON.parse(await this.$tools.getFile(coworkUrl) || '[]')
+                this.coworkData.forEach(item => {
+                    item.coworkGroupInfoList.forEach(info => {
+                        info.snapshotUrl = `${sas.url}/${sas.name}/records/${this.recordInfo.id}${info.snapshot}?${sas.sas}`
+                        info.stuList = info.members.map(members => {
+                            return this.baseData.student.find(stu => stu.seatID == members)
+                        })
+                    })
+                })
+            } catch (e) {
+                console.log('15645f6451f', e);
+                this.coworkData = []
+            }
 
             //这里需要判断录制开始的pageid
             let startInfo = pageEvents.find(item => item.Event === 'EzsStartRecord')
@@ -663,11 +699,22 @@ export default {
                                 }
                             }
                             this.filtertype.smart += 1
-                        break
+                            break
+                        case 'cowork':
+                            e.data = this.coworkData.find(t => t.pageID == e.Pgid)
+                            this.filtertype.cowork += 1
+                            break
                         default:
                             break
                     }
                 })
+                for (let i = 1; i < page.pageData.length; i++) {
+                    // 星光大评分会操作几次就返回几条数据,所以需要去重只显示第一条
+                    if(page.pageData[i - 1].Event === 'RatingStart' && page.pageData[i].Event === 'RatingStart' && page.pageData[i].RatingType && page.pageData[i].RatingType === 'GrandRating' && page.pageData[i - 1].Pgid === page.pageData[i].Pgid) {
+                        page.pageData.splice(i, 1)
+                        i--
+                    }
+                }
                 this.pageList.push(page)
             })
             if(this.baseData) {

+ 115 - 8
TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/ShowQues.vue

@@ -47,14 +47,43 @@
                 </template>
             </div>
         </div> -->
-        <p class="event-type">{{evtType == 'PopQuesLoad' ? $t("studentWeb.hiteachNote.qA") : $t("studentWeb.hiteachNote.qaAgain")}}:</p>
-        <p class="show-ques" v-if="ansData.length">
-            <span v-for="(ans, index) in ansData" :key="index" v-html="ans"></span>
-        </p>
-        <p class="show-ques" v-else>{{ $t("studentWeb.hiteachNote.noanswer") }}</p>
-        <div style="margin-right: 20px;" v-if="answer.length">
-            <Icon type="md-checkmark-circle" size="18" color="#00ad6c" v-show="isRight" />
-            <Icon type="md-close-circle" size="18" color="#ff5508" v-show="!isRight" />
+        <div>
+            <p class="event-type">
+                {{evtType == 'PopQuesLoad' ? $t("studentWeb.hiteachNote.qA") : $t("studentWeb.hiteachNote.qaAgain")}}
+                <span v-if="irsData.quesType === 'subjective'">({{ $t('evaluation.subjective') }}-{{ $t(`evaluation.newExercise[${irsData.ansType}]`) }})</span>:
+            </p>
+            <div class="show-ques file-box" v-if="ansData.length">
+                <template v-if="irsData.quesType === 'subjective' && irsData.ansType != 'text' && irsData.ansType != 'text_Image'">
+                    <div v-for="(ans, index) in ansData" :key="index">
+                        <img :src="ans" alt="" v-if="irsData.ansType === 'image'">
+                        <audio controls v-if="irsData.ansType === 'audio'">
+                            <source :src="ans">
+                            {{$t('teachContent.notAudio')}}
+                        </audio>
+                        <video v-if="irsData.ansType === 'video'" :src="ans" width="870" controls="controls" style="max-height: 800px;"></video>
+                        <div v-if="irsData.ansType === 'file'" class="repair-link-wrap-item-box">
+                            <div class="file-icon">
+                                <img :src="$tools.getFileThum($tools.getSuffix(ans), getFileName(ans))"/>
+                            </div>
+                            <div class="file-info">
+                                <p class="file-name">{{ getFileName(ans) }}</p>
+                                <div>
+                                    <span @click="onPreview(ans)">{{ $t('ability.review.preview')}}</span>
+                                    <span @click="onDownload(ans)">{{ $t('ability.review.download')}}</span>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </template>
+                <template v-else>
+                    <span v-for="(ans, index) in ansData" :key="index" v-html="ans"></span>
+                </template>
+            </div>
+            <p class="show-ques" v-else>{{ $t("studentWeb.hiteachNote.noanswer") }}</p>
+            <div style="margin-right: 20px;" v-if="answer.length">
+                <Icon type="md-checkmark-circle" size="18" color="#00ad6c" v-show="isRight" />
+                <Icon type="md-close-circle" size="18" color="#ff5508" v-show="!isRight" />
+            </div>
         </div>
         <div>
             <Icon type="ios-person" class="owner-student-client-icon"/>
@@ -106,6 +135,33 @@ export default {
         TeacherClient,
         StudentClient
     },
+    methods: {
+        getFileName(url) {
+            let text = url.substring(url.lastIndexOf("/Ans/") + 5, url.lastIndexOf("?"))
+            return text
+        },
+        /* 预览 */
+        async onPreview(item){
+            let url = item
+            if (this.$tools.getSuffix(this.getFileName(item)) === 'pdf') {
+                window.open('/web/viewer.html?file=' + encodeURIComponent(url));
+            } else if(item.type === 'doc') {
+                window.open('https://view.officeapps.live.com/op/view.aspx?src=' + escape(url));
+            } else if(item.type === 'image') {
+                this.$hevueImgPreview(url)
+            } else if(item.type === 'link') {
+				window.open(/^(http:|https:)/i.test(url) ? url : "http://" + url)
+            } else {
+                console.log(1111111111111);
+                this.$hevueImgPreview(url)
+            }
+        },
+
+        /* 下载 */
+        async onDownload(item){
+            this.$tools.doDownloadByUrl(item, this.getFileName(item))
+        },
+    },
     watch: {
         irsData: {
             deep: true,
@@ -217,6 +273,50 @@ export default {
 
     .show-ques {
         margin-right: 10px;
+        img {
+            cursor: pointer;
+            width: 120px !important;
+            height: auto;
+        }
+        
+        .repair-link-wrap-item-box {
+            display: flex;
+            position: relative;
+            // background-color: #e3e3e3;
+            border-radius: 5px;
+            padding: 10px 0;
+            font-size: 14px;
+
+            &:hover {
+                background-color: #ebe9e9;
+            }
+
+            .file-icon {
+                img {
+                    width: 45px !important;
+                }
+            }
+
+            .file-info {
+                margin-left: 10px;
+
+                .file-name {
+                    font-weight: bold;
+                    margin-bottom: 5px;
+                }
+
+                span {
+                    color: #16a3b5;
+                    margin-right: 15px;
+                    cursor: pointer;
+                }
+            }
+        }
+    }
+    .file-box {
+        display: flex;
+        flex-wrap: wrap;
+        justify-content: flex-end;
     }
 
 }
@@ -224,5 +324,12 @@ export default {
     margin-right: 20px;
     font-size: 15px;
     font-weight: 600;
+    margin: 0 10px 5px 0;
+}
+</style>
+<style lang="less">
+.show-ques svg{
+    width: 200px;
+    height: auto;
 }
 </style>

+ 120 - 36
TEAMModelOS/ClientApp/src/components/student-web/ClassRecord/SmartRating.vue

@@ -6,39 +6,69 @@
             mutualSummary:互评
             mutualDetailSummary:互评数据
          -->
-        <div class="smart-wrap">
-            <p class="clt-type">{{ smartType.name }}:</p>
-            <template v-if="smartType.value === 'vote'">
-                <!-- <div v-for="(item, index) in scoreListNew" :key="index" style="margin-bottom: 10px;"> -->
-                    {{ $t('answerSheet.tip2') }}{{ vote.round }}{{ $t('cusMgt.rcd.wheel') }}({{ vote.votes }}{{ $t('studentWeb.vote.tickets') }})
-                    <Icon :title="$t('cusMgt.rcd.viewCom')" type="md-chatbubbles" size="17" color="#2EC7C9" @click="openComment('vote')" style="cursor: pointer; margin-top: 3px; margin-right: 5px;" />
-                    <SmartVote :smartData="scoreListNew"></SmartVote>
-                <!-- </div> -->
-            </template>
-            <template v-else-if="smartType.value === 'score'">
-                <SmartScore :smartData="scoreListNew"></SmartScore>
-            </template>
-            <template v-else>
-                <div v-for="(item, index) in scoreListNew" :key="index" class="smart-list">
-                    <p style="height: 21px;">
-                        <Icon type="md-trophy" v-if="item.king" color="#ff880d" />
-                    </p>
-                    <p>
-                        <span style="color: #2d8cf0;">{{ item.name }}</span>
-                        <Icon type="md-chatbubbles" color="#2EC7C9" style="cursor: pointer;" @click="openComment('mutal', index, item)"
-                                v-show="!smartRate.smartRateSummary.rateInfo.AnonyCandi" />
-                    </p>
-                    <p>{{ $t('cusMgt.rcd.avgScore') }}:{{ item.result }}</p>
-                    <img v-if="smartRate.smartRateSummary.rateInfo.RatingSource === 'StudentWork'" :src="item.material" @click="$hevueImgPreview(item.material)" />
-                    <p v-if="smartRate.smartRateSummary.rateInfo.RatingSource === 'IRS'" v-html="item.material" class="smart-material"></p>
-                </div>
-            </template>
+        <div>
+            <p class="clt-type">{{ smartType.name }}</p>
+            <div class="smart-wrap">
+                <template v-if="smartType.value === 'vote'">
+                    <!-- <div v-for="(item, index) in scoreListNew" :key="index" style="margin-bottom: 10px;"> -->
+                        {{ $t('answerSheet.tip2') }}{{ vote.round }}{{ $t('cusMgt.rcd.wheel') }}({{ vote.votes }}{{ $t('studentWeb.vote.tickets') }})
+                        <Icon :title="$t('cusMgt.rcd.viewCom')" type="md-chatbubbles" size="17" color="#2EC7C9" @click="openComment('vote')" style="cursor: pointer; margin-top: 3px; margin-right: 5px;" />
+                        <SmartVote :smartData="scoreListNew"></SmartVote>
+                    <!-- </div> -->
+                </template>
+                <template v-else-if="smartType.value === 'score'">
+                    <SmartScore :smartData="scoreListNew"></SmartScore>
+                </template>
+                <template v-else>
+                    <div v-for="(item, index) in scoreListNew" :key="index" class="smart-list">
+                        <p style="height: 21px;">
+                            <Icon type="md-trophy" v-if="item.king" color="#ff880d" />
+                        </p>
+                        <p>
+                            <span style="color: #2d8cf0;">{{ item.name }}</span>
+                            <Icon type="md-chatbubbles" color="#2EC7C9" style="cursor: pointer;" @click="openComment('mutal', index, item)"
+                                    v-show="!smartRate.smartRateSummary.rateInfo.AnonyCandi" />
+                        </p>
+                        <p>{{ $t('cusMgt.rcd.avgScore') }}:{{ item.result }}</p>
+                        <img v-if="smartRate.smartRateSummary.rateInfo.RatingSource === 'StudentWork'" :src="item.material" @click="$hevueImgPreview(item.material)" />
+                        <template v-else-if="smartRate.smartRateSummary.rateInfo.RatingSource === 'IRS'">
+                            <audio v-if="materialDataType === 'Audio' || (materialDataType === 'File' && item.fileType === 'audio')" controls>
+                                <source :src="item.material">
+                                {{ $t('teachContent.notAudio') }}
+                            </audio>
+                            <img v-else-if="materialDataType === 'Image' || (materialDataType === 'File' && item.fileType === 'image')" :src="item.material" alt="">
+                            <div v-else-if="materialDataType === 'File'">
+                                <video v-if="item.fileType === 'video'" :src="item.material" width="870" controls="controls" style="max-height: 800px;"></video>
+                                <div v-else class="repair-link-wrap-item-box">
+                                    <div class="file-icon">
+                                        <img :src="$tools.getFileThum(item.fileType, item.fileName)"/>
+                                    </div>
+                                    <div class="file-info">
+                                        <p class="file-name">{{ item.fileName }}</p>
+                                        <div>
+                                            <span @click="onPreview(item)">{{ $t('ability.review.preview')}}</span>
+                                            <span @click="onDownload(item)">{{ $t('ability.review.download')}}</span>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                            <p v-else v-html="item.material"></p>
+                        </template>
+                    </div>
+                </template>
+            </div>
         </div>
         <StudentClient></StudentClient>
         <Modal v-model="isComment" :title="$t('homework.table.comment')" :footer-hide="true">
             <div class="mutal-info" v-if="mutalInfo">
                 <img v-if="smartRate.smartRateSummary.rateInfo.RatingSource === 'StudentWork'" :src="mutalInfo" />
-                <p v-if="smartRate.smartRateSummary.rateInfo.RatingSource === 'IRS'" v-html="mutalInfo"></p>
+                <template v-else-if="smartRate.smartRateSummary.rateInfo.RatingSource === 'IRS'">
+                    <audio v-if="smartRate.smartRateSummary.mutualSummary.materialDataType === 'Audio'" controls>
+                        <source :src="mutalInfo">
+                        {{ $t('teachContent.notAudio') }}
+                    </audio>
+                    <p v-else v-html="mutalInfo"></p>
+                </template>
             </div>
             <div v-for="(item, index) in showResult" :key="index" style="margin-bottom: 5px;">
                 <span style="color: #2d8cf0;">
@@ -103,6 +133,9 @@ export default {
         }
     },
     computed: {
+        materialDataType() {
+            return this.smartRate.smartRateSummary.mutualSummary?.materialDataType
+        },
         smartType() {
             let types = {
                 value: '',
@@ -192,6 +225,8 @@ export default {
                     let s = lists.find(item => {
                         return item.id === seatID
                     })
+                    s.fileName = ''
+                    s.fileType = ''
                     if(this.smartRate.smartRateSummary.mutualSummary.materialInfos.length) {
                         let materialInfos = this.smartRate.smartRateSummary.mutualSummary.materialInfos.find(info => {
                             return info.id === s.id
@@ -199,8 +234,18 @@ export default {
                         // IRS:文字题 StudentWork:图片
                         if(this.smartRate.smartRateSummary.rateInfo.RatingSource === 'StudentWork') {
                             s.material = `${this.sas.url}/${this.sas.name}/records/${this.recordInfo.id}${materialInfos.material}?${this.sas.sas}`
+                            s.fileName = materialInfos.material.substr(materialInfos.material.lastIndexOf("/Ans/") + 5)
                         } else {
-                            s.material = materialInfos.material
+                            if(this.materialDataType != 'Text') {
+                                s.material = `${this.sas.url}/${this.sas.name}/records/${this.recordInfo.id}${materialInfos.material}?${this.sas.sas}`
+                                s.fileName = materialInfos.material.substr(materialInfos.material.lastIndexOf("/Ans/") + 5)
+                                if(this.materialDataType === 'File') {
+                                    let suffix = this.$tools.getSuffix(materialInfos.material)
+                                    item.fileType = ['jpg', 'png', 'gif'].includes(suffix) ? 'image' : ['mp4', 'webm'].includes(suffix) ? 'video' : ['mp3', 'wav'].includes(suffix) ? 'audio' : (['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'pdf'].includes(suffix) ? 'doc' : 'other')
+                                }
+                            } else {
+                                s.material = materialInfos.material
+                            }
                         }
                     } else  {
                         s.material = ''
@@ -209,6 +254,8 @@ export default {
                 } else {
                     lists = lists.map(item => {
                         item.material = ''
+                        item.fileName = ''
+                        item.fileType = ''
                         if(item.result === lists[0].result) {
                             item.king = true
                         }
@@ -219,8 +266,18 @@ export default {
                             // IRS:文字题 StudentWork:图片
                             if(this.smartRate.smartRateSummary.rateInfo.RatingSource === 'StudentWork') {
                                 item.material = `${this.sas.url}/${this.sas.name}/records/${this.recordInfo.id}${materialInfos.material}?${this.sas.sas}`
+                                item.fileName = materialInfos.material.substr(materialInfos.material.lastIndexOf("/Ans/") + 5)
                             } else {
-                                item.material = materialInfos.material
+                                if(this.materialDataType != 'Text') {
+                                    item.material = `${this.sas.url}/${this.sas.name}/records/${this.recordInfo.id}${materialInfos.material}?${this.sas.sas}`
+                                    item.fileName = materialInfos.material.substr(materialInfos.material.lastIndexOf("/Ans/") + 5)
+                                    if(this.materialDataType === 'File') {
+                                        let suffix = this.$tools.getSuffix(materialInfos.material)
+                                        item.fileType = ['jpg', 'png', 'gif'].includes(suffix) ? 'image' : ['mp4', 'webm'].includes(suffix) ? 'video' : ['mp3', 'wav'].includes(suffix) ? 'audio' : (['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'pdf'].includes(suffix) ? 'doc' : 'other')
+                                    }
+                                } else {
+                                    item.material = materialInfos.material
+                                }
                             }
                         }
                         return item
@@ -288,23 +345,45 @@ export default {
             }
             this.isComment = true
         },
+        /* 预览 */
+        async onPreview(item){
+            let url = item.material
+            if (this.$tools.getSuffix(item.fileName) === 'pdf') {
+                window.open('/web/viewer.html?file=' + encodeURIComponent(url));
+            } else if(item.fileType === 'doc') {
+                window.open('https://view.officeapps.live.com/op/view.aspx?src=' + escape(url));
+            } else if(item.fileType === 'image') {
+                this.$hevueImgPreview(url)
+            } else if(item.fileType === 'link') {
+				window.open(/^(http:|https:)/i.test(url) ? url : "http://" + url)
+            } else {
+                this.$hevueImgPreview(url)
+            }
+        },
+        /* 下载 */
+        async onDownload(item){
+            this.$tools.doDownloadByUrl(item.material, item.fileName)
+        },
     },
 }
 </script>
 
 <style lang="less" scoped>
+.clt-type {
+    /* margin-right: 10px;
+    font-size: 15px;
+    font-weight: 600; */
+    
+    text-align: right;
+    font-weight: bold;
+    margin: 0 10px 5px 0;
+}
 .smart-wrap {
     display: flex;
     flex-wrap: wrap;
     justify-content: end;
     position: relative;
 
-    .clt-type {
-        margin-right: 10px;
-        font-size: 15px;
-        font-weight: 600;
-    }
-
     .smart-vote {
         width: 200px;
         height: 200px;
@@ -340,4 +419,9 @@ export default {
         width: 300px;
     }
 }
+
+.smart-list svg {
+    width: 200px;
+    height: auto;
+}
 </style>

+ 3 - 3
TEAMModelOS/ClientApp/src/components/student-web/CourseView/CourseView/ActivityView.vue

@@ -180,12 +180,12 @@ export default {
                         } else {
                             ids.push(item.classIds)
                         }
-                        teaIds.push(item.creatorId)
+                        if(!teaIds.includes(item.creatorId)) teaIds.push(item.creatorId)
                     })
                     let teaidNames = []
                     teaidNames = await this.getTeacherName(teaIds)
-                    idsSchool = [].concat.apply([], idsSchool)
-                    ids = [].concat.apply([], ids)
+                    idsSchool = Array.from(new Set([].concat.apply([], idsSchool)))
+                    ids = Array.from(new Set([].concat.apply([], ids)))
                     let namesSchool = idsSchool.length ? await this.getClassName(idsSchool, true) : []
                     let names = ids.length ? await this.getClassName(ids) : []
                     for (let item of res.datas) {

+ 16 - 7
TEAMModelOS/ClientApp/src/components/student-web/DiscussionBoard.vue

@@ -91,13 +91,13 @@
                                     <p v-html="child.comment"></p>
                                     <div class="disAction">
                                         <div>
-                                            <span @click="likeTopic(child, no)">
+                                            <span @click="likeTopic(child, no, item)">
                                                 <Icon custom="iconfont icon-dianzan" size="17" color="#ff621a" v-show="child.likeit" />
                                                 <Icon custom="iconfont icon-dianzan1" size="17" v-show="!child.likeit" />
                                                 {{ $t("jyzx.common.like") }}
                                                 {{ child.likes.length ? child.likes.length : '' }}
                                             </span>
-                                            <span style="margin-left: 20px" class="delete-icon" v-if="child.tmdid === $store.state.userInfo.sub" @click="deleteReply(child, no, index)">
+                                            <span style="margin-left: 20px" class="delete-icon" v-if="child.tmdid === $store.state.userInfo.sub" @click="deleteReply(child, no, index, item)">
                                                 <Icon custom="iconfont icon-shanchu" size="17" />
                                                 {{ $t("jyzx.common.delete") }}
                                             </span>
@@ -278,7 +278,10 @@ export default {
             let param = {
                 source: 'course',
                 comid: this.courseNow.id,
-                code: this.courseNow.scope === 'school' ? this.courseNow.school : this.courseNow.teacherId
+                // code: this.courseNow.scope === 'school' ? this.courseNow.school : this.courseNow.teacherId
+            }
+            if(this.courseNow.scope === 'school') {
+                param.code = this.courseNow.school
             }
             if(search) {
                 if(this.keyword) {
@@ -321,6 +324,7 @@ export default {
                 debate: {
                     // 个人课程传课程创建者
                     code: this.courseNow.scope === 'school' ? this.courseNow.school : this.courseNow.teacherId,
+                    scope: this.courseNow.scope,
                     id: this.editId, //编辑传id
                     title: this.topicTitle,
                     userType: this.$store.state.userInfo.scope === 'student' ? 'student' : 'tmdid',
@@ -365,7 +369,8 @@ export default {
                 onOk: () => {
                     let param = {
                         debateId: content.id,
-                        debateCode: this.courseNow.scope === 'school' ? this.courseNow.school : this.courseNow.teacherId
+                        debateCode: this.courseNow.scope === 'school' ? this.courseNow.school : this.courseNow.teacherId,
+                        scope: content.scope,
                     }
                     this.$api.studentWeb.deleteTopic(param).then(res => {
                         if(res.status) {
@@ -383,7 +388,8 @@ export default {
                 this.replyDis = ""
                 let param = {
                     debateId: content.id,
-                    debateCode: this.courseNow.scope === 'school' ? this.courseNow.school : this.courseNow.teacherId
+                    debateCode: this.courseNow.scope === 'school' ? this.courseNow.school : this.courseNow.teacherId,
+                    scope: content.scope
                 }
                 this.$api.studentWeb.findReply(param).then(res => {
                     if(res.debate) {
@@ -409,6 +415,7 @@ export default {
             let param = {
                 debateId: content.id,
                 debateCode: this.courseNow.scope === 'school' ? this.courseNow.school : this.courseNow.teacherId,
+                scope: content.scope,
                 opt: 'add', // 课程id
                 reply: {
                     id: '', //编辑传id
@@ -436,11 +443,12 @@ export default {
             })
         },
         // 点赞
-        likeTopic(content, index) {
+        likeTopic(content, index, contestPar) {
             console.log(content, index);
             let param = {
                 debateId: content.pid,
                 debateCode: this.courseNow.scope === 'school' ? this.courseNow.school : this.courseNow.teacherId,
+                scope: contestPar.scope,
                 opt: content.likeit ? 'unlike' : 'like',
                 likeData: {
                     replyId: content.id,
@@ -462,7 +470,7 @@ export default {
             })
         },
         // 删除回复
-        deleteReply(content, childindex, index) {
+        deleteReply(content, childindex, index, contestPar) {
             this.$Modal.confirm({
                 title: this.$t('jyzx.common.deleteTopic2'),
                 content: this.$t('jyzx.common.deleteTopic3'),
@@ -470,6 +478,7 @@ export default {
                     let param = {
                         debateId: content.pid,
                         debateCode: this.courseNow.scope === 'school' ? this.courseNow.school : this.courseNow.teacherId,
+                        scope: contestPar.scope,
                         opt: 'del', // 课程id
                         replyId: content.id,
                     }

+ 195 - 0
TEAMModelOS/ClientApp/src/components/student-web/EventView/EventContentTypeTemplate/LessonTestReportCharts/AudioRecorder.vue

@@ -0,0 +1,195 @@
+<template>
+    <div>
+        <div class="audio-box" v-if="audioType === 2">
+            <audio controls :key="audioKey" :id="`audioId${audioId}`">
+                <source :src="fileFullPath">
+                {{ $t('teachContent.notAudio') }}
+            </audio>
+            <Icon custom="iconfont icon-shuaxin1" @click="startRecorder(true)" />
+        </div>
+        <div class="audio-recorder">
+            <p v-show="audioType != 2">{{ stuDuration }}</p>
+            <Icon custom="iconfont icon-luyin" v-show="!audioType" @click="startRecorder()" />
+            <Icon custom="iconfont icon-luyin-zanting" v-show="audioType === 1" @click="stopRecorder()" />
+            <!-- <Icon custom="iconfont icon-start" v-show="audioType === 2" /> -->
+        </div>
+    </div>
+</template>
+
+<script>
+import Recorder from 'js-audio-recorder'
+import BlobTool from "@/utils/blobTool.js"
+import { mapGetters } from 'vuex'
+export default {
+    props: {
+        index: {
+            type: Number,
+            default: -1,
+        },
+        textData: {
+            type: Array,
+            default: () => {
+                return []
+            },
+        },
+    },
+    data () {
+        return {
+            recorder: undefined,
+            audioType: 0, //未开始(0) 进行中(1) 停止(2)
+            currentUrl: null,
+            answerUrl: [],
+            fileFullPath: '',
+            sasData: undefined,
+            audioId: '',
+            audioKey: '',
+        }
+    },
+    created () {
+        this.audioId = this.$jsFn.getBtwRandom(0, 100000000)
+        this.recorder = new Recorder({
+            sampleBits: 16,
+            sampleRate: 16000,
+            numChannels: 1,
+            // compiling: false,
+        })
+    },
+    async mounted () {
+        let { scope, cntr } = this.getComposeData
+        this.sasData = scope === 'school' ? await this.$tools.getSchoolSas(cntr) : await this.$api.blob.blobSasRCW({ name: cntr, role: 'teacher' })
+        this.getAnsInfo()
+    },
+    computed: {
+        ...mapGetters([
+            "getComposeData",
+        ]),
+        stuDuration() {
+            if(this.recorder?.duration) {
+                // 分钟
+                let minutes = Math.floor(this.recorder.duration / 60) % 60
+                minutes = minutes >= 10 ? minutes : ("0" + minutes)
+                // 秒数
+                let seconds = Math.floor(this.recorder.duration % 60)
+                seconds = seconds >= 10 ? seconds : ("0" + seconds)
+                return `${minutes}:${seconds}`
+            } else {
+                return '00:00'
+            }
+        },
+    },
+    watch: {
+        index() {
+            this.recorder = undefined
+            this.recorder = new Recorder({
+                sampleBits: 16,
+                sampleRate: 16000,
+                numChannels: 1,
+                // compiling: false,
+            })
+            this.getAnsInfo()
+            deep: true
+            immediate: true
+        }
+    },
+    methods: {
+        getAnsInfo() {
+            this.audioType = 0
+            this.answerUrl = []
+            this.currentUrl = null
+            this.fileFullPath = ''
+            this.audioKey = this.$jsFn.getBtwRandom(0, 10000000000)
+            if(this.textData.length) {
+                let { cntr } = this.getComposeData
+                this.answerUrl = [...this.textData]
+                this.currentUrl = this.textData[0]
+                this.fileFullPath = `${this.sasData.url}/${cntr}${this.currentUrl}${this.sasData.sas}`
+                this.audioType = 2
+            }
+        },
+        startRecorder(isRefresh) {
+            // return
+            Recorder.getPermission().then(() => {
+                if(isRefresh) {
+                    /* this.fileFullPath = ''
+                    document.getElementById(`audioId${this.audioId}`).setAttribute('src', this.fileFullPath) */
+                }
+                this.recorder.start()
+                this.audioType = 1
+            }, error => {
+                console.log('报错:', error);
+                this.$Message.warning('请允许网页使用麦克风')
+            })
+        },
+        playRecorder() {
+            this.recorder.play()
+        },
+        async stopRecorder() {
+            this.recorder.stop()
+            if(!this.recorder || !this.recorder.duration) {
+                this.$Message.warning('请先录音')
+                return
+            }
+            this.audioType = 2
+            // return
+            // this.recorder.downloadWAV() //下载文件
+            // 停止录音后马上上传到blob
+            const blob = this.recorder.getWAVBlob()
+            const newBlob = new Blob([blob], {type: 'audio/wav'})
+            let random = this.$jsFn.getBtwRandom(0, 10)
+            const fileBlob = new File([newBlob], `录音${this.index}(${random}).wav`,{type: 'audio/wav'})
+            
+            let { scope, cntr, examId, subjectId, stuId } = this.getComposeData
+            let sas = '?' + this.sasData.sas
+            let containerClient = new BlobTool(this.sasData.url, this.sasData.name, sas, scope)
+            let path = `exam/${examId}/${subjectId}/${stuId}`
+            let that = this
+            containerClient.upload(fileBlob, {
+                path,
+                checkSize: false
+            }).then(res => {
+                that.currentUrl = res.blob
+                that.fileFullPath = res.url + that.sasData.sas
+                document.getElementById(`audioId${that.audioId}`).setAttribute('src', that.fileFullPath)
+                that.$emit("dataGet", that.currentUrl, that.index)
+                that.$Message.warning(that.$t('cusMgt.saveOk'))
+            }).catch(err => {
+                console.log(err);
+                that.$Message.warning(that.$t('cusMgt.saveErr'))
+                that.fileFullPath = `${that.sasData.url}/${cntr}${that.currentUrl}${that.sasData.sas}`
+            })
+        },
+    }
+}
+</script>
+
+<style lang="less" scoped>
+.audio-recorder {
+    text-align: center;
+    width: 10rem;
+
+    &>p {
+        font-size: 30px;
+    }
+
+    .ivu-icon {
+        font-size: 10rem;
+        cursor: pointer;
+        color: #65af5b;
+    }
+}
+.audio-box {
+    display: flex;
+    align-items: center;
+
+    .ivu-icon {
+        margin-left: 10px;
+        font-size: 3rem;
+        cursor: pointer;
+        color: #888888;
+        &:hover {
+            color: #65af5b;
+        }
+    }
+}
+
+</style>

+ 0 - 0
TEAMModelOS/ClientApp/src/components/student-web/EventView/EventContentTypeTemplate/LessonTestReportCharts/LessonTestReportCharts.vue


Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików