Browse Source

Merge branch 'develop' of http://52.130.252.100:10000/TEAMMODEL/TEAMModelOS into develop

Li 2 years ago
parent
commit
18bfb52dab
24 changed files with 600 additions and 1420 deletions
  1. 26 1
      TEAMModelOS.SDK/DI/AzureSignalR/AzureSignalRExtensions.cs
  2. 9 9
      TEAMModelOS.SDK/DI/AzureSignalR/AzureSignalRFactory.cs
  3. 105 32
      TEAMModelOS.SDK/Models/Service/StudentService.cs
  4. 2 2
      TEAMModelOS/ClientApp/public/lang/en-US.js
  5. 2 2
      TEAMModelOS/ClientApp/public/lang/zh-CN.js
  6. 2 2
      TEAMModelOS/ClientApp/public/lang/zh-TW.js
  7. 1 1
      TEAMModelOS/ClientApp/src/router/routes.js
  8. 25 16
      TEAMModelOS/ClientApp/src/view/classrecord/ClassRecord.less
  9. 194 116
      TEAMModelOS/ClientApp/src/view/classrecord/ClassRecord.vue
  10. 0 417
      TEAMModelOS/ClientApp/src/view/classrecord/ClassRecordNew.less
  11. 0 635
      TEAMModelOS/ClientApp/src/view/classrecord/ClassRecordNew.vue
  12. 2 1
      TEAMModelOS/ClientApp/src/view/classrecord/eventchart/DataCount.vue
  13. 10 2
      TEAMModelOS/ClientApp/src/view/classrecord/eventchart/Receive.vue
  14. 3 2
      TEAMModelOS/ClientApp/src/view/dashboard/Art.less
  15. 8 4
      TEAMModelOS/ClientApp/src/view/dashboard/Index.vue
  16. 98 91
      TEAMModelOS/ClientApp/src/view/dashboard/Research.less
  17. 33 48
      TEAMModelOS/ClientApp/src/view/mycourse/MyCourse.vue
  18. 7 7
      TEAMModelOS/ClientApp/src/view/mycourse/student/Student.vue
  19. 15 0
      TEAMModelOS/Controllers/OpenApi/Business/BizTeacherController.cs
  20. 31 29
      TEAMModelOS/Controllers/OpenApi/IRS/TianboController.cs
  21. 16 0
      TEAMModelOS/Controllers/OpenApi/OpenApiService.cs
  22. 1 1
      TEAMModelOS/Controllers/OpenApi/OpenSchool/ScGroupListController.cs
  23. 8 1
      TEAMModelOS/Controllers/Student/StudentController.cs
  24. 2 1
      TEAMModelOS/appsettings.Development.json

+ 26 - 1
TEAMModelOS.SDK/DI/AzureSignalR/AzureSignalRExtensions.cs

@@ -6,11 +6,36 @@ using System.Threading;
 using System.Threading.Tasks;
 using System.Linq;
 using StackExchange.Redis;
+using System.Collections.Concurrent;
+using Microsoft.Azure.SignalR.Management;
 
 namespace TEAMModelOS.SDK.DI
 {
     public static class AzureSignalRExtensions
     {
-       
+        private static ConcurrentDictionary<string, ServiceHubContext> ServiceHubContexts { get; } = new();
+
+
+
+        /// <summary>
+        /// µo°e«H®§¦Ü¹ï¦C©Î¥DÃD
+        /// </summary>       
+        /// <param name="name">QueueName or TopicName</param>
+        /// <param name="message">°T®§</param>
+        /// <returns></returns>
+        public static ServiceHubContext GetHubContext(this ServiceManager sm, string name)
+        {
+            try
+            {
+                ServiceHubContext hub = ServiceHubContexts.GetOrAdd(name, (x) =>
+                sm.CreateHubContextAsync(name, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult());
+
+                return hub;
+            }
+            catch
+            {
+                throw;
+            }
+        }
     }
 }

+ 9 - 9
TEAMModelOS.SDK/DI/AzureSignalR/AzureSignalRFactory.cs

@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Options;
 using StackExchange.Redis;
 using System;
+using System.Collections.Concurrent;
 
 namespace TEAMModelOS.SDK.DI
 {
@@ -10,6 +11,7 @@ namespace TEAMModelOS.SDK.DI
     {
         private readonly IServiceProvider _services;
         private readonly IOptionsMonitor<AzureSignalRFactoryOptions> _optionsMonitor;
+        private ConcurrentDictionary<string, ServiceManager> ServiceManagers { get; } = new();
         private readonly ILogger _logger;
 
         public AzureSignalRFactory(IServiceProvider services, IOptionsMonitor<AzureSignalRFactoryOptions> optionsMonitor, ILogger<AzureSignalRFactory> logger)
@@ -20,18 +22,16 @@ namespace TEAMModelOS.SDK.DI
         }
 
         public ServiceManager GetServiceManager(string name = "Default")
-        {            
+        {
             try
             {
+                var serviceManager = ServiceManagers.GetOrAdd(name, x =>
                 // TODO(Mickey) : Signalr SDK 1.10.0之後,aud會無法擴充,會變成asrs.u.aud,會造成無法使用restAPI,故先降回舊版
-                var serviceManager = new ServiceManagerBuilder()
-                     .WithOptions(option =>
-                     {
-                         option.ConnectionString = _optionsMonitor.Get(name).SignalRConnectionString;
-                         option.ServiceTransportType = ServiceTransportType.Persistent;
-                         // option.ServiceTransportType = ServiceTransportType.Persistent
-                     })
-                     .BuildServiceManager();
+                new ServiceManagerBuilder().WithOptions(option =>
+                {
+                    option.ConnectionString = _optionsMonitor.Get(name).SignalRConnectionString;
+                    option.ServiceTransportType = ServiceTransportType.Persistent;
+                }).BuildServiceManager());
 
                 return serviceManager;
             }

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

@@ -816,8 +816,7 @@ namespace TEAMModelOS.SDK
                     writerNew.Flush();
                     if (!string.IsNullOrWhiteSpace(stud.Value.imei))
                     {
-                        Imei imei = new Imei { id = stud.Value.imei, code = "Imei", pk = "Imei", stuid = stud.Key, school = schoolId };
-                        await cosmosContainer.UpsertItemAsync(imei, new PartitionKey($"Imei"));
+                        await upsertImei(stud.Key, stud.Value.imei, schoolId, "keep", cosmosContainer);
                     }
                     var response = await cosmosContainer.CreateItemStreamAsync(memoryStream, new PartitionKey($"Base-{schoolId}"));
 
@@ -917,6 +916,90 @@ namespace TEAMModelOS.SDK
             }
             return (null, null, null);
         }
+        /// <summary>
+        /// grand_type 
+        /// 为import:如果导入则已最新,没导入则不动这个字段的值,维持现状
+        /// 为create:如果有值,则绑定,并删除stuid关联的别的imei.
+        /// 为update 
+        /// 为delete
+        /// </summary>
+        /// <param name="stuid"></param>
+        /// <param name="imei"></param>
+        /// <param name="schoolId"></param>
+        /// <param name="grand_type"></param>
+        /// <param name="cosmosContainer"> 
+        /// clean 当传入的电子学生证值为空,判断要强制清除关联的学生电子学生证 ,
+        /// keep 保持现状,导入时
+        /// delete 因学生被删除,强制删除电子学生证。</param>
+        /// <returns></returns>
+        public static async Task upsertImei(string stuid ,string imeiid,string schoolId,string grand_type, CosmosContainer cosmosContainer) {
+            List<Imei> imeis = new List<Imei>();
+            string sql = $"select value c from c where c.stuid='{stuid}'  and c.school='{schoolId}' ";
+            await foreach (var item in cosmosContainer.GetItemQueryIterator<Imei>(queryText: sql, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey("Imei") }))
+            {
+                imeis.Add(item);
+            }
+            switch (grand_type)
+            {
+                case "clean":
+                case "keep":
+                    bool notin = true;
+                    List<Imei> update = new List<Imei>();
+                    List<Imei> delete = new List<Imei>();
+                    imeis.ForEach(x => {
+                        //如果传入的电子学生证id不存在,且是单个更新或创建,则解除学生id的电子学生证绑定,并删除该电子学生证。
+                        if (string.IsNullOrWhiteSpace(imeiid)) {
+                            if (grand_type.Equals("clean") )
+                            {
+                                delete.Add(x);
+                            }// 如果是导入模式,且电子学生证没有值则不动。
+                        }
+                        else {
+                            //如果电子学生证有值则,解除之前的所有与当前电子学生证id不符的绑定。
+                            if (!x.id.Equals(imeiid))
+                            {
+                                x.stuid = null;
+                                x.school = null;
+                                delete.Add(x);
+                            }
+                            else
+                            {
+                                //存在且正确绑定
+                                notin = false;
+                            }
+                        }
+                        
+                    });
+                    //当前学生还未有任何电子学生证,且有新的电子学生证id传入,则需要捞出电子学生证,绑定,如果没有捞出,则新建电子学生证,并绑定
+                    if (notin && !string.IsNullOrWhiteSpace(imeiid))
+                    {
+                        var imeiResponse = await cosmosContainer.ReadItemStreamAsync(imeiid, new PartitionKey("Imei"));
+                        if (imeiResponse.Status == 200)
+                        {
+                            Imei imeiDb = JsonDocument.Parse(imeiResponse.Content).RootElement.Deserialize<Imei>();
+                            imeiDb.stuid = stuid;
+                            imeiDb.school = schoolId;
+                            await cosmosContainer.ReplaceItemAsync(imeiDb, imeiDb.id, new PartitionKey("Imei"));
+                        }
+                        else
+                        {
+                            Imei imei = new Imei { id = imeiid, code = "Imei", pk = "Imei", stuid = stuid, school = schoolId };
+                            await cosmosContainer.CreateItemAsync(imei, new PartitionKey($"Imei"));
+                        };
+                    }
+                    if (delete.Any())
+                    {
+                        await cosmosContainer.DeleteItemsStreamAsync(delete.Select(x => x.id).ToList(), "Imei");
+                    }
+                    break;
+                case "delete":
+                    if (imeis.Any()) {
+                        await cosmosContainer.DeleteItemsStreamAsync(imeis.Select(x => x.id).ToList(), "Imei");
+                    }
+                    break;
+            }
+            
+        }
 
         /// <summary>
         /// 單純建立單一學生
@@ -993,17 +1076,15 @@ namespace TEAMModelOS.SDK
                 }
                 writer.WriteEndObject();
                 writer.Flush();
-                if (!string.IsNullOrWhiteSpace(studCreateInfo.imei))
-                {
-                    Imei imei = new Imei { id = studCreateInfo.imei, code = "Imei", pk = "Imei", stuid = studCreateInfo.id, school = schoolId };
-                    await _azureCosmos
-                                .GetCosmosClient()
-                                .GetContainer(Constant.TEAMModelOS, "Student").UpsertItemAsync(imei, new PartitionKey($"Imei"));
-                }
+                
                 var response = await _azureCosmos
                                 .GetCosmosClient()
                                 .GetContainer(Constant.TEAMModelOS, "Student")
                                 .CreateItemStreamAsync(stream, new PartitionKey($"Base-{schoolId}"));
+                //更新电子学生证、
+                await upsertImei(studCreateInfo.id, studCreateInfo.imei, schoolId, "keep", _azureCosmos
+                               .GetCosmosClient()
+                               .GetContainer(Constant.TEAMModelOS, "Student"));
                 if (response.Status == (int)HttpStatusCode.Created || response.Status == (int)HttpStatusCode.OK) return true;
                 if (response.Status == (int)HttpStatusCode.Conflict) return false;
                 else
@@ -1011,6 +1092,7 @@ namespace TEAMModelOS.SDK
                     await _dingDing.SendBotMsg($"IES5,{_option.Location},StudentController/createStudent()\nCosmosDB Create response status = {response.Status}\nID:{studCreateInfo.id}", GroupNames.醍摩豆服務運維群組);
                     return false;
                 }
+                
             }
             catch (Exception ex)
             {
@@ -1488,9 +1570,11 @@ namespace TEAMModelOS.SDK
                     string id = string.Empty;
                     try
                     {
+                       
                         JsonElement student = students.Current;
                         id = student.GetProperty("id").GetString();
                         var ret = await container.DeleteItemStreamAsync(id, new PartitionKey($"Base-{schoolId}"));
+                        await upsertImei(id, null, schoolId, "delete", container);
                         if (ret.Status == (int)HttpStatusCode.NoContent) sucIds.Add(id);
                         await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, "School").GetItemQueryIterator<GroupList>(queryText: $"select value(c) from c join A0 in c.members where A0.id = '{id}' and A0.code='{schoolId}' ", requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"GroupList-{schoolId}") }))
                         {
@@ -1751,7 +1835,7 @@ namespace TEAMModelOS.SDK
         /// <param name="students"></param>
         /// <returns></returns>
         public static async Task<(List<object> studs, Dictionary<string, List<string>> classDuplNos, List<string> nonexistentIds, List<string> errorIds, Dictionary<string, List<string>> errorNos, List<string> errorClassId)>
-            updateStudents(AzureCosmosFactory _azureCosmos, DingDing _dingDing, Option _option, string schoolId, JsonElement.ArrayEnumerator students)
+            updateStudents(AzureCosmosFactory _azureCosmos, DingDing _dingDing, Option _option, string schoolId, JsonElement.ArrayEnumerator students,bool cleanImei)
         {
             try
             {
@@ -2091,23 +2175,7 @@ namespace TEAMModelOS.SDK
                                                     writer.WriteString("irs", studentInfos[id].irs);
                                                 }
                                                 break;
-                                            case bool _ when element.Name.Equals("imei", StringComparison.Ordinal):
-                                                //移除座號的話會給空的
-                                                if (studentInfos[id].imei != null && studentInfos[id].imei.Length == 0)
-                                                {
-                                                    writer.WriteNull("imei");
-                                                    tmpData.imei = null;
-                                                }
-                                                else if (string.IsNullOrWhiteSpace(studentInfos[id].imei))
-                                                {
-                                                    element.WriteTo(writer);
-                                                    tmpData.imei = element.Value.GetString();
-                                                }
-                                                else
-                                                {
-                                                    writer.WriteString("imei", studentInfos[id].imei);
-                                                }
-                                                break;
+                                            
                                             case bool _ when element.Name.StartsWith("_", StringComparison.Ordinal):
                                                 break;
                                             default:
@@ -2158,12 +2226,17 @@ namespace TEAMModelOS.SDK
                                     
                                     writer.WriteEndObject();
                                     writer.Flush();
-                                    if (!string.IsNullOrWhiteSpace(studentInfos[id].imei))
+                                    //编辑是是否要明确清除电子学生证。
+                                    if (cleanImei)
                                     {
-                                        Imei imei = new Imei { id = studentInfos[id].imei, code = "Imei", pk = "Imei", stuid = id, school = schoolId };
-                                        await _azureCosmos
-                                                    .GetCosmosClient()
-                                                    .GetContainer(Constant.TEAMModelOS, "Student").UpsertItemAsync(imei, new PartitionKey($"Imei"));
+                                        await upsertImei(id, studentInfos[id].imei, schoolId, "clean", _azureCosmos
+                                                         .GetCosmosClient()
+                                                         .GetContainer(Constant.TEAMModelOS, "Student"));
+                                    }
+                                    else {
+                                        await upsertImei(id, studentInfos[id].imei, schoolId, "keep", _azureCosmos
+                                                       .GetCosmosClient()
+                                                       .GetContainer(Constant.TEAMModelOS, "Student"));
                                     }
                                     try
                                     {

+ 2 - 2
TEAMModelOS/ClientApp/public/lang/en-US.js

@@ -1147,7 +1147,7 @@ const LANG_EN_US = {
             gCount: 'Group No.',
             taskCount: 'Task No.',
             colctCount: 'Work No.',
-            pushCount: 'Push Time',
+            pushCount: '推送資源數',
             totalScore: 'Total Points',
             examCount: 'No. of Test',
             quCount: 'IRS Question No.',
@@ -2818,7 +2818,7 @@ const LANG_EN_US = {
         totalPoint: 'Total Points',
         collateTaskCount: 'Task No.',
         collateCount: 'Work No.',
-        pushCount: 'Push No.',
+        pushCount: '推送資源數',
         score: 'Interactive Score',
         interactionCount: 'Interactive Qs',
         clientInteractionCount: 'Response',

+ 2 - 2
TEAMModelOS/ClientApp/public/lang/zh-CN.js

@@ -1147,7 +1147,7 @@ const LANG_ZH_CN = {
             gCount: '小组数',
             taskCount: '任务总数',
             colctCount: '作品总数',
-            pushCount: '推送数',
+            pushCount: '推送资源数',
             totalScore: '总计分',
             examCount: '测验总数',
             quCount: '互动题数',
@@ -2818,7 +2818,7 @@ const LANG_ZH_CN = {
         totalPoint: '总记分',
         collateTaskCount: '任务总数',
         collateCount: '作品总数',
-        pushCount: '推送数',
+        pushCount: '推送资源数',
         score: '总互动分',
         interactionCount: '互动题数',
         clientInteractionCount: '互动总数',

+ 2 - 2
TEAMModelOS/ClientApp/public/lang/zh-TW.js

@@ -1147,7 +1147,7 @@ const LANG_ZH_TW = {
             gCount: '小組數',
             taskCount: '任務總數',
             colctCount: '作品總數',
-            pushCount: '推送數',
+            pushCount: '推送資源數',
             totalScore: '總計分',
             examCount: '測驗總數',
             quCount: '互動題數',
@@ -2818,7 +2818,7 @@ const LANG_ZH_TW = {
         totalPoint: '總記分',
         collateTaskCount: '任務總數',
         collateCount: '作品總數',
-        pushCount: '推送數',
+        pushCount: '推送資源數',
         score: '總互動分',
         interactionCount: '互動題數',
         clientInteractionCount: '互動總數',

+ 1 - 1
TEAMModelOS/ClientApp/src/router/routes.js

@@ -1252,7 +1252,7 @@ export const routes = [{
 	{
 		path: 'classRecord',
 		name: 'classRecord',
-		component: resolve => require(['@/view/classrecord/ClassRecordNew.vue'], resolve),
+		component: resolve => require(['@/view/classrecord/ClassRecord.vue'], resolve),
 		meta: {
 			activeName: 'myCourse'
 		}

+ 25 - 16
TEAMModelOS/ClientApp/src/view/classrecord/ClassRecord.less

@@ -9,7 +9,7 @@
 
 .class-record-container {
     width: 100%;
-    background: #f2f2f2;
+    background: #ffffff;
     height: 100%;
 }
 
@@ -41,12 +41,9 @@
 }
 
 .class-content {
-    width: ~"calc(100% - 20px)";
-    display: flex;
-    flex-direction: row;
-    height: 450px;
+    width: 100%;
     margin-left: 10px;
-    margin-top: 15px;
+    margin-top: 7px;
     position: relative;
     &:hover .cus-page-wrap {
         opacity: 1;
@@ -63,18 +60,19 @@
 }
 
 .video-player-box {
-    width: 50%;
+    width: ~"calc(100% - 15px)";
     box-shadow: 0px 0px 10px 2px #d8d8d8;
-    height: 450px;
+    max-height: 450px;
     display: flex;
     position: relative;
     background: #fff;
 }
 .courseware-wrap {
-    width: ~"calc(50% - 15px)";
+    width: ~"calc(100% - 15px)";
     margin-right: 15px;
     position: relative;
-    height: 450px;
+    max-height: 450px;
+    margin-top: 10px;
     background: #fff;
     box-shadow: 0px 0px 10px 2px #d8d8d8;
 
@@ -112,19 +110,20 @@
     height: 36px;
     background: rgba(43, 51, 63, 0.7);
     text-align: center;
-    opacity: 0;
+    opacity: 1;
     transition: opacity 1s;
     border-right: 2px solid black;
 }
 
 .content-title {
-    border-left: 8px solid #1CC0F3;
-    padding-left: 14px;
+    border-left: 4px solid #1CC0F3;
+    padding-left: 4px;
     line-height: 20px;
     font-size: 20px;
-    margin-top: 25px;
+    margin-top: 10px;
     margin-bottom: 6px;
-    width: 100%;
+    margin-left: 10px;
+    width: fit-content;
 }
 .e-note-tag{
     font-size: 14px;
@@ -178,6 +177,7 @@
     border: 1px solid #f0f0f0;
     margin-bottom: 10px;
     position: relative;
+    background: #f9f9f9;
     min-height: 300px;
 }
 
@@ -250,7 +250,7 @@
 .page-item {
     display: flex;
     position: relative;
-    background: white;
+    // background: white;
     padding: 10px 70px 10px 20px;
     .record-data-wrap {
         width: 100%;
@@ -405,4 +405,13 @@
     left: 0px;
     color: #ff9900;
     width: 100%;
+}
+.rcd-split-pane{
+    height: ~"calc(100% - 60px)";
+}
+.top-wrap{
+    width: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
 }

+ 194 - 116
TEAMModelOS/ClientApp/src/view/classrecord/ClassRecord.vue

@@ -1,130 +1,132 @@
 <template>
     <div class="class-record-container">
-        <vuescroll ref="pagewrap">
-            <div>
-                <!--头部信息-->
-                <div class="class-record-header">
-                    <span class="back-page" @click="goBack">
-                        <Icon type="md-arrow-round-back" />
-                        {{$t('cusMgt.rcd.rtn')}}
-                    </span>
-                    <span class="course-name">
-                        {{recordInfo.name}}
-                    </span>
-                    <div style="display:inline-block;margin-left:30px">
-                        <span class="label-text">
-                            {{$t('cusMgt.rcd.ctime')}}
-                        </span>
-                        <span class="label-value">
-                            {{$jsFn.timeFormat(recordInfo.startTime)}}
-                        </span>
-                    </div>
-                    <div class="action-wrap">
-                        <!-- 电子笔记 -->
-                        <span class="e-note-tag" @click="viewENote">
-                            <Icon type="ios-paper" />
-                            {{$t('cusMgt.rcd.enote')}}
-                        </span>
-                        <!-- 苏格拉底报告 -->
-                        <!-- <span class="e-note-tag" @click="viewReport">
-                            {{$t('cusMgt.rcd.sokrateRpt')}}
-                        </span> -->
-                        <!-- 表格下载 -->
-                        <span class="e-note-tag" @click="downloadData">
-                            <Icon type="md-download" />
-                            {{$t('cusMgt.rcd.exportData')}}
-                        </span>
-                        <!-- 数据统计 -->
-                        <span class="e-note-tag" v-show="hasVideo" @click="isShowVd = !isShowVd">
-                            <Icon :type="isShowVd ? 'md-podium' : 'logo-youtube'" />
-                            {{isShowVd ? $t('cusMgt.rcd.dataCount') : $t('cusMgt.rcd.videoData')}}
-                        </span>
-                    </div>
-                </div>
+        <!--头部信息-->
+        <div class="class-record-header">
+            <span class="back-page" @click="goBack">
+                <Icon type="md-arrow-round-back" />
+                {{$t('cusMgt.rcd.rtn')}}
+            </span>
+            <span class="course-name">
+                {{recordInfo.name}}
+            </span>
+            <div style="display:inline-block;margin-left:30px">
+                <span class="label-text">
+                    {{$t('cusMgt.rcd.ctime')}}
+                </span>
+                <span class="label-value">
+                    {{$jsFn.timeFormat(recordInfo.startTime)}}
+                </span>
+            </div>
+            <div class="action-wrap">
+                <!-- 下载学生作品 -->
+                <span class="e-note-tag" @click="downloadStuWork">
+                    <Icon type="md-download" />
+                    {{$t('cusMgt.rcd.dlStuWrk')}}
+                </span>
+                <!-- 电子笔记 -->
+                <span class="e-note-tag" @click="viewENote">
+                    <Icon type="ios-paper" />
+                    {{$t('cusMgt.rcd.enote')}}
+                </span>
+                <!-- 表格下载 -->
+                <span class="e-note-tag" @click="downloadData">
+                    <Icon type="md-download" />
+                    {{$t('cusMgt.rcd.exportData')}}
+                </span>
+                <!-- 数据统计 -->
+                <span class="e-note-tag" v-show="hasVideo" @click="isShowVd = !isShowVd">
+                    <Icon :type="isShowVd ? 'md-podium' : 'logo-youtube'" />
+                    {{isShowVd ? $t('cusMgt.rcd.dataCount') : $t('cusMgt.rcd.videoData')}}
+                </span>
+            </div>
+        </div>
+        <Split v-model="split1">
+            <div slot="left" class="rcd-split-pane">
                 <!--上课内容-->
-                <div :class="isShowBar ? 'class-content mouse-over-status':'class-content'" @mousemove="isShowBar = true" @mouseleave="isShowBar = false">
-                    <!-- 暂时没有HTEX数据 -->
-                    <!-- <div class="courseware-wrap">
-                        <MyHTEXRender :page="curPage" isInner @onPageChange="getCurHTEX" url="https://teammodelstorage.blob.core.chinacloudapi.cn/0-public/testdata/%E3%80%8A%E5%9B%9B%E8%BE%B9%E5%BD%A2%E3%80%8B%E8%AF%BE%E4%BB%B6%20-%20TEST/index.json"></MyHTEXRender>
-                    </div> -->
-                    <div class="courseware-wrap">
-                        <img :src="curImg" alt="" class="course-cur-img">
-                        <div class="cus-page-wrap">
-                            <Page :total="pageList.length" :current.sync="curPage" :page-size="1" size="small" @on-change="getCurHTEX" />
+                <vuescroll>
+                    <!-- <div :class="isShowBar ? 'class-content mouse-over-status':'class-content mouse-over-status'" @mousemove="isShowBar = true" @mouseleave="isShowBar = false"> -->
+                    <div class="class-content mouse-over-status">
+                        <video-player2 v-if="hasVideo" v-show="isShowVd" @on-vd-error="videoError" class="video-player-box" :markers="markers" ref="videoPlayer" :options="playerOptions" :playsinline="true" @getCurPage="getCurPage">
+                        </video-player2>
+                        <div v-show="!isShowVd" class="video-player-box" style="padding:25px 0px">
+                            <Alert v-show="!hasVideo" class="no-video-tips" type="warning" show-icon>
+                                {{$t('cusMgt.rcd.noVideo')}}
+                            </Alert>
+                            <DataCount :rcdInfo="recordInfo"></DataCount>
                         </div>
+                        <Carousel class="courseware-wrap" loop>
+                            <CarouselItem v-for="(item) in pageList" :key="item.id">
+                                <img :src="item.img" alt="" class="course-cur-img">
+                            </CarouselItem>
+                        </Carousel>
+                        <!-- <div class="courseware-wrap">
+                            <img :src="curImg" alt="" class="course-cur-img">
+                            <div class="cus-page-wrap">
+                                <Page :total="pageList.length" :current.sync="curPage" :page-size="1" size="small" @on-change="getCurHTEX" />
+                            </div>
+                        </div> -->
                     </div>
-                    <video-player2 v-if="hasVideo" v-show="isShowVd" @on-vd-error="videoError" class="video-player-box" :markers="markers" ref="videoPlayer" :options="playerOptions" :playsinline="true" @getCurPage="getCurPage">
-                    </video-player2>
-                    <div v-show="!isShowVd" class="video-player-box" style="padding:25px 0px">
-                        <Alert v-show="!hasVideo" class="no-video-tips" type="warning" show-icon>
-                            {{$t('cusMgt.rcd.noVideo')}}
-                        </Alert>
-                        <DataCount :rcdInfo="recordInfo"></DataCount>
-                    </div>
-                </div>
-
-                <div class="cus-data-wrap">
-                    <!--课堂互动记录-->
+                </vuescroll>
+            </div>
+            <div slot="right" class="rcd-split-pane">
+                <!--课堂互动记录-->
+                <div class="top-wrap">
                     <h2 class="content-title">
                         {{$t('cusMgt.rcd.rcdLabel')}}
                     </h2>
-                    <div class="interaction-record-wrap">
-                        <!-- <div class="interaction-type-wrap">
-                            <span style="border-bottom:5px solid #efefef;display:block; margin-bottom:10px;padding-top:5px;">
-                                <Icon size="22" :class="typeIndex == 'all' ? 'type-icon-active type-icon':'type-icon'" custom="iconfont icon-basic-setting" @click="filterType('all')" :title="$t('cusMgt.rcd.allRcd')" />
-                            </span>
-                            <span :class="typeIndex == 'irs' ? 'type-icon-active type-icon':'type-icon'" style="font-size:32px;line-height:30px;" @click="filterType('irs')" :title="$t('cusMgt.rcd.qustion')">Q</span>
-                            <Icon :class="typeIndex == 'msg' ? 'type-icon-active type-icon':'type-icon'" custom="iconfont icon-message" style="font-size:24px;" @click="filterType('msg')" :title="$t('cusMgt.rcd.message')" />
-                            <Icon :class="typeIndex == 'img' ? 'type-icon-active type-icon':'type-icon'" type="ios-image" @click="filterType('img')" :title="$t('cusMgt.rcd.image')" />
-                            <Icon :class="typeIndex == 'file' ? 'type-icon-active type-icon':'type-icon'" type="md-document" @click="filterType('file')" :title="$t('cusMgt.rcd.file')" />
-                            <Icon :class="typeIndex == 'link' ? 'type-icon-active type-icon':'type-icon'" type="ios-link" @click="filterType('link')" :title="$t('cusMgt.rcd.link')" />
-                        </div> -->
-                        <!-- <vuescroll ref="datawrap"> -->
-                        <template v-for="(item,index) in pageList">
-                            <div v-if="item.pageData && item.pageData.length" class="page-item" :key="index" :id="'page'+(item.page)">
-                                <div class="page-info-wrap">
-                                    <span class="page-tag" @click="toVideo(index+1,$event)" :ref="'page'+(index+1)">
-                                        {{`${$t('cusMgt.rcd.cw')}${item.page}${$t('cusMgt.rcd.page')}`}}
-                                    </span>
-                                    <!-- 课件缩略图 -->
-                                    <!-- <img class="page-mini-img" @click="openViewer(item.img)" :src="item.img" /> -->
-                                    <!-- <Timeline style="margin-top:10px;margin-left:-5px">
+                    <div style="margin-top:5px;margin-right:3px">
+                        <RadioGroup v-model="filterType" size="small" @on-change="handleFilter">
+                            <Radio v-for="item in filterInte" :key="item.value" :label="item.value" border>
+                                {{item.text}}
+                                {{item.value == 'all' ? '' : `(${item.count})`}}
+                            </Radio>
+                        </RadioGroup>
+                    </div>
+                </div>
+                <vuescroll ref="pagewrap">
+                    <div class="cus-data-wrap">
+                        <div class="interaction-record-wrap">
+                            <template v-for="(item,index) in pageListShow">
+                                <div v-if="item.pageData && item.pageData.length" class="page-item" :key="index" :id="'page'+(item.page)">
+                                    <div class="page-info-wrap">
+                                        <span class="page-tag" @click="toVideo(index+1,$event)" :ref="'page'+(index+1)">
+                                            {{`${$t('cusMgt.rcd.cw')}${item.page}${$t('cusMgt.rcd.page')}`}}
+                                        </span>
+                                        <!-- 课件缩略图 -->
+                                        <img class="page-mini-img" @click="openViewer(item.img)" :src="item.img" />
+                                        <!-- <Timeline style="margin-top:10px;margin-left:-5px">
                                             <TimelineItem v-for="(rtItem, rtIndex) in item.pageData" :key="rtIndex +''+index">
                                                 <Icon type="md-arrow-dropright" slot="dot" />
                                                 <p class="event-tag" @click="toEvent(rtItem)">{{rtItem.eventName}}</p>
                                             </TimelineItem>
                                         </Timeline> -->
-                                </div>
-                                <div class="record-data-wrap">
-                                    <!-- <EmptyData v-if="!item.pageData.length" :textContent="$t('cusMgt.rcd.pageNoRcd')"></EmptyData> -->
-                                    <!-- <p v-if="!item.pageData.length" style="color:#c0c0c0;padding-top:4px">
-                                        {{$t('cusMgt.rcd.pageNoRcd')}}
-                                    </p> -->
-                                    <!-- <template v-else> -->
-                                    <!-- 互动数据 -->
-                                    <div v-for="event in item.pageData" :key="event.Time">
-                                        <!-- 即问即答 -->
-                                        <PopQues class="event-item" v-if="event.Event === 'PopQuesLoad' || event.Event === 'ReAtmpAnsStrt'" :evtType="event.Event" :irsData="event.data"></PopQues>
-                                        <!-- 抢权 -->
-                                        <Buzr class="event-item student-event" v-else-if="event.Event === 'BuzrAns'" :buzrData="event.data" :students="baseData.student"></Buzr>
-                                        <!-- 推送 -->
-                                        <Push class="event-item" v-else-if="event.Event === 'FastPgPush'" :pushData="event.data"></Push>
-                                        <!-- 作品收集 -->
-                                        <Receive :recordInfo="recordInfo" class="student-event event-item" v-else-if="event.Event === 'WrkSpaceLoad'" :rcvData="event.data" :students="baseData.student"></Receive>
-                                        <!-- 随机挑人 -->
-                                        <Pick class="event-item student-event" :pickData="event.data" v-else-if="event.Event === 'PickupResult'" :students="baseData.student"></Pick>
-                                        <!-- 课中评测 -->
-                                        <Exam class="event-item" :examInfo="event.data" :recordInfo="recordInfo" v-else-if="event.Event === 'SPQStrt'"></Exam>
                                     </div>
-                                    <!-- </template> -->
+                                    <div class="record-data-wrap">
+                                        <!-- 互动数据 -->
+                                        <div v-for="event in item.pageData" :key="event.Time">
+                                            <!-- 即问即答 -->
+                                            <PopQues class="event-item" v-if="event.Event === 'PopQuesLoad' || event.Event === 'ReAtmpAnsStrt'" :evtType="event.Event" :irsData="event.data"></PopQues>
+                                            <!-- 抢权 -->
+                                            <Buzr class="event-item student-event" v-else-if="event.Event === 'BuzrAns'" :buzrData="event.data" :students="baseData.student"></Buzr>
+                                            <!-- 推送 -->
+                                            <Push class="event-item" v-else-if="event.Event === 'FastPgPush'" :pushData="event.data"></Push>
+                                            <!-- 作品收集 -->
+                                            <Receive :recordInfo="recordInfo" class="student-event event-item" v-else-if="event.Event === 'WrkSpaceLoad'" :rcvData="event.data" :students="baseData.student"></Receive>
+                                            <!-- 作品回帖 -->
+                                            <WrkCmp :recordInfo="recordInfo" class="student-event event-item" v-else-if="event.Event === 'WrkCmp'" :cmpData="event.data" :students="baseData.student"></WrkCmp>
+                                            <!-- 随机挑人 -->
+                                            <Pick class="event-item student-event" :pickData="event.data" v-else-if="event.Event === 'PickupResult'" :students="baseData.student"></Pick>
+                                            <!-- 课中评测 -->
+                                            <Exam class="event-item" :examInfo="event.data" :recordInfo="recordInfo" v-else-if="event.Event === 'SPQStrt'"></Exam>
+                                        </div>
+                                    </div>
                                 </div>
-                            </div>
-                        </template>
-                        <!-- </vuescroll> -->
+                            </template>
+                        </div>
                     </div>
-                </div>
+                </vuescroll>
             </div>
-        </vuescroll>
+        </Split>
         <div v-if="openImageViewer" class="image-viewer" @click="closeViewer()">
             <Icon type="md-close" class="close-icon" @click="closeViewer()" />
             <img :src="viewUrl" class="animated fadeIn" @click.stop />
@@ -134,6 +136,7 @@
     </div>
 </template>
 <script>
+import WrkCmp from './eventchart/WrkCmp.vue'
 import PopQues from './eventchart/PopQues.vue'
 import Buzr from './eventchart/Buzr.vue'
 import Pick from './eventchart/Pick.vue'
@@ -142,12 +145,41 @@ import Exam from './eventchart/Exam.vue'
 import Receive from './eventchart/Receive.vue'
 import DataCount from './eventchart/DataCount.vue'
 import CountTo from 'vue-count-to'
+import BlobTool from '@/utils/blobTool.js'
 export default {
     components: {
-        PopQues, Pick, Push, Receive, DataCount, CountTo, Buzr, Exam
+        PopQues, Pick, Push, Receive, DataCount, CountTo, Buzr, Exam, WrkCmp
     },
     data() {
         return {
+            filterType: 'all',
+            filterInte: [
+                {
+                    value: 'all',
+                    text: this.$t('cusMgt.rcd.filter1'),
+                },
+                {
+                    value: 'FastPgPush',
+                    text: this.$t('cusMgt.rcd.filter2'),
+                    count: 0
+                },
+                {
+                    value: 'WrkSpaceLoad',
+                    text: this.$t('cusMgt.rcd.filter3'),
+                    count: 0
+                },
+                {
+                    value: 'Other',
+                    text: this.$t('cusMgt.rcd.filter4'),
+                    count: 0
+                },
+                {
+                    value: 'SPQStrt',
+                    text: this.$t('cusMgt.rcd.filter5'),
+                    count: 0
+                }
+            ],
+            split1: 0.4,
             backPage: undefined,
             baseData: {}, //base.json
             pushData: [],//push.json
@@ -163,6 +195,7 @@ export default {
             pageIds: [],
             pageEvents: [],
             pageList: [],
+            pageListShow: [],
             videoUrl: '',
             videoPoster: '',
             recordInfo: {},
@@ -200,6 +233,11 @@ export default {
         }
     },
     methods: {
+        // 下载学生作品
+        downloadStuWork() {
+            const containerClient = BlobTool.CreateBlobTool(this.recordInfo.scope)
+            containerClient.downloadFolder(`records/${this.recordInfo.id}/Clients`, this.$t('cusMgt.rcd.stuWrk'))
+        },
         //下载统计表格
         downloadData() {
             let blobInfo = this.recordInfo.scope === 'school' ? this.$store.state.user.schoolProfile : this.$store.state.user.userProfile
@@ -313,9 +351,9 @@ export default {
                 this.baseUrl = {}
             }
             let pgids = this.pageIds
-
             //这里需要判断录制开始的pageid
-            let startInfo = this.pageEvents.findLast(item => item.Event === 'EzsStartRecord')
+            // let startInfo = this.pageEvents?.findLast(item => item.Event === 'EzsStartRecord')
+            let startInfo = this._.findLast(this.pageEvents, item => item.Event === 'EzsStartRecord') //排查兼容性问题
             let startId = startInfo ? startInfo.Pgid : ''
             let startIndex = 0
             if (startId) {
@@ -345,18 +383,23 @@ export default {
                     switch (rlt) {
                         case 'irs':
                             e.data = this.irsData.find(i => i.pageID == e.Pgid)
+                            this.filterInte[3].count++
                             break
                         case 'push':
                             e.data = this.pushData.find(p => p.pageId == e.Pgid || p.pageID == e.Pgid) //数据兼容 pageId 有些大写有些小写
+                            this.filterInte[1].count++
                             break
                         case 'task':
                             e.data = this.taskData.find(t => t.pageID == e.Pgid)
+                            this.filterInte[2].count++
                             break
                         case 'timeline':
                             e.data = this._.cloneDeep(e)
+                            this.filterInte[3].count++
                             break
                         case 'exam':
                             e.data = this._.cloneDeep(e)
+                            this.filterInte[4].count++
                             break
                         default:
                             break
@@ -365,6 +408,15 @@ export default {
                 this.pageList.push(page)
             })
             console.log(this.pageList)
+            this.pageListShow = this.pageList
+            console.log('互动数据', this.pageList)
+            this.pageList.forEach((pageInfo, pageIndex) => {
+                pageInfo.pageData.forEach(e => {
+                    if (e.Event == 'WrkCmp') {
+                        console.log(pageIndex, e)
+                    }
+                })
+            })
             let pageEvent = this.pageEvents.filter(item => item.Event === 'PgJump' || item.Event === 'PgAdd' || item.Event === 'DiscussStart')
             let page = this.markers.length
             pageEvent.forEach((item, index) => {
@@ -457,8 +509,27 @@ export default {
             if (pageInfo) this.$refs.videoPlayer.player.currentTime(pageInfo.time)
         },
         //互动类型筛选
-        filterType(type) {
-            this.typeIndex = type
+        handleFilter() {
+            if (this.filterType == 'all') {
+                this.pageListShow = this.pageList
+            } else {
+                let data = this._.cloneDeep(this.pageList)
+                let events = ['FastPgPush', 'WrkSpaceLoad', 'SPQStrt',]
+                this.pageListShow = data.map(item => {
+                    if (item.pageData.length) {
+                        item.pageData = item.pageData.filter(event => {
+                            if (this.filterType == 'Other') {
+                                return !events.includes(event.Event)
+                            } else {
+                                return event.Event == this.filterType
+                            }
+                        })
+
+                    }
+                    return item
+                })
+            }
+
         },
         //查看图片
         openViewer(url) {
@@ -493,7 +564,7 @@ export default {
         this.hiTeachEvent = this.$GLOBAL.HI_TEACH_EVENT()
         //正式站先暂时不放出课中评测
         // if (this.$store.state.config.srvAdrType == 'product') {
-        //     this.$delete(this.hiTeachEvent,'SPQStrt')
+        //     this.$delete(this.hiTeachEvent, 'SPQStrt')
         // }
         this.events = Object.keys(this.hiTeachEvent)
         this.fnEvents = this.events.filter(key => this.hiTeachEvent[key].type === 'fn')
@@ -529,6 +600,13 @@ export default {
 @import "./ClassRecord.less";
 </style>
 <style>
+.top-wrap .ivu-radio-wrapper-checked {
+    color: white;
+    background: #2d8cf0;
+}
+.top-wrap .ivu-radio {
+    display: none;
+}
 .class-content .vjs-progress-holder {
     font-size: 10px !important;
 }

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

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

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

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

+ 2 - 1
TEAMModelOS/ClientApp/src/view/classrecord/eventchart/DataCount.vue

@@ -116,7 +116,8 @@ export default {
     padding: 20px 0px;
 }
 .data-item {
-    width: 150px;
+    min-width: 19%;
+    width: fit-content;
     padding: 10px;
     text-align: center;
 }

+ 10 - 2
TEAMModelOS/ClientApp/src/view/classrecord/eventchart/Receive.vue

@@ -43,7 +43,7 @@
                 {{ item.isGroupItem ? item.groupID : item.sName}}
             </p>
         </div>
-        <StudentClient></StudentClient>
+        <StudentClient class="receive-student"></StudentClient>
         <!--文件预览-->
         <div v-if="previewStatus" class="image-viewer">
             <div style="width:fit-content;position:relative;margin:auto;">
@@ -184,7 +184,7 @@ export default {
 }
 </script>
 <style lang="less" scoped>
-.name-text{
+.name-text {
     color: #2d8cf0;
 }
 .image-viewer {
@@ -214,7 +214,15 @@ export default {
     }
 }
 .receive-wrap {
+    position: relative;
+    padding-right: 45px !important;
     display: flex;
+    flex-wrap: wrap;
+}
+.receive-student {
+    position: absolute;
+    right: 0px;
+    top: 5px;
 }
 .receive-item {
     margin-right: 20px;

+ 3 - 2
TEAMModelOS/ClientApp/src/view/dashboard/Art.less

@@ -22,10 +22,11 @@
     cursor: pointer;
 
     .time-text {
-      font-size: 20px;
+      font-size: 30px;
       margin-right: 20px;
-      font-weight: 600;
+      font-weight: 200;
       color: #9f9f9f;
+      font-family: 'yjsz';
     }
   }
 

File diff suppressed because it is too large
+ 8 - 4
TEAMModelOS/ClientApp/src/view/dashboard/Index.vue


+ 98 - 91
TEAMModelOS/ClientApp/src/view/dashboard/Research.less

@@ -6,60 +6,62 @@
   // transform-origin: left top;
   overflow: hidden;
   position: relative;
-  
-  .tools{
-	  position: absolute;
-	  right: 30px;
-	  top: 30px;
-	  font-size: 26px;
-	  font-weight: bold;
-	  cursor: pointer;
-	  
-	  .time-text{
-		  font-size: 20px;
-		  margin-right: 20px;
-		  font-weight: 600;
-		  color: #9f9f9f;
-	  }
+
+  .tools {
+    position: absolute;
+    right: 30px;
+    top: 30px;
+    font-size: 26px;
+    font-weight: bold;
+    cursor: pointer;
+
+    .time-text {
+      font-size: 30px;
+      margin-right: 20px;
+      font-weight: 200;
+      color: #9f9f9f;
+      font-family: 'yjsz';
+    }
   }
-  .school-info{
-	  position: absolute;
-	  left: 30px;
-	  top: 40px;
-	  font-size: 20px;
-	  font-weight: bold;
-	  cursor: pointer;
-	  display: flex;
-	  align-items: center;
-	  
-	  &-name{
-		  margin: 0 10px;
-	  }
-	  
-	  &-period{
-		  font-size: 12px;
-		  background-color: #2d2d2d;
-		  display: inline-block;
-		  padding: 2px 10px;
-		  border-radius: 4px;
-		  margin-right: 5px;
-	  }
-	  
-	  &-semester{
-		  font-size: 12px;
-		  background-color: #2d2d2d;
-		  display: inline-block;
-		  padding: 2px 10px;
-		  border-radius: 4px;
-		  margin-right: 5px;
-	  }
-	  
-	  img{
-		  border-radius: 50%;
-		  width: 25px;
-	  }
+
+  .school-info {
+    position: absolute;
+    left: 30px;
+    top: 40px;
+    font-size: 20px;
+    font-weight: bold;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+
+    &-name {
+      margin: 0 10px;
+    }
+
+    &-period {
+      font-size: 12px;
+      background-color: #2d2d2d;
+      display: inline-block;
+      padding: 2px 10px;
+      border-radius: 4px;
+      margin-right: 5px;
+    }
+
+    &-semester {
+      font-size: 12px;
+      background-color: #2d2d2d;
+      display: inline-block;
+      padding: 2px 10px;
+      border-radius: 4px;
+      margin-right: 5px;
+    }
+
+    img {
+      border-radius: 50%;
+      width: 25px;
+    }
   }
-  
+
   .bg {
     width: 100%;
     height: 100%;
@@ -70,18 +72,22 @@
   }
 
   .host-body {
+
     .dv-dec-10,
     .dv-dec-10-s {
       width: 33.3%;
       height: 5px;
     }
+
     .dv-dec-10-s {
       transform: rotateY(180deg);
     }
+
     .dv-dec-8 {
       width: 200px;
       height: 50px;
     }
+
     .title {
       position: relative;
       width: 500px;
@@ -91,7 +97,7 @@
 
       .title-text {
         font-size: 24px;
-		font-weight: bold;
+        font-weight: bold;
         position: absolute;
         bottom: 20px;
         left: 50%;
@@ -112,6 +118,7 @@
     .aside-width {
       width: 40%;
     }
+
     .react-r-s,
     .react-l-s {
       // background-color: #0f1325;
@@ -123,6 +130,7 @@
         text-align: left;
         width: 500px;
       }
+
       font-size: 18px;
       width: 300px;
       line-height: 50px;
@@ -150,6 +158,7 @@
         width: 500px;
         text-align: left;
       }
+
       font-size: 18px;
       width: 300px;
       height: 50px;
@@ -177,51 +186,49 @@
     .body-box {
       margin-top: 20px;
       display: flex;
-	  justify-content: space-between;
+      justify-content: space-between;
 
       //下方区域的布局
       .left-box {
-		width: 50%;
-		height: calc(100vh - 75px);
-		display: flex;
-		flex-direction: column;
-		
-		&-top{
-			width: 100%;
-			height: 24%;
-		}
-		
-		&-bottom{
-			width: 100%;
-			height: 71%;
-		}
+        width: 50%;
+        height: calc(100vh - 75px);
+        display: flex;
+        flex-direction: column;
+
+        &-top {
+          width: 100%;
+          height: 24%;
+        }
+
+        &-bottom {
+          width: 100%;
+          height: 71%;
+        }
       }
 
       // 底部数据
       .right-box {
-		width: 50%;
-		height: calc(100vh - 75px);
-       display: flex;
-       flex-direction: column;
-	   
-	   &-top{
-	   	width: 100%;
-	   	height: 40%;
-	   }
-	   
-	   &-bottom{
-	   	width: 100%;
-	   	height: 55%;
-		display: flex;
-		
-		.right-bottom-single{
-			width: 50%;
-			height: 100%;
-		}
-	   }
+        width: 50%;
+        height: calc(100vh - 75px);
+        display: flex;
+        flex-direction: column;
+
+        &-top {
+          width: 100%;
+          height: 40%;
+        }
+
+        &-bottom {
+          width: 100%;
+          height: 55%;
+          display: flex;
+
+          .right-bottom-single {
+            width: 50%;
+            height: 100%;
+          }
+        }
       }
     }
   }
-}
-
-
+}

+ 33 - 48
TEAMModelOS/ClientApp/src/view/mycourse/MyCourse.vue

@@ -61,7 +61,7 @@
                                 </p>
                                 <p class="class-attr-item">
                                     <span class="attr-label">{{$t('cusMgt.stuCount')}}</span>
-                                    <span class="class-name">{{item.allStu ? item.allStu.length : 0}}{{$t('unit.text7')}}</span>
+                                    <span class="class-name">{{getStudenCount(item.classId || item.stulist)}}{{$t('unit.text7')}}</span>
                                 </p>
                                 <Icon size="25" custom="iconfont icon-qr-code" :class="['qr-code-icon', {'qr-code-icon-color': item.joinLock}]" @click="showQrCode(index)" v-if="listType == 'private'" :title="$t('cusMgt.qrCodeLabel')" />
                                 <span v-if="hasCCAuth" :class="['qr-code-icon', hasCCAuth ? 'cc-icon' : 'no-cc-auth']" @click="startCus(index)">
@@ -308,6 +308,7 @@ export default {
             courseList: [],
             filterPeriod: '',
             courseInfo: {},
+            courseGroupList: [],//当前课程所有名单详细信息
             courseTypeClass: ['course-list-wrap'],
             addCusInfo: {
                 name: '',
@@ -338,7 +339,7 @@ export default {
         },
         curStuList() {
             let groupId = this.teaClassList[this.curClassIndex]?.classId || this.teaClassList[this.curClassIndex]?.stulist
-            return this.groupList.find(item => item.id == groupId)
+            return this.courseGroupList.find(item => item.id == groupId)
         },
         rcdParams() {
             return {
@@ -348,12 +349,14 @@ export default {
             }
         },
         students() {
-            if (this.teaClassList && this.teaClassList[this.curClassIndex]) {
-                return this.teaClassList[this.curClassIndex].allStu
-            }
-            else {
-                return []
+            if (this.teaClassList) {
+                let groupId = this.teaClassList[this.curClassIndex]?.classId || this.teaClassList[this.curClassIndex]?.stulist
+                if (groupId) {
+                    let group = this.courseGroupList.find(item => item.id == groupId)
+                    if (group) return group.members
+                }
             }
+            return
         },
         //查询成绩表所需参数(courseId, cId, code)
         gradeParams() {
@@ -365,7 +368,7 @@ export default {
             return data
         },
         courseListShow() {
-            if(!this.courseList.length){
+            if (!this.courseList.length) {
                 return []
             }
             let data = this.courseList.filter(item => item.scope == this.listType)
@@ -375,9 +378,9 @@ export default {
             if (data.length) {
                 return data
             } else {
-                setTimeout(()=>{
+                setTimeout(() => {
                     this.listType = this.listType == 'school' ? 'private' : 'school'
-                },100)
+                }, 100)
                 return []
             }
         },
@@ -417,6 +420,13 @@ export default {
         }, 100)
     },
     methods: {
+        getStudenCount(groupId) {
+            let group = this.courseGroupList.find(item => item.id == groupId)
+            if (group) {
+                return group.members?.length || 0
+            }
+            return 0
+        },
         setSourceId(value, selectData) {
             console.log(value)
             if (value.length) {
@@ -522,13 +532,7 @@ export default {
         },
         onSetIrs(data) {
             let { students } = data
-            // 当前名单对应的课程安排用于更新UI
-            let schedule = this.courseInfo.schedule.find(item => {
-                return item.stulist == this.teaClassList[this.curClassIndex].stulist && !item.classId //只有自定名单能添加学生,所以classId == ‘’
-            })
-            schedule.allStu = students
-
-            let stulist = this.groupList.find(item => {
+            let stulist = this.courseGroupList.find(item => {
                 return item.id == this.teaClassList[this.curClassIndex].stulist
             })
             if (stulist) {
@@ -536,7 +540,7 @@ export default {
             }
         },
         agreeJoinClass(status) {
-            let stulist = this.groupList.find(item => {
+            let stulist = this.courseGroupList.find(item => {
                 return item.id == this.teaClassList[this.curClassIndex].stulist
             })
             if (stulist) {
@@ -562,7 +566,7 @@ export default {
             let listId = this.teaClassList[index].stulist
             let listName = this.teaClassList[index].listName
             let cusName = this.courseInfo.name
-            let stulistInfo = this.groupList.find(item => {
+            let stulistInfo = this.courseGroupList.find(item => {
                 return item.id == listId
             })
             if (stulistInfo) {
@@ -631,6 +635,7 @@ export default {
                             students: s.students,
                             time: []
                         })
+                        this.courseListShow.push(this._.cloneDeep(s))
                         this.btnLoading = true
                         this.updCusInfo()
                         this.listId = ''
@@ -687,7 +692,7 @@ export default {
         },
         confirmRename() {
             let stulistId = this.teaClassList[this.curClassIndex].stulist
-            let stulist = this.groupList.find(item => {
+            let stulist = this.courseGroupList.find(item => {
                 return item.id == this.teaClassList[this.curClassIndex].stulist
             })
             if (stulist) {
@@ -995,13 +1000,15 @@ export default {
                         try {
                             //新版名单API支持同时获取各种名单详细信息
                             let allGroupIds = ids.concat(classIds)
+                            this.courseGroupList = []
                             if (allGroupIds.length) {
                                 let allGroups = await this.getListInfo([...allGroupIds])
                                 if (allGroups.groups) {
+                                    this.courseGroupList = allGroups.groups
                                     resSchedule.forEach(item => {
                                         //补充行政班信息
                                         if (item.classId) {
-                                            let classInfo = allGroups.groups.find(classItem => {
+                                            let classInfo = this.courseGroupList.find(classItem => {
                                                 return classItem.id == item.classId
                                             })
                                             item.classInfo = {
@@ -1009,19 +1016,15 @@ export default {
                                                 name: classInfo ? classInfo.name : this.$t('cusMgt.hasDelClass'),
                                                 year: classInfo ? classInfo.year : 0 //方便计算年级
                                             }
-                                            item.allStu = classInfo ? this._.cloneDeep(classInfo.members) : []
-                                            item.fullStu = true
                                             item.joinLock = classInfo.joinLock
                                         }
                                         //补充教学班信息
                                         if (item.stulist) {
-                                            let listInfo = allGroups.groups.find(listItem => {
+                                            let listInfo = this.courseGroupList.find(listItem => {
                                                 return listItem.id == item.stulist
                                             })
                                             item.listName = listInfo ? listInfo.name : this.$t('cusMgt.hasDelClass')
-                                            item.allStu = listInfo ? this._.cloneDeep(listInfo.members) : []
                                             item.listSchool = listInfo ? listInfo.school : undefined
-                                            item.fullStu = true
                                             item.joinLock = listInfo.joinLock
                                         }
                                         //统一数据格式
@@ -1045,15 +1048,8 @@ export default {
         },
         onAddStudent(data) {
             let { addStudents } = data
-            // 当前名单对应的课程安排用于更新UI
-            let schedule = this.courseInfo.schedule.find(item => {
-                return item.stulist == this.teaClassList[this.curClassIndex].stulist && !item.classId //只有自定名单能添加学生
-            })
-            if (schedule) {
-                schedule.allStu.unshift(...addStudents)
-            }
             //当前名单完整信息 用于更新名单API
-            let stulist = this.groupList.find(item => {
+            let stulist = this.courseGroupList.find(item => {
                 return item.id == this.teaClassList[this.curClassIndex].stulist
             })
             if (stulist) {
@@ -1063,20 +1059,8 @@ export default {
         },
         onDelStudent(data) {
             let { delIds } = data
-            // 当前名单对应的课程安排用于更新UI
-            let schedule = this.courseInfo.schedule.find(item => {
-                return item.stulist == this.teaClassList[this.curClassIndex].stulist && !item.classId //只有自定名单能添加学生
-            })
-            if (schedule) {
-                for (let i = 0; i < schedule.allStu.length; i++) {
-                    if (delIds.includes(schedule.allStu[i].id)) {
-                        schedule.allStu.splice(i, 1)
-                        i--
-                    }
-                }
-            }
             //当前名单完整信息 用于更新名单API
-            let stulist = this.groupList.find(item => {
+            let stulist = this.courseGroupList.find(item => {
                 return item.id == this.teaClassList[this.curClassIndex].stulist
             })
             if (stulist) {
@@ -1121,7 +1105,8 @@ export default {
                             students: res.list.members,
                             time: []
                         })
-                        this.groupList.push(res.list)
+                        this.groupList.push(this._.cloneDeep(res.list))
+                        this.courseGroupList.push(this._.cloneDeep(res.list))
                         this.updCusInfo()
                     }
                 },

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

@@ -37,7 +37,7 @@
                             {{$t('cusMgt.addStu')}}
                         </span>
                     </DropdownItem>
-                    <DropdownItem  @click.native="delStudents" v-if="basicStuList && basicStuList.scope == 'private'">
+                    <DropdownItem @click.native="delStudents" v-if="basicStuList && basicStuList.scope == 'private'">
                         <span class="action-item">
                             <Icon type="md-trash" size="16" />
                             {{$t('cusMgt.delStuBatch')}}
@@ -247,7 +247,7 @@ export default {
             return !!this.classInfo.stulist
         },
         students() {
-            return this.classInfo.allStu || this.basicStuList?.members || []
+            return this.basicStuList?.members || []
         },
         basicStuList() {
             return this._.cloneDeep(this.stuList)
@@ -274,7 +274,7 @@ export default {
                         //TODO 更新父组件数据
                         this.$emit('on-update-students', {
                             stuListId: this.basicStuList.id,
-                            students: this.basicStuList.members
+                            students: this._.cloneDeep(this.basicStuList.members)
                         })
                         this.setIrsStatus = false
                     },
@@ -302,8 +302,8 @@ export default {
             if (this.selections.length > 0) {
                 let stuIds = []
                 let addStudents = []
-                if (this.classInfo?.allStu) {
-                    stuIds = this.classInfo.allStu.map(item => {
+                if (this.basicStuList?.members) {
+                    stuIds = this.basicStuList?.members.map(item => {
                         return item.id
                     })
                 }
@@ -526,7 +526,7 @@ export default {
                         //TODO 更新父组件数据
                         this.$emit('on-update-students', {
                             stuListId: this.basicStuList.id,
-                            students: this.basicStuList.members
+                            students: this._.cloneDeep(this.basicStuList.members)
                         })
                         this.editIndex = -1
                     },
@@ -556,7 +556,7 @@ export default {
                         //TODO 更新父组件数据
                         this.$emit('on-update-students', {
                             stuListId: this.basicStuList.id,
-                            students: this.basicStuList.members
+                            students: this._.cloneDeep(this.basicStuList.members)
                         })
                         this.editIndex = -1
                     },

+ 15 - 0
TEAMModelOS/Controllers/OpenApi/Business/BizTeacherController.cs

@@ -69,5 +69,20 @@ namespace TEAMModelBI.Controllers
             var responseData = await OpenApiService.GetTeacherInfo(_azureCosmos, _dingDing, id, school, json);
             return Ok(new { responseData });
         }
+        /// <summary>
+        /// 教师执教的班级和课程
+        /// </summary>
+        /// <param name="json"></param>
+        /// <returns></returns>
+        [ProducesDefaultResponseType]
+        [HttpPost("get-teacher-teach")]
+        [ApiToken(Auth = "1502", Name = "教师执教的班级和课程", RWN = "R", Limit = false)]
+        public async Task<IActionResult> GetTeacherTeach(JsonElement json)
+        {
+            var (id, school) = HttpContext.GetApiTokenInfo();
+            var responseData = await OpenApiService.GetTeacherTeach(_azureCosmos, _dingDing, id, school, json);
+            return Ok(new { responseData });
+        }
+
     }
 }

+ 31 - 29
TEAMModelOS/Controllers/OpenApi/IRS/TianboController.cs

@@ -13,6 +13,7 @@ using System.Net.Http;
 using Microsoft.Extensions.Configuration;
 using TEAMModelOS.SDK.Models.Service;
 using System.Threading;
+using TEAMModelOS.SDK.Extension;
 
 namespace TEAMModelOS.Controllers
 {
@@ -70,49 +71,50 @@ namespace TEAMModelOS.Controllers
 
                     if (!string.IsNullOrWhiteSpace(stu?.channel) && !string.IsNullOrWhiteSpace(stu?.userid))
                     {
-                        var serviceManager = _azureSignalR.GetServiceManager();
-                        var cancel = new CancellationToken();
-                        var hub = await serviceManager.CreateHubContextAsync($"C{stu.channel}", cancel);
-                        try
+                        // 處理單例復用,提高請求效率,避免ServiceTransportType.Persistent websocket斷線在Azure偶發的消息遺失
+                        // TODO 尚須注意釋放的問題,後續須處理
+                        var hub = _azureSignalR.GetServiceManager().GetHubContext($"C{stu.channel}");
+                        // 下面註解寫法會造成Azure上消息偶發遺失,Localhost不會發生
+                        //var serviceManager = _azureSignalR.GetServiceManager();                       
+                        //using var hub = await serviceManager.CreateHubContextAsync($"C{stu.channel}", CancellationToken.None);
+
+                        var con = content.GetString();
+                        var ans = con switch
                         {
-                            var con = content.GetString();
-                            var ans = con switch
-                            {
-                                string _ when con.Length == 1 => new string[] { con },
-                                string _ when con.Contains('+', StringComparison.OrdinalIgnoreCase) => con.Split('+'),
-                                string _ when con.Equals("true", StringComparison.OrdinalIgnoreCase) => new string[] { "A" },
-                                string _ when con.Equals("false", StringComparison.OrdinalIgnoreCase) => new string[] { "B" },
-                                _ => throw new ArgumentNullException(nameof(content))
-                            };
+                            string _ when con.Length == 1 => new string[] { con },
+                            string _ when con.Contains('+', StringComparison.OrdinalIgnoreCase) => con.Split('+'),
+                            string _ when con.Equals("true", StringComparison.OrdinalIgnoreCase) => new string[] { "A" },
+                            string _ when con.Equals("false", StringComparison.OrdinalIgnoreCase) => new string[] { "B" },
+                            _ => throw new ArgumentNullException(nameof(content))
+                        };
 
-                            var common = JsonSerializer.Serialize(new
-                            {
-                                action = "DirectIRS.Answer",
-                                clientType = "DI",
-                                sender = stu.stuid,
-                                timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
-                                waitReturn = false,
-                                payload = new { irsno = "", answer = ans }
-                            });
+                        var common = JsonSerializer.Serialize(new
+                        {
+                            action = "DirectIRS.Answer",
+                            clientType = "DI",
+                            sender = stu.stuid,
+                            timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
+                            waitReturn = false,
+                            payload = new { irsno = "", answer = ans }
+                        });
 
-                            await hub.Clients.User(stu.userid).SendCoreAsync("onMessage", new[]{new {
+                        await hub.Clients.User(stu.userid).SendCoreAsync("onMessage", new[]{new {
                                 connectionId = (string)null,
                                 to = (string)null,
                                 groupname = (string)null,
                                 sender = stu.stuid,
                                 text = common
                             }});
-                        }
-                        finally
-                        {
-                            hub.Dispose();
-                        }
+
                     }
+                    await _dingDing.SendBotMsg($"IES5,{_option.Location},tianbo/api/course/cardUploadData()\n{stu?.ToJsonString()}", GroupNames.醍摩豆服務運維群組);
                 }
+
                 return Ok(new { code = 200, msg = "成功", data = (string)null });
             }
-            catch
+            catch (Exception ex)
             {
+                await _dingDing.SendBotMsg($"IES5,{_option.Location},tianbo/api/course/cardUploadData()\n{ex.Message}\n{ex.StackTrace}{request.ToJsonString()}", GroupNames.醍摩豆服務運維群組);
                 return Ok(new { code = 200, msg = "成功", data = (string)null });
             }
 

+ 16 - 0
TEAMModelOS/Controllers/OpenApi/OpenApiService.cs

@@ -516,6 +516,22 @@ namespace TEAMModelOS.Controllers
             }
         }
 
+
+        /// <summary>
+        /// 教师执教的班级和课程
+        /// [ApiToken(Auth = "1502", Name = "教师详细信息", RWN = "R", Limit = false)]
+        /// </summary>
+        /// <param name="_azureCosmos"></param>
+        /// <param name="_dingDing"></param>
+        /// <param name="bizId"></param>
+        /// <param name="school"></param>
+        /// <param name="json"></param>
+        /// <returns></returns>
+        public static async Task<ResponseData<dynamic>> GetTeacherTeach(AzureCosmosFactory _azureCosmos, DingDing _dingDing, string bizId, string school, JsonElement json) {
+
+            return null;
+        }
+
         /// <summary>
         /// 教师详细信息
         /// [ApiToken(Auth = "1502", Name = "教师详细信息", RWN = "R", Limit = false)]

+ 1 - 1
TEAMModelOS/Controllers/OpenApi/OpenSchool/ScGroupListController.cs

@@ -134,7 +134,7 @@ namespace TEAMModelOS.Controllers
                     List<Student> webStudents = json.GetProperty("students").ToObject<List<Student>>();
                     webStudents.ForEach(x => x.periodId = $"{_periodId}");
                     List<Student> preStudents = await StudentService.GeStudentData(_azureCosmos, school, webStudents?.Select(x => x.id));
-                    var retUpdate = await StudentService.updateStudents(_azureCosmos, _dingDing, _option, school, json.GetProperty("students").EnumerateArray());
+                    var retUpdate = await StudentService.updateStudents(_azureCosmos, _dingDing, _option, school, json.GetProperty("students").EnumerateArray(),false);
                     await StudentService.CheckStudent(_serviceBus, _configuration, _azureCosmos, school, webStudents, preStudents);
                     return Ok(new { code = school, students = retUpdate.studs, retUpdate.classDuplNos, retUpdate.nonexistentIds, retUpdate.errorNos, retUpdate.errorClassId });
                 }

+ 8 - 1
TEAMModelOS/Controllers/Student/StudentController.cs

@@ -175,8 +175,15 @@ namespace TEAMModelOS.Controllers
                     case "update":
                         //更新學生資料,批量密碼重置,基本資訊更新(姓名、教室ID、性別、學年及座號)
                         webStudents = request.GetProperty("students").ToObject<List<Student>>();
+                        var cleanImei  = false;
+                         
+                        if (request.GetProperty("cleanImei").ValueKind.Equals(JsonValueKind.True))
+                        {
+                            cleanImei = true;
+                        }
+                        
                         preStudents = await StudentService.GeStudentData(_azureCosmos, schoolId.GetString(), webStudents?.Select(x => x.id));
-                        var retUpdate = await StudentService.updateStudents(_azureCosmos, _dingDing, _option, schoolId.GetString(), request.GetProperty("students").EnumerateArray());
+                        var retUpdate = await StudentService.updateStudents(_azureCosmos, _dingDing, _option, schoolId.GetString(), request.GetProperty("students").EnumerateArray(), cleanImei);
                         await StudentService.CheckStudent(_serviceBus, _configuration, _azureCosmos, schoolId.GetString(), webStudents, preStudents);
                         return this.Ok(new { code = $"Base-{schoolId.GetString()}", students = retUpdate.studs, retUpdate.classDuplNos, retUpdate.nonexistentIds, retUpdate.errorNos, retUpdate.errorClassId });
                     case "delete":

+ 2 - 1
TEAMModelOS/appsettings.Development.json

@@ -36,7 +36,8 @@
       "GenPdfQueue": "dep-genpdf"
     },
     "SignalR": {
-      "ConnectionString": "Endpoint=https://channel.service.signalr.net;AccessKey=KrblW06tuA4a/GyqRPDU0ynFFmAWxbAvyJihHclSXbQ=;Version=1.0;"
+      //"ConnectionString": "Endpoint=https://channel.service.signalr.net;AccessKey=KrblW06tuA4a/GyqRPDU0ynFFmAWxbAvyJihHclSXbQ=;Version=1.0;",
+      "ConnectionString": "Endpoint=https://channel.signalr.azure.cn;AccessKey=AtcB7JYFNUbUXb1rGxa3PVksQ2X5YSv3JOHZR9J88tw=;Version=1.0;"
     }
   },
   "HaBookAuth": {