瀏覽代碼

Merge branch 'develop' of http://163.228.141.122:3000/TEAMMODEL/TEAMModelOS into develop

jeff 6 月之前
父節點
當前提交
e8eff2944e
共有 47 個文件被更改,包括 8837 次插入718 次删除
  1. 140 87
      HTEX.DataETL/Controllers/LessonRecordController.cs
  2. 3 3
      TEAMModelBI/TEAMModelBI.csproj
  3. 774 234
      TEAMModelOS.Extension/HTEX.Lib/ETL/Lesson/LessonETLService.cs
  4. 113 2
      TEAMModelOS.Extension/HTEX.Lib/summary.xml
  5. 5 8
      TEAMModelOS.Extension/HTEX.Test/Controllers/MockDataController.cs
  6. 15 199
      TEAMModelOS.Extension/HTEX.Test/Program.cs
  7. 0 8
      TEAMModelOS.Extension/HTEX.Test/x.xml
  8. 13 2
      TEAMModelOS.Function/IESServiceBusTrigger.cs
  9. 3 3
      TEAMModelOS.Function/TEAMModelOS.Function.csproj
  10. 4 1
      TEAMModelOS.SDK/Models/Cosmos/Common/LessonRecord.cs
  11. 5 1
      TEAMModelOS.SDK/Models/Cosmos/School/SchoolSetting.cs
  12. 10 10
      TEAMModelOS.SDK/Models/Service/SystemService.cs
  13. 3 3
      TEAMModelOS.SDK/TEAMModelOS.SDK.csproj
  14. 134 80
      TEAMModelOS/ClientApp/src/common/BaseQuickArtPaper.vue
  15. 430 0
      TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseBar.vue
  16. 350 0
      TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseEntryBar.vue
  17. 285 0
      TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseKnowledgeDetail.vue
  18. 318 0
      TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseLevelDetail.vue
  19. 123 0
      TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseLevelPie.vue
  20. 288 0
      TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseLineBar.vue
  21. 813 0
      TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseMyTable.vue
  22. 120 0
      TEAMModelOS/ClientApp/src/components/student-analysis/total/htBasePie.vue
  23. 197 0
      TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseRadar.vue
  24. 359 0
      TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseScatter.vue
  25. 238 0
      TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseScoreRateBar.vue
  26. 354 0
      TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseSingleStuScatter.vue
  27. 291 0
      TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseTestScatter.vue
  28. 35 26
      TEAMModelOS/ClientApp/src/store/module/totalAnalysis.js
  29. 126 1
      TEAMModelOS/ClientApp/src/utils/public.js
  30. 114 9
      TEAMModelOS/ClientApp/src/view/artexam/ExamSetting.vue
  31. 17 6
      TEAMModelOS/ClientApp/src/view/evaluation/bank/index.vue
  32. 2 1
      TEAMModelOS/ClientApp/src/view/htcommunity/htMgtExam.vue
  33. 20 14
      TEAMModelOS/ClientApp/src/view/learnactivity/tabs/htCloudDAS.vue
  34. 5 5
      TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/AchievementAnalysis/htAchievementAnalysis.vue
  35. 627 0
      TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/AchievementAnalysis/htEarlyWarning.vue
  36. 292 0
      TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/AchievementAnalysis/htEntryTables.vue
  37. 172 0
      TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/KnowledgeAnalysis/htKnowledgeAnalysis.vue
  38. 250 0
      TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/KnowledgeAnalysis/htScoreDetails.vue
  39. 159 0
      TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/LevelAnalysis/htLevelAnalysis.vue
  40. 276 0
      TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/LevelAnalysis/htScoreDetails.vue
  41. 492 0
      TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/ScatterAnalysis/htScatterAnalysis.vue
  42. 410 0
      TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/TestAnalysis/htQuestionList.vue
  43. 434 0
      TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/TestAnalysis/htTestAnalysis.vue
  44. 12 9
      TEAMModelOS/Controllers/System/BlobController.cs
  45. 4 4
      TEAMModelOS/TEAMModelOS.csproj
  46. 1 1
      TEAMModelOS/appsettings.Development.json
  47. 1 1
      TEAMModelOS/appsettings.json

+ 140 - 87
HTEX.DataETL/Controllers/LessonRecordController.cs

@@ -4,6 +4,7 @@ using MathNet.Numerics.Distributions;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Azure.Cosmos;
 using StackExchange.Redis;
+using System.Collections.Generic;
 using System.Text.Json;
 using System.Xml;
 using TEAMModelOS.SDK;
@@ -141,15 +142,17 @@ namespace HTEX.DataETL.Controllers
                     studentLessonDatas = LessonETLService.GetTaskData(lessonBase, timeLineData, taskDatas, studentLessonDatas, id);
                     var pickupData = LessonETLService.GetPickupData(lessonBase, timeLineData, studentLessonDatas, id);
                     studentLessonDatas= pickupData.studentLessonDatas;
-
+                    var codeBools= LessonETLService.GetCodeBools(studentLessonDatas);
                     await System.IO.File.WriteAllTextAsync(Path.Combine(path, $"student-analysis.json"), studentLessonDatas.ToJsonString());
-                    string jsons = await System.IO.File.ReadAllTextAsync($"{lessonPath}\\analysis\\analysis.json");
-                    LessonDataAnalysisCluster lessonDataAnalysis = jsons.ToObject<LessonDataAnalysisCluster>();
-                    var lessonItems = LessonETLService.ProcessStudentDataV2(studentLessonDatas, lessonDataAnalysis);
+                    string jsons = await System.IO.File.ReadAllTextAsync($"{lessonPath}\\analysis\\analysis-model.json");
+                    LessonDataAnalysisModel lessonDataAnalysis = jsons.ToObject<LessonDataAnalysisModel>();
+                    
+
+                    var studentLessons = LessonETLService.ProcessStudentDataV2(studentLessonDatas, lessonDataAnalysis,codeBools);
                     XmlDocument xmlDocument = new XmlDocument();
                     var runtimePath = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
                     xmlDocument.Load($"{runtimePath}\\summary.xml");
-                    await LessonETLService.ExportToExcelLocal(lessonItems, $"{path}\\analysis.xlsx", xmlDocument);
+                    await LessonETLService.ExportToExcelLocal(studentLessons, $"{path}\\analysis.xlsx", xmlDocument);
                 }
             }
             return Ok();
@@ -160,24 +163,24 @@ namespace HTEX.DataETL.Controllers
         /// </summary>
         /// <param name="json"></param>
         /// <returns></returns>
-        //[HttpPost("process-fix-history-students")]
-        //public async Task<IActionResult> ProcessFixHistoryStudents(JsonElement json)
-        //{
-        //    string? lessonBasePath = _configuration.GetValue<string>("LessonPath");
-        //    string studentsPath = $"{lessonBasePath}\\students";
-        //    List<string> studentsPs = FileHelper.ListAllFiles(studentsPath);
-        //    string? pathLessons = $"{lessonBasePath}\\lessons";
-        //    List<string> filesLessons = FileHelper.ListAllFiles(pathLessons, "-local.json");
-        //    int index = 0;
-        //    foreach (var stu in studentsPs)
-        //    {
-        //        string stujson = await System.IO.File.ReadAllTextAsync(stu);
-        //        var studentSemester = stujson.ToObject<StudentSemesterRecord>();
-        //        await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Student).UpsertItemAsync(studentSemester, new PartitionKey(studentSemester.code));
-        //        index++;
-        //    }
-        //    return Ok();
-        //}
+        [HttpPost("process-fix-history-students")]
+        public async Task<IActionResult> ProcessFixHistoryStudents(JsonElement json)
+        {
+            string? pathAnalysis = $"F:\\lesson-local\\analysis";
+            try
+            {
+                string jsons = await System.IO.File.ReadAllTextAsync($"F:\\lesson-local\\analysis\\analysis-model.json");
+                var s = JsonSerializer.Deserialize<LessonDataAnalysisModel>(jsons);
+                LessonDataAnalysisModel lessonDataAnalysis = JsonDocument.Parse(jsons).RootElement.ToObject<LessonDataAnalysisModel>();
+                var per = LessonETLService.GetPersent(lessonDataAnalysis.irs, 2);
+
+            }
+            catch (Exception ex)
+            {
+                Console.WriteLine(ex.ToString());
+            }
+            return Ok();
+        }
         /// <summary>
         /// 
         /// </summary>
@@ -191,13 +194,12 @@ namespace HTEX.DataETL.Controllers
             string? lessonBasePath = _configuration.GetValue<string>("LessonPath");
             string? pathLessons = $"{lessonBasePath}\\lessons";
             string? pathAnalysis = $"{lessonBasePath}\\analysis";
-            string jsons = await System.IO.File.ReadAllTextAsync($"{pathAnalysis}\\analysis.json");
-            LessonDataAnalysisCluster lessonDataAnalysis = jsons.ToObject<LessonDataAnalysisCluster>();
+            string jsons = await System.IO.File.ReadAllTextAsync($"{pathAnalysis}\\analysis-model.json");
+            LessonDataAnalysisModel lessonDataAnalysis = jsons.ToObject<LessonDataAnalysisModel>();
             List<string> filesLessons = FileHelper.ListAllFiles(pathLessons, "-local.json");
             var runtimePath = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
             XmlDocument xmlDocument = new XmlDocument();
             xmlDocument.Load($"{runtimePath}\\summary.xml");
-            List<StudentSemesterRecord> students = new List<StudentSemesterRecord>();
             List<School> schools = new List<School>();
            // await Parallel.ForEachAsync(filesLessons, async (fileLesson, _) =>
             List<string> localIds = new List<string>();
@@ -209,9 +211,10 @@ namespace HTEX.DataETL.Controllers
                     localIds.Add(lessonId);
                 }
             }
-            long stime = 1690819200000;//2023-08-01 00:00:00
+            long stime = 1693497600000;//2023-08-01 00:00:00
+            long etime = 1731513599000;//2023-08-01 00:00:00
             var resultSchool = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.School)
-                 .GetList<LessonRecord>($"SELECT value c FROM   c  where c.startTime>{stime} and   c.expire<=0  and c.status<>404  and c.duration>300 and c.pk='LessonRecord' ", $"LessonRecord-ydzt");
+                 .GetList<LessonRecord>($"SELECT value c FROM   c  where   ( c.analysis>=0 or IS_DEFINED(c.analysis) = false ) and c.startTime>={stime} and c.startTime<={etime} and   c.expire<=0  and c.status<>404  and c.duration>300 and c.pk='LessonRecord' ");
             List<string> ignore = new List<string>() { "PgJump", "PgRcv", "PgAdd" };
             int index = 0;
             if (resultSchool.list.IsNotEmpty())
@@ -219,45 +222,89 @@ namespace HTEX.DataETL.Controllers
                 List<StudentSemesterRecord>  studentSemesterRecords= new List<StudentSemesterRecord>();
                 List<OverallEducation> overallEducations= new List<OverallEducation>();
                 List<Student> studentsBase = new List<Student>();
-             
-                await foreach (var item in LessonETLService.GetLessonLocal(resultSchool.list, localIds, _azureStorage, pathLessons))
+                List<LessonLocal>lessonLocals = new List<LessonLocal>();
+                await Parallel.ForEachAsync(resultSchool.list, async (lessonRecord, cancellationToken) =>
+                {
+                    var  item = await LessonETLService.GetLessonLocal(lessonRecord,localIds,_azureStorage,pathLessons);
+                    lessonLocals.Add(item);
+                    index++;
+                });
+                
+                var  schoolGroup =   lessonLocals.Where(x=>!string.IsNullOrWhiteSpace(x.lessonRecord?.school)).GroupBy(x => x.lessonRecord?.school).Select(x => new { key=x.Key ,list=x.ToList()});
+                string schoolSql = $"select value c from c where c.id in ({string.Join(",", schoolGroup.Select(x => $"'{x.key}'"))})";
+                var schoolResults= await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.School).GetList<School>(schoolSql, "Base");
+                if (schoolResults.list.IsNotEmpty()) 
                 {
-                    string yearMonthPath = DateTimeOffset.FromUnixTimeMilliseconds(item.lessonRecord.startTime).ToString("yyyyMM");
-                    if (item.lessonBase!=null && item.lessonBase.student!=null)
+                    schools.AddRange(schoolResults.list);
+                }
+                foreach (var group in schoolGroup) 
+                {
+                    var studentIds = group.list.SelectMany(x => x.studentLessonDatas).Where(x=>!string.IsNullOrWhiteSpace(x.id)).Select(x => x.id).Distinct();
+                    string studentSql = $"select value c from c where c.id in ({string.Join(",", studentIds.Select(x => $"'{x}'"))})";
+                    var  studentResults=  await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Student).GetList<Student>(studentSql, $"Base-{group.key}");
+                    if (studentResults.list.IsNotEmpty()) 
                     {
-                        TechCount techCount = new TechCount
-                        {
-                            lessonId=item.lessonRecord?.id,
-                            examCount = item.examDatas.Count,
-                            taskCount = item.taskDatas.Count,
-                            irsCount = item.irsDatas.Count,
-                            coworkCount = item.coworkDatas.Count,
-                            smartRatingCount =item.smartRatingDatas.Count,
-                            timeCount=item.sokratesDatas.Where(x => !ignore.Contains(x.Event)  &&  !x.Event.Contains("End", StringComparison.OrdinalIgnoreCase)).GroupBy(x => x.Event).Select(x => new CodeLong() { code=x.Key, value= x.ToList().Count }).ToList()
-                        };
-                        await System.IO.File.WriteAllTextAsync($"{pathLessons}\\MM{yearMonthPath}\\{item.lessonRecord.id}-count.json", techCount.ToJsonString());
-                        await System.IO.File.WriteAllTextAsync($"{pathLessons}\\MM{yearMonthPath}\\{item.lessonRecord!.id}-local.json", item.ToJsonString());
+                        studentsBase.AddRange(studentResults.list);
                     }
-                    else
+                    string studentSemesterRecordSql = $"select value c from c where c.stuid in ({string.Join(",", studentIds.Select(x => $"'{x}'"))}) and c.studyYear>=2023";
+                    var studentSemesterRecordResults = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Student).GetList<StudentSemesterRecord>(studentSemesterRecordSql, $"StudentSemesterRecord-{group.key}");
+                    if (studentSemesterRecordResults.list.IsNotEmpty())
                     {
-
-                        System.IO.File.Delete($"{pathLessons}\\MM{yearMonthPath}\\{item.lessonRecord!.id}-local.json");
-                        System.IO.File.Delete($"{pathLessons}\\MM{yearMonthPath}\\{item.lessonRecord!.id}-count.json");
+                        studentSemesterRecords.AddRange(studentSemesterRecordResults.list);
+                    }
+                    string overallEducationSql = $"select value c from c where c.studentId in ({string.Join(",", studentIds.Select(x => $"'{x}'"))}) and c.year>=2023";
+                    var overallEducationResults = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Student).GetList<OverallEducation>(overallEducationSql, $"OverallEducation-{group.key}");
+                    if (overallEducationResults.list.IsNotEmpty())
+                    {
+                        overallEducations.AddRange(overallEducationResults.list);
                     }
-                    await LessonETLService.DoStudentLessonData(Constant.objectiveTypes, _azureStorage, item, _dingDing, _azureCosmos.GetCosmosClient(), "China", _azureRedis, studentSemesterRecords,overallEducations, lessonDataAnalysis, studentsBase, schools);
-                    index++;
                 }
-                foreach (var studentSemester in studentSemesterRecords) 
+                List<(string id,string owner, List<StudentLessonData> studentLessonData)> studentLessonDatas= new List<(string id, string owner, List<StudentLessonData>)>();
+                List<(string id, string owner, List<StudentLessonItem> studentLessons,List<CodeBool> codeBools, List<StudentLessonData> studentLessonData)> lessonItems = new List<(string id, string owner, List<StudentLessonItem> studentLessons, List<CodeBool> codeBools, List<StudentLessonData> studentLessonData)>();
+                int n = 0;
+                foreach (var  item in lessonLocals) 
                 {
-                    await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Student).UpsertItemAsync(studentSemester, new PartitionKey(studentSemester.code));
+                    var studata = await LessonETLService.DoStudentLessonData(Constant.objectiveTypes, _azureStorage, item, _dingDing, _azureCosmos.GetCosmosClient(), "China", _azureRedis, studentSemesterRecords, overallEducations, lessonDataAnalysis, studentsBase, schools);
+                    if (studata.codeBools.FindAll(x=>x.value).IsNotEmpty()) 
+                    {
+                        string owner = item.lessonRecord.scope.Equals("school") ? item.lessonRecord.school : item.lessonRecord.tmdid;
+                        studentLessonDatas.Add((item.lessonRecord.id, owner, studata.studentLessonDatas));
+                        lessonItems.Add((item.lessonRecord.id, owner, studata.lessonItems,studata.codeBools,studata.studentLessonDatas));
+                    }
+                    n++;
                 }
-                foreach (var overallEducation in overallEducations)
+                int p = 0;
+                //XmlDocument xmlDocument = new XmlDocument();
+                //var runtimePath = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
+                //xmlDocument.Load($"{runtimePath}\\summary.xml");
+                await Parallel.ForEachAsync(lessonItems, async (lessonItem, cancellationToken) =>
+                {
+                    lessonItem.studentLessons = LessonETLService.ProcessStudentDataV2(lessonItem.studentLessonData, lessonDataAnalysis, lessonItem.codeBools);
+                    await LessonETLService.ExportToExcelAzureBlob(lessonItem.studentLessons, _azureStorage, lessonItem.owner, $"{lessonItem.id}/student-analysis.xlsx", xmlDocument);
+                    p++;
+                });
+                int s = 0;
+                await Parallel.ForEachAsync(studentLessonDatas, async (studentLessonData, cancellationToken) =>
+                {
+                    await _azureStorage.GetBlobContainerClient(studentLessonData.owner).UploadFileByContainer(studentLessonData.studentLessonData.ToJsonString(), "records", $"{studentLessonData.id}/student-analysis.json");
+                    s++;
+                });
+
+                int m   =0;
+                await Parallel.ForEachAsync(studentSemesterRecords, async (studentSemester, cancellationToken) => 
+                {
+                    await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Student).UpsertItemAsync(studentSemester, new PartitionKey(studentSemester.code));
+                    m++;
+                });
+                int k = 0;
+                await Parallel.ForEachAsync(overallEducations, async (overallEducation, cancellationToken) =>
                 {
                     await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Student).UpsertItemAsync(overallEducation, partitionKey: new PartitionKey(overallEducation.code));
-                    string key = $"OverallEducation:{overallEducation.schoolCode}:{overallEducation.periodId}:{overallEducation.year}:{overallEducation.semesterId}:{overallEducation?.classId}";
-                    await _azureRedis.GetRedisClient(8).HashSetAsync(key, overallEducation.studentId, overallEducation.ToJsonString());
-                    await _azureRedis.GetRedisClient(8).KeyExpireAsync(key, new TimeSpan(180 *24, 0, 0));
-                }
+                    //string key = $"OverallEducation:{overallEducation.schoolCode}:{overallEducation.periodId}:{overallEducation.year}:{overallEducation.semesterId}:{overallEducation?.classId}";
+                    //await _azureRedis.GetRedisClient(8).HashSetAsync(key, overallEducation.studentId, overallEducation.ToJsonString());
+                    //await _azureRedis.GetRedisClient(8).KeyExpireAsync(key, new TimeSpan(180 *24, 0, 0));
+                    k++;
+                });
             }
             return Ok();
         }
@@ -289,7 +336,7 @@ namespace HTEX.DataETL.Controllers
             bool loadLocal = true;
             List<LessonDataAnalysisMonth> lessonDataAnalysisMonths = new List<LessonDataAnalysisMonth>();
             var filesAnalysis = FileHelper.ListAllFiles(pathAnalysis);
-            long stime = 1690819200000;//2023-08-01 00:00:00
+            long stime = 1693497600000;//2023-09-01 00:00:00
             foreach (var file in filesAnalysis)
             {
                 //读取每月的数据
@@ -329,7 +376,7 @@ namespace HTEX.DataETL.Controllers
             {
                 List<LessonRecord> lessonRecords = new List<LessonRecord>();
                 var resultSchool = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.School)
-                    .GetList<LessonRecord>($"SELECT value c FROM   c  where c.startTime>={stime} and c.expire<=0  and c.status<>404  and c.duration>300 and c.pk='LessonRecord' ", null);
+                    .GetList<LessonRecord>($"SELECT value c FROM   c  where   ( c.analysis>=0 or IS_DEFINED(c.analysis) = false ) and  c.startTime>={stime} and c.expire<=0  and c.status<>404  and c.duration>300 and c.pk='LessonRecord' ", null);
                 if (resultSchool.list.IsNotEmpty())
                 {
                     newest= resultSchool.list.Max(x => x.startTime);
@@ -340,7 +387,7 @@ namespace HTEX.DataETL.Controllers
                     newest=stime;
                 }
                 var resultTeacher = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Teacher)
-                   .GetList<LessonRecord>($"SELECT value c FROM   c  where c.startTime>={stime} and c.expire<=0  and c.status<>404  and c.duration>300 and c.pk='LessonRecord'   ", null);
+                   .GetList<LessonRecord>($"SELECT value c FROM   c  where   ( c.analysis>=0 or IS_DEFINED(c.analysis) = false ) and  c.startTime>={stime} and c.expire<=0  and c.status<>404  and c.duration>300 and c.pk='LessonRecord'   ", null);
                 if (resultTeacher.list.IsNotEmpty())
                 {
                     long max = resultTeacher.list.Max(x => x.startTime);
@@ -353,40 +400,46 @@ namespace HTEX.DataETL.Controllers
                 List<string> ignore = new List<string>() { "PgJump", "PgRcv", "PgAdd" };
                 if (lessonRecords.IsNotEmpty())
                 {
-
-                    await foreach (var item in LessonETLService.GetLessonLocal(lessonRecords, localIds, _azureStorage, pathLessons))
-                    {
+                    await Parallel.ForEachAsync(lessonRecords, async(record, _)=>{
+                        LessonLocal  item = await LessonETLService.GetLessonLocal(record, localIds, _azureStorage, pathLessons);
                         string yearMonthPath = DateTimeOffset.FromUnixTimeMilliseconds(item.lessonRecord.startTime).ToString("yyyyMM");
-                        if (item.lessonBase!=null && item.lessonBase.student!=null)
-                        {
-                            TechCount techCount = new TechCount
-                            {
-                                lessonId=item.lessonRecord?.id,
-                                examCount = item.examDatas.Count,
-                                taskCount = item.taskDatas.Count,
-                                irsCount = item.irsDatas.Count,
-                                coworkCount = item.coworkDatas.Count,
-                                smartRatingCount =item.smartRatingDatas.Count,
-                                timeCount=item.sokratesDatas.Where(x => !ignore.Contains(x.Event)  &&  !x.Event.Contains("End", StringComparison.OrdinalIgnoreCase)).GroupBy(x => x.Event).Select(x => new CodeLong() { code=x.Key, value= x.ToList().Count }).ToList()
-                            };
-                            await System.IO.File.WriteAllTextAsync($"{pathLessons}\\MM{yearMonthPath}\\{item.lessonRecord.id}-count.json", techCount.ToJsonString());
-                            await System.IO.File.WriteAllTextAsync($"{pathLessons}\\MM{yearMonthPath}\\{item.lessonRecord!.id}-local.json", item.ToJsonString());
-                        }
-                        else
+                        item.lessonRecord.learningCategory= item.lessonBase?.summary?.learningCategory;
+                        if (item.lessonRecord.learningCategory == null)
                         {
 
-                            System.IO.File.Delete($"{pathLessons}\\MM{yearMonthPath}\\{item.lessonRecord!.id}-local.json");
-                            System.IO.File.Delete($"{pathLessons}\\MM{yearMonthPath}\\{item.lessonRecord!.id}-count.json");
+                            item.lessonRecord.learningCategory = new LearningCategory();
                         }
-                    }
+                        await System.IO.File.WriteAllTextAsync($"{pathLessons}\\MM{yearMonthPath}\\{item.lessonRecord!.id}-local.json", item.ToJsonString());
+                    });
+
+
+                   //await foreach (var item in LessonETLService.GetLessonLocal(lessonRecords, localIds, _azureStorage, pathLessons))
+                   // {
+                   //     string yearMonthPath = DateTimeOffset.FromUnixTimeMilliseconds(item.lessonRecord.startTime).ToString("yyyyMM");
+                   //     item.lessonRecord.learningCategory= item.lessonBase?.summary?.learningCategory;
+                   //     if (item.lessonRecord.learningCategory == null)
+                   //     {
+
+                   //         item. lessonRecord.learningCategory = new LearningCategory();
+                   //     }
+                   //     await System.IO.File.WriteAllTextAsync($"{pathLessons}\\MM{yearMonthPath}\\{item.lessonRecord!.id}-local.json", item.ToJsonString());
+                   // }
                 }
                 List<TechCount> techCounts = new List<TechCount>();
                 filesLessons = FileHelper.ListAllFiles(pathLessons, "-local.json");
-                await foreach (var item in LessonETLService.GetTeachCount(lessonRecords, filesLessons, pathLessons, ignore, Constant.objectiveTypes, _azureStorage, force))
-                {
-                    techCounts.Add(item);
-                }
-                await   LessonETLService.GenAnalysisData(pathAnalysis, newest, techCounts,_azureStorage);
+                //await foreach (var item in LessonETLService.GetTeachCount(_azureCosmos,lessonRecords, filesLessons, pathLessons, ignore, Constant.objectiveTypes, _azureStorage, force))
+                //{
+                //    techCounts.Add(item);
+                //}
+
+                await Parallel.ForEachAsync(filesLessons, async (item, _) => {
+                    TechCount techCount=  await  LessonETLService.GetTeachCount(_azureCosmos, item, pathLessons, ignore, Constant.objectiveTypes, _azureStorage, force);
+                    if (techCount != null) {
+                        techCounts.Add(techCount);
+                    }
+                });
+
+                await LessonETLService.GenAnalysisData(pathAnalysis, newest, techCounts,_azureStorage);
             }
             return Ok(new { yearMonth });
         }

+ 3 - 3
TEAMModelBI/TEAMModelBI.csproj

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

文件差異過大導致無法顯示
+ 774 - 234
TEAMModelOS.Extension/HTEX.Lib/ETL/Lesson/LessonETLService.cs


文件差異過大導致無法顯示
+ 113 - 2
TEAMModelOS.Extension/HTEX.Lib/summary.xml


+ 5 - 8
TEAMModelOS.Extension/HTEX.Test/Controllers/MockDataController.cs

@@ -21,9 +21,6 @@ namespace HTEX.Test.Controllers
         public static async Task MockData()
         {
             
-
-
-            
             #region 数据模拟
             //学生人数
             int scount = Random.Shared.Next(40, 45);
@@ -460,17 +457,17 @@ namespace HTEX.Test.Controllers
             }
            
             #endregion 数据模拟
-            string jsons = await System.IO.File.ReadAllTextAsync("F:\\lesson-local\\analysis\\analysis.json");
+            string jsons = await System.IO.File.ReadAllTextAsync("F:\\lesson-local\\analysis\\analysis-model.json");
             long time = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
-            LessonDataAnalysisCluster lessonDataAnalysis = jsons.ToObject<LessonDataAnalysisCluster>();
-
-            var lessonItems= LessonETLService. ProcessStudentDataV2(students, lessonDataAnalysis);
+            LessonDataAnalysisModel lessonDataAnalysis = jsons.ToObject<LessonDataAnalysisModel>();
+            var codeBools = LessonETLService.GetCodeBools(students);
+            var studentLessons = LessonETLService. ProcessStudentDataV2(students, lessonDataAnalysis, codeBools);
             var excleFile = $"F:\\mock-data\\{time}.xlsx";
             var runtimePath = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
             XmlDocument xmlDocument = new XmlDocument();
             xmlDocument.Load($"{runtimePath}\\summary.xml");
             List<string> noStujson = new List<string>();
-            await LessonETLService.ExportToExcelLocal(lessonItems, excleFile, xmlDocument);
+            await LessonETLService.ExportToExcelLocal(studentLessons, excleFile, xmlDocument);
             try {
                 await System.IO.File.WriteAllTextAsync($"F:\\mock-data\\{time}.json", new { scount, ecount, qcount, icount, tcount, pcount, xcount, students }.ToJsonString());
             } catch (Exception ex) {

+ 15 - 199
TEAMModelOS.Extension/HTEX.Test/Program.cs

@@ -11,6 +11,7 @@ using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Reflection;
+using System.Text.Json;
 using System.Text.RegularExpressions;
 using TEAMModelOS.SDK;
 using TEAMModelOS.SDK.DI;
@@ -26,10 +27,10 @@ namespace HTEX.Test
     {
         public static async Task Main(string[] args)
         {
-            //Test2();
+            Test2();
 
            
-           await MockDataController.MockData();
+           //await MockDataController.MockData();
             //var builder = WebApplication.CreateBuilder(args);
 
             //// Add services to the container.
@@ -55,218 +56,33 @@ namespace HTEX.Test
 
             //app.Run();
         }
-        static double CalculateScore(int n, double k = 1)
-        {
-            if (n == 0)
-            {
-                return 0;
-            }
-            else
-            {
-                double score = 60 + (40 / (1 + Math.Exp(-k * (n - 1))));
-                return Math.Max(80, Math.Min(score, 100));
-            }
-        }
+        
 
-        public static string Test2() 
+        public static async Task<string> Test2() 
         {
             string? pathAnalysis = $"F:\\lesson-local\\analysis";
-            var filesAnalysis = FileHelper.ListAllFiles(pathAnalysis);
-            List<LessonDataAnalysisMonth> lessonDataAnalysisMonths = new List<LessonDataAnalysisMonth>();
-            LessonDataAnalysisCluster lessonDataAnalysisCluster = new LessonDataAnalysisCluster();
-            foreach (var file in filesAnalysis)
-            {
-                //读取每月的数据
-                if (file.EndsWith("-m-analysis.json"))
-                {
-                    string jsons =   System.IO.File.ReadAllText(file);
-                    LessonDataAnalysisMonth lessonDataAnalysisMonth = jsons.ToObject<LessonDataAnalysisMonth>();
-
-                    lessonDataAnalysisMonths.Add(lessonDataAnalysisMonth);
-                    if (lessonDataAnalysisMonth.task.IsNotEmpty())
-                    {
-                        lessonDataAnalysisCluster.task.AddRange(lessonDataAnalysisMonth.task);
-                    }
-                    if (lessonDataAnalysisMonth.irs.IsNotEmpty())
-                    {
-                        lessonDataAnalysisCluster.irs.AddRange(lessonDataAnalysisMonth.irs);
-                    }
-                    if (lessonDataAnalysisMonth.interactNormal.IsNotEmpty())
-                    {
-                        lessonDataAnalysisCluster.interactNormal.AddRange(lessonDataAnalysisMonth.interactNormal);
-                    }
-                    if (lessonDataAnalysisMonth.pscore.IsNotEmpty())
-                    {
-                        lessonDataAnalysisCluster.pscore.AddRange(lessonDataAnalysisMonth.pscore);
-                    }
-                    if (lessonDataAnalysisMonth.gscore.IsNotEmpty())
-                    {
-                        lessonDataAnalysisCluster.gscore.AddRange(lessonDataAnalysisMonth.gscore);
-                    }
-                    if (lessonDataAnalysisMonth.tscore.IsNotEmpty())
-                    {
-                        lessonDataAnalysisCluster.tscore.AddRange(lessonDataAnalysisMonth.tscore);
-                    }
-                    if (lessonDataAnalysisMonth.stuCowork.IsNotEmpty())
-                    {
-                        lessonDataAnalysisCluster.stuCowork.AddRange(lessonDataAnalysisMonth.stuCowork);
-                    }
-                    if (lessonDataAnalysisMonth.groupCowork.IsNotEmpty())
-                    {
-                        lessonDataAnalysisCluster.groupCowork.AddRange(lessonDataAnalysisMonth.groupCowork);
-                    }
-                }
-            }
-          
-            //标准差偏差N倍,视为异常数据
-            double thresholdMultiplier =2;
-            lessonDataAnalysisCluster.task= LessonETLService.CleanDataBySDThreshold(lessonDataAnalysisCluster.task.OrderBy(x => x), thresholdMultiplier);
-            lessonDataAnalysisCluster.pscore= LessonETLService.CleanDataBySDThreshold(lessonDataAnalysisCluster.pscore.OrderBy(x => x), thresholdMultiplier);
-            lessonDataAnalysisCluster.gscore= LessonETLService.CleanDataBySDThreshold(lessonDataAnalysisCluster.gscore.OrderBy(x => x), thresholdMultiplier);
-            lessonDataAnalysisCluster.tscore= LessonETLService.CleanDataBySDThreshold(lessonDataAnalysisCluster.tscore.OrderBy(x => x), thresholdMultiplier);
-            lessonDataAnalysisCluster.irs = LessonETLService.CleanDataBySDThreshold(lessonDataAnalysisCluster.irs.OrderBy(x => x), thresholdMultiplier);
-            lessonDataAnalysisCluster.interactNormal=LessonETLService.CleanDataBySDThreshold(lessonDataAnalysisCluster.interactNormal.OrderBy(x => x), thresholdMultiplier);
-            lessonDataAnalysisCluster.stuCowork=LessonETLService.CleanDataBySDThreshold(lessonDataAnalysisCluster.stuCowork.OrderBy(x => x), thresholdMultiplier);
-            lessonDataAnalysisCluster.groupCowork=LessonETLService.CleanDataBySDThreshold(lessonDataAnalysisCluster.groupCowork.OrderBy(x => x), thresholdMultiplier);
-             //超过60 80的
-            var all = lessonDataAnalysisCluster.interactNormal.Select(x =>  x).OrderBy(x => x).ToArray();
-            double n = all.Max()+1;
-            var clusterInteract = HTEX.Lib.ETL. KMeansService.KMeansOptimized(all,3);
-            //foreach (var item in clusterInteract.OrderBy(x => x.Key))
-            //{
-            //    lessonDataAnalysisCluster.clustersInteract.Add(new KeyValuePair<double, List<double>>(item.Value.Average(), item.Value));
-            //    Console.WriteLine($"dp:{item.Key} ,avg: {item.Value.Average()}, count: {item.Value.Count}, min:{item.Value.Min()}, max:{item.Value.Max()},weight:{item.Value.Count*1.0/d.Count()}");
-            //}
+            try {
+                string jsons = await System.IO.File.ReadAllTextAsync($"F:\\lesson-local\\analysis\\analysis-model.json");
+                var s = JsonSerializer.Deserialize<LessonDataAnalysisModel>(jsons);
+                LessonDataAnalysisModel lessonDataAnalysis =JsonDocument.Parse(jsons).RootElement.ToObject<LessonDataAnalysisModel>();
+                var per = LessonETLService.GetPersent(lessonDataAnalysis.irs, 2);
 
-           //IEnumerable<double> all = lessonDataAnalysisCluster.clustersInteract.SelectMany(x => x.Value);
-            int pass = 0;
-            for (var i = 1; i<n; i++) 
-            {
-                var p = LessonETLService.GetPersent(all, i);
-                if (p.persent>=60) 
-                {
-                    pass = i;
-                    break;
-                }
             }
-            var low = all.Where(x => x<pass).Average();
-            int good = 0;
-            for (var i = 1; i<n; i++)
-            {
-                var p = LessonETLService.GetPersent(all, i);
-                if (p.persent>=80)
-                {
-                    good = i;
-                    break;
-                }
+            catch (Exception ex) {
+                Console.WriteLine(ex.ToString());
             }
-            var medium = all.Where(x => x>=pass  &&x<good ).Average();
-            var high = all.Where(x => x>=good).Average();
-            lessonDataAnalysisCluster.interactGood=good;
-            lessonDataAnalysisCluster.interactPass=pass;
-            lessonDataAnalysisCluster.interactHigh=high;
-            lessonDataAnalysisCluster.interactMedium=medium;
-            lessonDataAnalysisCluster.interactLow=low;
-            List<KeyValuePair<double, List<double>>> levelInteract = new List<KeyValuePair<double, List<double>>>()
-            {
-                new KeyValuePair<double, List<double>>(low, all.Where(x => x<pass).ToList()),
-                new KeyValuePair<double, List<double>>(medium, all.Where(x => x>=pass  &&x<good).ToList() ),
-                new KeyValuePair<double, List<double>>(high, all.Where(x => x>=good).ToList())
-            };
-            lessonDataAnalysisCluster.levelInteract= levelInteract;
-            double ss = 0;
-            for (var i = 1; i<n; i++) 
-            {
-               // KeyValuePair<double, List<int>> curr = new KeyValuePair<double, List<int>>();
-                var s = lessonDataAnalysisCluster.levelInteract.FindAll(x => x.Value.Min()<=i  && x.Value.Max()>=i).MinBy(x=>x.Key);
-                var p = LessonETLService.GetPersent(all, i);
-                var l= i<pass?pass:i<good?pass:good;
-                var e = (i*1.0/l) *(p.persent)  *  (s.Value.Count*1.0/all.Count());
-                ss+= e;
-                Console.WriteLine($"n: {i},l: {l}, persent: {p.persent},count: {p.count},s-min: {s.Value.Min()},s-max: {s.Value.Max()},value: {e}");
-            }
-            //foreach (var s in clusterInteract.OrderBy(x => x.Key))
-            //{
-            //    clustersDataInteract.Add(s);
-            //}
-            //lessonDataAnalysisCluster.clustersInteract= clustersDataInteract;
-            System.IO.File.WriteAllText(Path.Combine(pathAnalysis, "analysis.json"), lessonDataAnalysisCluster.ToJsonString());
             return "";
         }
 
-        public static string Test()
-        {
-            
-            // 创建一个随机数生成器
-            Random random = new Random();
-
-            // 示例数据集
-            var data = Enumerable.Range(1, 25000).Select(i => Vector<double>.Build.Dense(i % 10, i % 10)).ToArray();
-          
-            return "Hello World!";
-        }
+        
          
-    static bool[] MarkAnomalies(List<int> array)
-        {
-            if (array.Count == 0) return new bool[0];
-
-            double average = array.Average();
-            double variance = array.Average(x => Math.Pow(x - average, 2));
-            double standardDeviation = Math.Sqrt(variance);
-
-            // 定义异常值的阈值,这里使用2倍标准差
-            double threshold =3* standardDeviation;
-
-            bool[] anomalies = new bool[array.Count];
-            for (int i = 0; i < array.Count; i++)
-            {
-                double deviation = Math.Abs(array[i] - average);
-                anomalies[i] = deviation > threshold;
-            }
-
-            return anomalies;
-        }
+     
 
        
        
         
-        /// <summary>
-        /// 计算当前元素在集合中超过了多少百分比的值
-        /// </summary>
-        /// <param name="nums"></param>
-        /// <param name="curr"></param>
-        /// <returns></returns>
-        public static double GetPersent(IEnumerable<double> nums, int curr)
-        {
-            int count = 0;
-            foreach (var op in nums.OrderBy(x => x))
-            {
-                if (op < curr)
-                {
-                    count++;
-                }
-                else if (op == curr)
-                {
-                    count++;
-                }
-                else
-                {
-                    break;
-                }
-            }
-            return count *1.0/ nums.Count() * 100;
-        }
 
 
-        public class KeyCount
-        {
-            public int count { get; set;}
-            public int key { get; set;}
-        }
-        class WeightedItem
-        {
-            public int Value { get; set; }
-            public double Weight { get; set; }
-        }
+         
     }
 } 

+ 0 - 8
TEAMModelOS.Extension/HTEX.Test/x.xml

@@ -4,14 +4,6 @@
         <name>HTEX.Test</name>
     </assembly>
     <members>
-        <member name="M:HTEX.Test.Program.GetPersent(System.Collections.Generic.IEnumerable{System.Double},System.Int32)">
-            <summary>
-            计算当前元素在集合中超过了多少百分比的值
-            </summary>
-            <param name="nums"></param>
-            <param name="curr"></param>
-            <returns></returns>
-        </member>
         <member name="M:HTEX.Test.Service.MLService.KMeans(System.Single[],System.Int32)">
             <summary>
             

+ 13 - 2
TEAMModelOS.Function/IESServiceBusTrigger.cs

@@ -1773,9 +1773,20 @@ namespace TEAMModelOS.Function
                                                 List<OverallEducation> overallEducations = new List<OverallEducation>();
                                                 List<Student> studentsBase = new List<Student>();
                                                 List<StudentSemesterRecord> students = new List<StudentSemesterRecord>();
-                                                LessonDataAnalysisCluster lessonDataAnalysis = null;
+                                                LessonDataAnalysisModel lessonDataAnalysis = null;
                                                 await LessonETLService.DoStudentLessonData(Constant.objectiveTypes, _azureStorage, lessonLocal, _dingDing, client, location,_azureRedis, studentSemesterRecords, overallEducations, lessonDataAnalysis, studentsBase, schools);
-                                                
+                                                await Parallel.ForEachAsync(studentSemesterRecords, async (studentSemester, cancellationToken) =>
+                                                {
+                                                    await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Student).UpsertItemAsync(studentSemester, new PartitionKey(studentSemester.code));
+
+                                                });
+                                                await Parallel.ForEachAsync(overallEducations, async (overallEducation, cancellationToken) =>
+                                                {
+                                                    await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Student).UpsertItemAsync(overallEducation, partitionKey: new PartitionKey(overallEducation.code));
+                                                    string key = $"OverallEducation:{overallEducation.schoolCode}:{overallEducation.periodId}:{overallEducation.year}:{overallEducation.semesterId}:{overallEducation?.classId}";
+                                                    await _azureRedis.GetRedisClient(8).HashSetAsync(key, overallEducation.studentId, overallEducation.ToJsonString());
+                                                    await _azureRedis.GetRedisClient(8).KeyExpireAsync(key, new TimeSpan(180 *24, 0, 0));
+                                                });
                                                 // 使用当前文化设置的日历
                                                 CultureInfo cultureInfo = CultureInfo.CurrentCulture;
                                                 Calendar calendar = cultureInfo.Calendar;

+ 3 - 3
TEAMModelOS.Function/TEAMModelOS.Function.csproj

@@ -5,9 +5,9 @@
     <OutputType>Exe</OutputType>
     <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
-	<Version>5.2411.6</Version>
-	<AssemblyVersion>5.2411.6.1</AssemblyVersion>
-	<FileVersion>5.2411.6.1</FileVersion>
+	<Version>5.2411.13</Version>
+	<AssemblyVersion>5.2411.13.1</AssemblyVersion>
+	<FileVersion>5.2411.13.1</FileVersion>
 	<PackageId>TEAMModelOS.FunctionV4</PackageId>
 	<Authors>teammodel</Authors>
 	<Company>醍摩豆(成都)信息技术有限公司</Company>

+ 4 - 1
TEAMModelOS.SDK/Models/Cosmos/Common/LessonRecord.cs

@@ -230,7 +230,10 @@ namespace TEAMModelOS.SDK.Models
         /// 课例来源 0 本公司  1 第三方公司    是因支持VR/AR那边课例
         /// </summary>
         public int source { get; set; } = 0;
-
+        /// <summary>
+        /// -1 代表不能进行分析的数据。未定义和 0未分析的数据, 1代表已分析的数据
+        /// </summary>
+        public int analysis { get; set; }
     }
 
     /*

+ 5 - 1
TEAMModelOS.SDK/Models/Cosmos/School/SchoolSetting.cs

@@ -75,11 +75,15 @@ namespace TEAMModelOS.SDK.Models
     }
     public class CodeLong
     {
-        
         public long value { get; set; }
         public string code { get; set; }
 
     }
+    public class CodeBool
+    {
+        public bool value { get; set; }
+        public string code { get; set; }
+    }
     public class CodeDouble
     {
         //attitude cooperate  ability  standard

+ 10 - 10
TEAMModelOS.SDK/Models/Service/SystemService.cs

@@ -644,19 +644,19 @@ public static string weeklyReportEN = @"<!DOCTYPE html>
                                     lessonBase = basejson.ToObject<LessonBase>();
                                     lessonCount++;
                                     var grpEngagement = string.Join(",", lessonBase.summary?.grpEngagement.Select((x, index) => $"G{index+1}({x})"));
-                                    var highRankEngagement = string.Join(",", lessonBase.summary?.highRankEngagement?.Select((x, index) => $"S{index+1}({x})"));
-
-                                    var highRankPerPoint = string.Join(",", lessonBase.summary?.highRankPerPoint?.Select((x, index) => $"S{index+1}({x})"));
-                                    var highRankGrpPoint = string.Join(",", lessonBase.summary?.highRankGrpPoint?.Select((x, index) => $"S{index+1}({x})"));
-                                    var highRankExam = string.Join(",", lessonBase.summary?.highRankExam?.Select((x, index) => $"S{index+1}({x})"));
-                                    var lowRankExam = string.Join(",", lessonBase.summary?.lowRankExam?.Select((x, index) => $"S{index+1}({x})"));
+                                    var highRankEngagement = string.Join(",", lessonBase.summary?.highRankEngagement?.Select((x, index) => $"{x}"));
+                                    var highRankPerPoint = string.Join(",", lessonBase.summary?.highRankPerPoint?.Select((x, index) => $"{x}"));
+                                    var highRankGrpPoint = string.Join(",", lessonBase.summary?.highRankGrpPoint?.Select((x, index) => $"{x}"));
+                                    var highRankExam = string.Join(",", lessonBase.summary?.highRankExam?.Select((x, index) => $"{x}"));
+                                    var lowRankExam = string.Join(",", lessonBase.summary?.lowRankExam?.Select((x, index) => $"{x}"));
+                                    DateTimeOffset time = DateTimeOffset.FromUnixTimeMilliseconds(lessonRecord.startTime).GetGMTTime((int)teacher.timezone);
                                     switch (lang)
                                     {
                                         case "zh-cn":
                                             if (lessonBase!=null)
                                             {
-
-                                                lessonDetailSB.Append(cn_lessonDetail.Replace("{name}", lessonRecord.name).Replace("{groupCount}",$"{lessonBase.group.Count()}").Replace("{time}", DateTimeOffset.FromUnixTimeMilliseconds(lessonRecord.startTime).ToString("yyyy-MM-dd HH:mm:ss"))
+                                                
+                                                lessonDetailSB.Append(cn_lessonDetail.Replace("{name}", lessonRecord.name).Replace("{groupCount}",$"{lessonBase.group.Count()}").Replace("{time}",time.ToString("yyyy-MM-dd HH:mm:ss"))
                                                     .Replace("{duration}", $"{Math.Round(lessonRecord.duration/60, 2)}").Replace("{count}", $"{lessonRecord.clientCount}").Replace("{attendCount}", $"{lessonRecord.attendCount}")
                                                     .Replace("{absentCount}", $"{lessonRecord.clientCount-lessonRecord.attendCount}").Replace("{attendRate}", $"{lessonRecord.attendRate}").Replace("{engagementIndexAverge}", $"{lessonBase.summary?.engagementIndexAverge}")
                                                     .Replace("{grpEngagement}", grpEngagement).Replace("{highRankEngagement}", highRankEngagement).Replace("{totalPoint}", $"{lessonBase.summary.totalPoint}").Replace("{highRankPerPoint}", highRankPerPoint)
@@ -672,7 +672,7 @@ public static string weeklyReportEN = @"<!DOCTYPE html>
                                         case "zh-tw":
                                             if (lessonBase!=null)
                                             {
-                                                lessonDetailSB.Append(tw_lessonDetail.Replace("{name}", lessonRecord.name).Replace("{groupCount}", $"{lessonBase.group.Count()}").Replace("{time}", DateTimeOffset.FromUnixTimeMilliseconds(lessonRecord.startTime).ToString("yyyy-MM-dd HH:mm:ss"))
+                                                lessonDetailSB.Append(tw_lessonDetail.Replace("{name}", lessonRecord.name).Replace("{groupCount}", $"{lessonBase.group.Count()}").Replace("{time}", time.ToString("yyyy-MM-dd HH:mm:ss"))
                                                     .Replace("{duration}", $"{Math.Round(lessonRecord.duration/60, 2)}").Replace("{count}", $"{lessonRecord.clientCount}").Replace("{attendCount}", $"{lessonRecord.attendCount}")
                                                     .Replace("{absentCount}", $"{lessonRecord.clientCount-lessonRecord.attendCount}").Replace("{attendRate}", $"{lessonRecord.attendRate}").Replace("{engagementIndexAverge}", $"{lessonBase.summary?.engagementIndexAverge}")
                                                     .Replace("{grpEngagement}", grpEngagement).Replace("{highRankEngagement}", highRankEngagement).Replace("{totalPoint}", $"{lessonBase.summary.totalPoint}").Replace("{highRankPerPoint}", highRankPerPoint)
@@ -689,7 +689,7 @@ public static string weeklyReportEN = @"<!DOCTYPE html>
                                             if (lessonBase!=null)
                                             {
 
-                                                lessonDetailSB.Append(en_lessonDetail.Replace("{name}", lessonRecord.name).Replace("{groupCount}", $"{lessonBase.group.Count()}").Replace("{time}", DateTimeOffset.FromUnixTimeMilliseconds(lessonRecord.startTime).ToString("yyyy-MM-dd HH:mm:ss"))
+                                                lessonDetailSB.Append(en_lessonDetail.Replace("{name}", lessonRecord.name).Replace("{groupCount}", $"{lessonBase.group.Count()}").Replace("{time}", time.ToString("yyyy-MM-dd HH:mm:ss"))
                                                     .Replace("{duration}", $"{Math.Round(lessonRecord.duration/60, 2)}").Replace("{count}", $"{lessonRecord.clientCount}").Replace("{attendCount}", $"{lessonRecord.attendCount}")
                                                     .Replace("{absentCount}", $"{lessonRecord.clientCount-lessonRecord.attendCount}").Replace("{attendRate}", $"{lessonRecord.attendRate}").Replace("{engagementIndexAverge}", $"{lessonBase.summary?.engagementIndexAverge}")
                                                     .Replace("{grpEngagement}", grpEngagement).Replace("{highRankEngagement}", highRankEngagement).Replace("{totalPoint}", $"{lessonBase.summary.totalPoint}").Replace("{highRankPerPoint}", highRankPerPoint)

+ 3 - 3
TEAMModelOS.SDK/TEAMModelOS.SDK.csproj

@@ -1,9 +1,9 @@
 <Project Sdk="Microsoft.NET.Sdk">
 	<PropertyGroup>
 		<TargetFramework>net8.0</TargetFramework>
-		<Version>5.2411.6</Version>
-		<AssemblyVersion>5.2411.6.1</AssemblyVersion>
-		<FileVersion>5.2411.6.1</FileVersion>
+		<Version>5.2411.13</Version>
+		<AssemblyVersion>5.2411.13.1</AssemblyVersion>
+		<FileVersion>5.2411.13.1</FileVersion>
 		<PackageReleaseNotes>发版</PackageReleaseNotes>
 	</PropertyGroup>
 

+ 134 - 80
TEAMModelOS/ClientApp/src/common/BaseQuickArtPaper.vue

@@ -20,7 +20,7 @@
 					{{ grade }}
 				</Option>
 			</Select>
-			<Select v-model="paperInfo.subjectIndex" style="width: 250px; margin-right: 15px; margin-top: 15px">
+			<Select v-model="paperInfo.subjectIndex" @on-change="getTags()" style="width: 250px; margin-right: 15px; margin-top: 15px">
 				<Option v-for="(subject, index) in subjectList" :value="index" :key="index">
 					{{ subject.name }}
 				</Option>
@@ -46,7 +46,7 @@
 			<!-- <p class="upper-tip">* {{ $t("evaluation.quickPaper.onlyUpper") }}</p> -->
 			<div class="add-type-list">
 				<span class="type-item" @click="doAddItem()">添加题目</span>
-				<span class="type-item" @click="fenpeiscore()" style="color: #b0b0b0;">手动配分</span>
+				<span class="type-item" @click="fenpeiscore()" style="background-color: #b0b0b0;">手动配分</span>
 			</div>
 			<div class="items">
 				<div v-if="!orderItemsArr.length" style="margin: 20px 10px; color: #999999">{{ $t("evaluation.quickPaper.tip5") }}</div>
@@ -71,6 +71,12 @@
 								</span>
 							</span>
 						</span>
+						<span style="margin: 0 10px">关联标签:</span>
+						<Select v-model="item.tags" clearable style="width: 100px; margin-right: 15px;">
+							<Option v-for="(tag, index) in tags" :value="tag" :key="index">
+								{{ tag }}
+							</Option>
+						</Select>
 						<span style="margin-left: 10px; cursor: pointer" @click="doRemoveItem(index)"><Icon type="md-trash" color="#FA8C16" size="18" /></span>
 					</div>
 				</div>
@@ -83,10 +89,7 @@
 </template>
 
 <script>
-	import JsPDF from "jspdf";
 	import blobTool from "@/utils/blobTool.js";
-	import * as pdfjs from "pdfjs-dist";
-	import * as pdfjsWorker from "pdfjs-dist/build/pdf.worker.entry";
 	export default {
 		props: {},
 		data() {
@@ -110,6 +113,7 @@
 				checkItemIndex: -1,
 				paperNum: 1,
 				paperList: [],
+				tags: [],
 			};
 		},
 		created() {
@@ -172,6 +176,7 @@
 					score: 10,
 					useAutoScore: false,
 					knowledge: [],
+					tags: '',
 				});
 			},
 			/* 移除试题 */
@@ -184,6 +189,7 @@
 				this.subjectList = this.schoolInfo.period[val].subjects;
 				this.paperInfo.gradeIndex = [];
 				this.paperInfo.subjectIndex = 0;
+				this.getTags()
 			},
 			/* 获取题目选项数据 */
 			getItemOptions(typeItem) {
@@ -264,6 +270,14 @@
 					this.$Message.warning('题目未选择知识点!');
 					return;
 				}
+				let itemScore = 0
+				this.orderItemsArr.forEach(item => {
+					itemScore += item.score
+				})
+				if(itemScore != this.paperInfo.score) {
+					this.$Message.warning('试卷配分与试卷总分不一致');
+					return;
+				}
 				/* 根据每个题目的 题型+知识点 查询相关题目,分配给每份试卷
 				   题目数量 < 试卷数量 :循环分配给试卷,保证每份试卷题目一致
 				   题目数量 = 0 :未创建成功,在对应题目序号后提示该题没有题目,请重新选择
@@ -279,7 +293,7 @@
 						"@DESC": "createTime",
 						code: this.$store.state.userInfo.schoolCode,
 						periodId: this.schoolInfo.period[periodIndex].id,
-						"gradeIds[*]": gradeIndex.length ? gradeIndex.map((i) => i + "") : this.gradeList.map((i, index) => index + ""),
+						"gradeIds[*]": gradeIndex.length ? gradeIndex.map((i) => i + "") : [],
 						subjectId: this.subjectList[subjectIndex].id,
 						scope: 'school',
 						"knowledge[*]": [],
@@ -288,7 +302,7 @@
 					this.paperList = Array.from(Array(this.paperNum).keys()).map(v => {
 						return {
 							name: `${this.paperInfo.name}(${v + 1})`,
-							quesList: [],
+							quesList: new Array(this.orderItemsArr.length)
 						}
 					})
 					let qIndex = await this.getPaperItem(this.orderItemsArr, params)
@@ -327,7 +341,7 @@
 					itemArr.forEach((item, index) => {
 						params['type'] = [item.type] || []
 						params['knowledge[*]'] = item.knowledge || []
-						params['tags[*]'] = [] || [] 
+						params['tags[*]'] = item.tags ? [item.tags] : []
 						promiseArr.push(new Promise((r, j) => {
 							this.$api.newEvaluation.FindExerciseList(params).then(res => {
 								if(res.items.length) {
@@ -337,7 +351,7 @@
 									// 需获取题目的json文件
 									this.paperList.forEach((paper, pIndex) => {
 										let qIndex = pIndex + 1 > res.items.length ? ((pIndex + 1) % res.items.length) - 1 : pIndex
-										paper.quesList.push(res.items[qIndex === -1 ? (res.items.length - 1) : qIndex])
+										paper.quesList.splice(index, 1, res.items[qIndex === -1 ? (res.items.length - 1) : qIndex])
 									})
 									r(-1)
 								} else {
@@ -409,6 +423,7 @@
 			},
 			/* 保存数据流程 */
 			async doSavePaper(containerClient) {
+				this.$parent.$parent.isLoading = true
 				this.$parent.$parent.quickLoading = true;
 				let { periodIndex, subjectIndex, gradeIndex } = this.paperInfo;
 				let periodId = this.schoolInfo.period[periodIndex].id
@@ -418,83 +433,108 @@
 				let sasData = await this.$tools.getSchoolSas()
 				let blobHost = this.$evTools.getBlobHost()
 
+				let promiseArr = []
 				this.paperList.forEach(async paper => {
-					let res = await this.savePaperItems(paper.quesList)
-					let paperId = this.$tools.guid();
-					let paperItem = {
-						id: paperId,
-						name: paper.name,
-						itemSort: 1,
-						isNumOption: 0,
-						multipleRule: 1,
-						qamode: 0,
-						tags: [this.paperInfo.tags],
-						points: this.getPaperPoints(paper.quesList),
-						scoring: this.getAnswers(paper.quesList),
-						periodId: periodId,
-						gradeIds: gradeIds,
-						subjectId: subjectId,
-						subjectName: subjectName,
-						secret: this.isSecret ? 1 : 0,
-						score: this.paperInfo.score,
-						code: this.$store.state.userInfo.schoolCode,
-						scope: "school",
-						blob: `/paper/${this.paperInfo.name}`
-					}
-					let blobFile = null
-					let blobPaper = await this.$evTools.createBlobPaper(paperItem, res.slides)
-					let paperFile = new File([JSON.stringify(blobPaper)], "index.json")
-					try {
-						let schoolBlob = new blobTool(sasData.url, sasData.name, sasData.sas, 'school')
-						for (let i = 0; i < res.files.length; i++) {
-							try {
-								let item = res.files[i]
-								await containerClient.copyFolder('paper/' + paperItem.name + '/', 'item/' + item.id, schoolBlob, null, false)
-							} catch (e) {
-								console.log(e)
-							}
-						}
-						blobFile = await containerClient.upload(paperFile, {
-							path: 'paper/' + paperItem.name,
-							checkSize: false
+					promiseArr.push(new Promise((resolve, reject) => {
+						resolve(this.doSaveOnePaper(paper, sasData, blobHost, containerClient))
+					}))
+				})
+				Promise.allSettled(promiseArr).then(result => {
+					let paperArr = result.filter(i => i.status === 'rejected')
+					if(paperArr.length) {
+						let message = ''
+						paperArr.forEach(item => {
+							message = (message ? '、' : '') + item.value
 						})
-						console.log('上传到试卷目录下', blobFile)
-						let noSaveArr = JSON.parse(localStorage.getItem('noSave'))
-						if (noSaveArr && noSaveArr.length) {
-							noSaveArr.forEach(async i => {
-								if (!i.path) {
-								} else {
-									let containerName = this.$store.state.userInfo.schoolCode
-									let mediaFullPath = blobHost + '/' + containerName + i.path
-									let fileName = i.path.split('/')[i.path.split('/').length - 1]
-									await schoolBlob.copyBlob('paper/' + paperItem.name + '/' + fileName, mediaFullPath, sasData.sas)
-								}
-							})
+						this.$Message.warning(`${message}未保存成功`)
+					} else {
+						this.$Message.success(this.$t('result.tip4'))
+					}
+					this.$emit("finish")
+				})
+			},
+			async doSaveOnePaper(paper, sasData, blobHost, containerClient) {
+				let { periodIndex, subjectIndex, gradeIndex } = this.paperInfo;
+				let periodId = this.schoolInfo.period[periodIndex].id
+				let subjectId = this.subjectList[subjectIndex].id
+				let subjectName = this.subjectList[subjectIndex].name
+				let gradeIds = gradeIndex.length ? gradeIndex.map((i) => i + "") : this.gradeList.map((i, index) => index + "")
+				let res = await this.savePaperItems(paper.quesList)
+				let paperId = this.$tools.guid();
+				let paperItem = {
+					id: paperId,
+					name: paper.name,
+					itemSort: 1,
+					isNumOption: 0,
+					multipleRule: 1,
+					qamode: 0,
+					tags: this.paperInfo.tags ? [this.paperInfo.tags] : [],
+					points: this.getPaperPoints(paper.quesList),
+					scoring: this.getAnswers(paper.quesList),
+					periodId: periodId,
+					gradeIds: gradeIds,
+					subjectId: subjectId,
+					subjectName: subjectName,
+					secret: this.isSecret ? 1 : 0,
+					score: this.paperInfo.score,
+					code: this.$store.state.userInfo.schoolCode,
+					scope: "school",
+					blob: `/paper/${this.paperInfo.name}`
+				}
+				let blobFile = null
+				let blobPaper = await this.$evTools.createBlobPaper(paperItem, res.slides)
+				let paperFile = new File([JSON.stringify(blobPaper)], "index.json")
+				try {
+					let schoolBlob = new blobTool(sasData.url, sasData.name, sasData.sas, 'school')
+					for (let i = 0; i < res.files.length; i++) {
+						try {
+							let item = res.files[i]
+							await containerClient.copyFolder('paper/' + paperItem.name + '/', 'item/' + item.id, schoolBlob, null, false)
+						} catch (e) {
+							console.log(e)
 						}
-						// 检查试卷目录下是否有 已被移除的试题 有则进行删除操作
-						let blobList = await this.getPaperFiles('paper/' + paperItem.name + '/')
-						let files = blobList.blobList.map(i => {
-							return {
-								blob: i.blob,
-								size: i.size
+					}
+					blobFile = await containerClient.upload(paperFile, {
+						path: 'paper/' + paperItem.name,
+						checkSize: false
+					})
+					console.log('上传到试卷目录下', blobFile)
+					let noSaveArr = JSON.parse(localStorage.getItem('noSave'))
+					if (noSaveArr && noSaveArr.length) {
+						noSaveArr.forEach(async i => {
+							if (!i.path) {
+							} else {
+								let containerName = this.$store.state.userInfo.schoolCode
+								let mediaFullPath = blobHost + '/' + containerName + i.path
+								let fileName = i.path.split('/')[i.path.split('/').length - 1]
+								await schoolBlob.copyBlob('paper/' + paperItem.name + '/' + fileName, mediaFullPath, sasData.sas)
 							}
 						})
-						await this.deleteNoUseItem(files, res.files)
-						if (blobFile.blob) {
-							// 试卷保存更新后需要重新生成答题卡
-							paperItem.blob = blobFile.blob.split('/index.json')[0]
-							let params = {
-								paper: await this.$evTools.createCosmosPaper(paperItem),
-								option: 'insert'
-							}
-							//  保存试卷到cosmos
-							await this.saveOnePaper(params)
+					}
+					// 检查试卷目录下是否有 已被移除的试题 有则进行删除操作
+					let blobList = await this.getPaperFiles('paper/' + paperItem.name + '/')
+					let files = blobList.blobList.map(i => {
+						return {
+							blob: i.blob,
+							size: i.size
+						}
+					})
+					await this.deleteNoUseItem(files, res.files)
+					if (blobFile.blob) {
+						// 试卷保存更新后需要重新生成答题卡
+						paperItem.blob = blobFile.blob.split('/index.json')[0]
+						let params = {
+							paper: await this.$evTools.createCosmosPaper(paperItem),
+							option: 'insert'
 						}
-					} catch (error) {
-						console.log('9999999999', error);
+						//  保存试卷到cosmos
+						await this.saveOnePaper(params)
+						return ''
 					}
-				})
-				console.log('88888888888888', );
+				} catch (error) {
+					console.log('9999999999', error);
+					return paper.name
+				}
 			},
 			async savePaperItems(list, sasData) {
 				// 获取初始化Blob需要的数据
@@ -683,12 +723,26 @@
 					i.score = scoreArr[index] || 0
 				})
 			},
+			getTags() {
+				let { periodIndex, subjectIndex, gradeIndex } = this.paperInfo;
+				let params = {
+					"@DESC": "createTime",
+					code: this.$store.state.userInfo.schoolCode,
+					periodId: [this.schoolInfo.period[periodIndex].id],
+					subjectId: [this.subjectList[subjectIndex].id],
+					scope: 'school',
+					pid: null,
+				}
+				this.$api.newEvaluation.getQuesTags(params).then(res => {
+					this.tags = res?.tags || []
+				})
+			},
 		},
 		computed: {
 			isSchool() {
 				return this.$route.name === "schoolBank" || this.$route.name === 'newSchoolPaper'
 			},
-		}
+		},
 	};
 </script>
 <style>

+ 430 - 0
TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseBar.vue

@@ -0,0 +1,430 @@
+<template>
+  <div id="myBar"></div>
+</template>
+
+<script>
+export default {
+  name: 'hello',
+  props: {
+    echartsData: {
+      type: Array,
+      default: () => []
+    },
+    subjectIndex: {
+      type: Number,
+      default: NaN
+    }
+  },
+  data() {
+    return {
+      subjectList: [],
+      subjectSeries: [],
+      subjectColors: ['#db615e', '#db8dd2', '#42beda', '#1EB58D', '#99ccff', '#dbaf4f'],
+      lineData: 440,
+      areaData: 490.5,
+      curSubjectIndex: NaN
+    }
+  },
+
+  methods: {
+    // 处理动态配置文件
+    handleOptions(val) {
+      let subjectList = val.datas
+      this.subjectSeries = []
+
+      // 配置 柱状图的 Series 根据科目 配置
+      subjectList.forEach((item, index) => {
+        let seriesItem = {
+          name: item.name,
+          type: 'bar',
+          stack: subjectList[0].name,
+          legendHoverLink: true,
+          itemStyle: {
+            color: this.subjectColors[index],
+            width: 10
+          },
+          barMaxWidth: 40,
+          data: item.score
+        }
+        this.subjectSeries.push(seriesItem)
+      })
+
+      // 配置学校校级平均分折线图Series
+      let schoolLine = {
+        name: this.$t('totalAnalysis.ach_text5'),
+        type: 'line',
+        itemStyle: {
+          color: '#66ff99',
+          width: 2
+        },
+        symbol: 'none',
+        lineStyle: {
+          type: 'dashed',
+          width: 0
+        },
+        markLine: {
+          data: [
+            { type: 'average' }
+          ],
+          lineStyle: {
+            color: '#66ff99',
+            type: 'dashed',
+            width: 2
+          }
+        }, // 平均分为各科平均分总和
+        data: [...Array(val.classList.length)].map(() => val.subjectAverage.map(item => Number(item)).reduce(function (pre, cur) { return pre + cur }).toFixed(2))
+      }
+
+      // 配置区级平均分折线图Series
+      let areaLine = {
+        name: this.$t('totalAnalysis.ach_text6'),
+        type: 'line',
+        itemStyle: {
+          color: '#11C2EE',
+          width: 2
+        },
+        symbol: 'none',
+        lineStyle: {
+          type: 'dashed',
+          width: 0
+        },
+        markLine: {
+          data: [
+            { type: 'average' }
+          ],
+          lineStyle: {
+            color: '#11C2EE',
+            type: 'dashed',
+            width: 2
+          }
+        },
+        data: [...Array(val.classList.length)].map(() => 190)
+      }
+
+      this.subjectSeries.push(schoolLine)
+      this.subjectSeries.push(areaLine)
+
+      this.drawLine(val)
+    },
+
+    // 执行图表渲染工作
+    drawLine(data) {
+      console.log('BaseBar', this.subjectSeries)
+      // 基于准备好的dom,初始化echarts实例
+      let myBar = this.$echarts.init(document.getElementById('myBar'))
+      let that = this
+      // 指定图表的配置项和数据
+      var option = {
+        legend: {
+          // data: data.datas.map(item => item.name).concat([this.$t('totalAnalysis.ach_text5'), this.$t('totalAnalysis.ach_text6')]),
+          data: this.subjectSeries.map(item => item.name).concat([this.$t('totalAnalysis.ach_text5'), this.$t('totalAnalysis.ach_text6')]),
+          textStyle: {
+            color: '#303030'
+          }
+        },
+        // ---  提示框 ----
+        tooltip: {
+          show: true, // 是否显示提示框,默认为true
+          trigger: 'axis', // 数据项图形触发
+          axisPointer: {
+            type: 'shadow',
+            axis: 'auto',
+            shadowStyle: {
+              color: 'rgba(128,128,128,0.1)'
+            }
+          },
+          padding: 5,
+          textStyle: {
+            color: '#fff'
+          },
+          formatter: function (value) {
+            let arr = value[0].name + '<br>'
+            let sum = 0
+            for (var i = 0; i < value.length; i++) {
+              var datalist = value[i].marker + value[i].seriesName + ' : ' + value[i].data.toFixed(2) + '<br>'
+              // if (i === value.length - 1 || i === value.length - 2) {
+              //     sum += 0
+              // } else {
+              //     sum += Number(value[i].data)
+              // }
+              sum += Number(value[i].data)
+              arr += datalist
+            };
+            return arr + value[value.length - 1].marker + that.$t('totalAnalysis.ach_text4') + ':' + sum.toFixed(2)
+          }
+
+        },
+        grid: {
+          show: false,
+          containLabel: true,
+          height: 300,
+          width: '75%',
+          right: '18%',
+          tooltip: {
+            show: true,
+            trigger: 'axis', // 触发类型
+            textStyle: {
+              color: '#666'
+            }
+          }
+        },
+        dataZoom: [{
+          'show': true,
+          'height': 10,
+          'xAxisIndex': [
+            0
+          ],
+          bottom: 10,
+          'start': 0,
+          'end': 100,
+          handleIcon: 'M512 497.821538m-418.264615 0a418.264615 418.264615 0 1 0 836.52923 0 418.264615 418.264615 0 1 0-836.52923 0Z',
+          handleSize: '160%',
+          handleStyle: {
+            color: '#d3dee5'
+
+          },
+          textStyle: {
+            color: '#fff'
+          },
+          // borderColor: '#C3C3C3',
+          borderRadius: '5px'
+        }],
+        xAxis: {
+          show: true, // 是否显示
+          position: 'bottom', // x轴的位置
+          type: 'category', // 轴类型, 默认为 'category'
+          name: '月份', // 轴名称
+          nameLocation: 'end', // 轴名称相对位置
+          nameTextStyle: {
+            color: 'transparent',
+            padding: [5, 0, 10, -5]
+          },
+          nameGap: 35, // 坐标轴名称与轴线之间的距离
+          nameRotate: 0, // 坐标轴名字旋转
+          axisLine: {
+            show: false, // 是否显示
+            symbol: ['none', 'arrow'], // 是否显示轴线箭头
+            symbolSize: [8, 8], // 箭头大小
+            symbolOffset: [0, 7], // 箭头位置
+            lineStyle: {
+              color: '#6aabef',
+              width: 1,
+              type: 'solid'
+            }
+          },
+          axisTick: {
+            // 坐标轴 刻度
+            show: false, // 是否显示
+            inside: true, // 是否朝内
+            length: 3, // 长度
+            lineStyle: {
+              // 默认取轴线的样式
+              color: '#989898',
+              width: 2,
+              type: 'solid'
+            }
+          },
+          axisLabel: {
+            // 坐标轴标签
+            show: true, // 是否显示
+            inside: false, // 是否朝内
+            rotate: 45, // 旋转角度
+            margin: 15, // 刻度标签与轴线之间的距离
+            color: '#989898', // 默认取轴线的颜色,
+            fontSize: 14,
+            fontFamily: 'Microsoft YaHei'
+          },
+          splitLine: {
+            show: false,
+            lineStyle: {
+              color: '#C3C3C3'
+            }
+          },
+          splitArea: {
+            show: false // 是否显示,默认为false
+          },
+          data: data.classes.map(i => i.className)
+        },
+        yAxis: {
+          show: true, // 是否显示
+          position: 'left', // y轴位置
+          offset: 0, // y轴相对于默认位置的偏移
+          type: 'value', // 轴类型,默认为 ‘category’
+          nameLocation: 'end', // 轴名称相对位置value
+          nameTextStyle: {
+            color: '#fff',
+            padding: [5, 0, 0, 5] // 坐标轴名称相对位置
+          },
+          nameGap: 15, // 坐标轴名称与轴线之间的距离
+          nameRotate: 270, // 坐标轴名字旋转
+
+          axisLine: {
+            show: false, // 是否显示
+            symbol: ['none', 'arrow'], // 是否显示轴线箭头
+            symbolSize: [8, 8], // 箭头大小
+            symbolOffset: [0, 7], // 箭头位置
+            lineStyle: {
+              color: '#6aabef',
+              width: 1,
+              type: 'solid'
+            }
+          },
+          axisTick: {
+            show: false, // 是否显示
+            inside: true, // 是否朝内
+            length: 3, // 长度
+            lineStyle: {
+              color: '#989898', // 默认取轴线的颜色
+              width: 2,
+              type: 'solid'
+            }
+          },
+          axisLabel: {
+            show: true, // 是否显示
+            inside: false, // 是否朝内
+            rotate: 0, // 旋转角度
+            margin: 8, // 刻度标签与轴线之间的距离
+            color: '#989898', // 默认轴线的颜色
+            fontSize: 12,
+            fontFamily: 'Microsoft YaHei'
+          },
+          splitLine: {
+            show: true,
+            lineStyle: {
+              color: '#C3C3C3',
+              width: 0.5,
+              type: 'solid'
+            }
+          }
+        },
+        series: this.subjectSeries
+      }
+
+      // 绘制图表
+      myBar.clear()
+      myBar.setOption(option)
+      window.addEventListener('resize', function () {
+        myBar.resize()
+      })
+
+      // 通过hook监听组件销毁钩子函数,并取消监听事件
+      that.$once('hook:beforeDestroy', () => {
+        window.removeEventListener('resize', () => { console.log('监听销毁') })
+      })
+
+
+      // 渲染完成事件
+      myBar.on('finished', function () {
+        // console.log("Render Finish!!!")
+      })
+
+      // 点击图例 根据展示的科目来计算最新的 校级和区级平均总和
+      myBar.on('legendselectchanged', function (obj) {
+        var selected = obj.selected
+        let arr = []
+        for (let i in selected) {
+          let o = {}
+          o[i] = selected[i]
+          arr.push(o)
+        }
+        arr.length = data.datas.length
+
+        let emptyNum = 0 // 不显示的科目数量
+        let schoolArr = []
+        let areaArr = []
+        arr.forEach(item => {
+          for (let key in item) {
+            // 如果当前科目是展示状态
+            if (item[key]) {
+              schoolArr.push(Number(data.datas.filter(item => item.name === key)[0].schoolAverage))
+              areaArr.push(Number(data.datas.filter(item => item.name === key)[0].areaAverage) + 15)
+            } else {
+              emptyNum++
+            }
+          }
+        })
+        // 如果都不显示 则参考线全部直接置为0
+        if (emptyNum == arr.length) {
+          option.series[option.series.length - 2].data = [...Array(data.classList.length).keys()].map(item => 0)
+          option.series[option.series.length - 1].data = [...Array(data.classList.length).keys()].map(item => 0)
+          myBar.setOption(option)
+        } else {
+          // 如果展示 则计算展示科目的平均分之和
+          let newSchoolAverage = schoolArr.reduce(function (pre, cur) { return pre + cur }).toFixed(2)
+          let newAreaAverage = areaArr.reduce(function (pre, cur) { return pre + cur }).toFixed(2)
+          // 计算完赋值最新的 平均分折线 值
+          option.series[option.series.length - 2].data = [...Array(data.classList.length).keys()].map(item => newSchoolAverage)
+          option.series[option.series.length - 1].data = [...Array(data.classList.length).keys()].map(item => newAreaAverage)
+          myBar.setOption(option)
+        }
+      })
+    },
+
+
+    // 获取当前学科下每个班级的平均分
+    getSeriesData(val, subjectIndex) {
+      let result = []
+      val.classes.forEach(j => {
+        result.push(j.subjects[subjectIndex].average)
+      })
+      return result
+    },
+
+    doRender(val) {
+      let subjectList = val.classes[0].subjects
+      this.subjectSeries = []
+      // 配置 柱状图的 Series 根据科目 配置
+      subjectList.forEach((item, index) => {
+        let seriesItem = {
+          name: item.name,
+          type: 'bar',
+          stack: !isNaN(this.curSubjectIndex) ? item.name : subjectList[0].name,
+          legendHoverLink: true,
+          itemStyle: {
+            color: this.subjectColors[index],
+            width: 10
+          },
+          barMaxWidth: 30,
+          data: this.getSeriesData(val, index)
+        }
+        // 如果是全科情况 或者 是当前单科情况 则加入
+        if (isNaN(this.curSubjectIndex) || (!isNaN(this.curSubjectIndex) && this.curSubjectIndex === index)) {
+          this.subjectSeries.push(seriesItem)
+        }
+      })
+      this.drawLine(val)
+    }
+  },
+  mounted() {
+    if (this.getBaseBarData) {
+      console.log('BaseBar接收到的vuex', this.getBaseBarData)
+      this.doRender(this.getBaseBarData)
+    }
+  },
+  computed: {
+    // 获取最新柱状图数据
+    getBaseBarData() {
+      return this.$store.state.totalAnalysis.analysisJsonJoint
+    }
+  },
+  watch: {
+    subjectIndex: {
+      handler(n, o) {
+        console.log(n)
+        this.curSubjectIndex = n
+        this.doRender(this.getBaseBarData)
+      },
+    }
+  }
+}
+</script>
+
+<style scoped>
+#myBar {
+  width: 100%;
+  height: 400px;
+  margin: 0 auto;
+  display: block;
+  margin-top: 20px;
+}
+</style>

+ 350 - 0
TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseEntryBar.vue

@@ -0,0 +1,350 @@
+<template>
+  <div class="entryNumberBar" :id="echartsId"></div>
+</template>
+
+<script>
+export default {
+  props: ['echartData', 'echartsId'],
+  data() {
+    return {
+    }
+  },
+
+  methods: {
+    drawLine(data) {
+      console.log('进线人数统计数据', data)
+      // 基于准备好的dom,初始化echarts实例
+      let myBar = this.$echarts.init(document.getElementById(this.echartsId))
+
+      // 指定图表的配置项和数据
+      var option = {
+        legend: {
+          //  data:['班级进线人数','班级总人数']
+          textStyle: {
+            color: '#303030'
+          }
+        },
+        tooltip: {
+          show: true, // 是否显示提示框,默认为true
+          trigger: 'axis', // 数据项图形触发
+          axisPointer: {
+            // 指示样式
+            type: 'shadow',
+            axis: 'auto',
+            shadowStyle: {
+              color: 'rgba(128,128,128,0.1)'
+            }
+          },
+          padding: 5,
+          textStyle: {
+            // 提示框内容的样式
+            color: '#fff'
+          },
+          formatter: function (params) {
+            var results = params[0].name + '<br>'
+            for (var i = 0; i < params.length; i++) {
+              if (params[i].seriesType === 'line') {
+                results += params[i].marker + params[i].seriesName + ':' + params[i].data + '%<br>'
+              } else {
+                results += params[i].marker + params[i].seriesName + ':' + params[i].data + '<br>'
+              }
+            }
+            return results
+          }
+        },
+        grid: {
+          show: false,
+          containLabel: true,
+          height: 300,
+          width: '75%',
+          right: '18%',
+          tooltip: {
+            show: true,
+            trigger: 'axis', // 触发类型
+            textStyle: {
+              color: '#666'
+            }
+          }
+        },
+        dataZoom: [{
+          'show': true,
+          'height': 10,
+          'xAxisIndex': [
+            0
+          ],
+          bottom: 10,
+          'start': 0,
+          'end': 100,
+          handleIcon: 'M512 497.821538m-418.264615 0a418.264615 418.264615 0 1 0 836.52923 0 418.264615 418.264615 0 1 0-836.52923 0Z',
+          handleSize: '160%',
+          handleStyle: {
+            color: '#d3dee5'
+
+          },
+          textStyle: {
+            color: '#fff'
+          },
+          // borderColor: '#eee'
+        }],
+        xAxis: {
+          show: true, // 是否显示
+          position: 'bottom', // x轴的位置
+          offset: 0, // x轴相对于默认位置的偏移
+          type: 'category', // 轴类型, 默认为 'category'
+          name: '月份', // 轴名称
+          nameLocation: 'end', // 轴名称相对位置
+          nameTextStyle: {
+            color: 'transparent',
+            padding: [5, 0, 10, -5]
+          },
+          nameGap: 35, // 坐标轴名称与轴线之间的距离
+          nameRotate: 50, // 坐标轴名字旋转
+          axisLine: {
+            show: false, // 是否显示
+            symbol: ['none', 'arrow'], // 是否显示轴线箭头
+            symbolSize: [8, 8], // 箭头大小
+            symbolOffset: [0, 7], // 箭头位置
+            lineStyle: {
+              color: '#6aabef',
+              width: 1,
+              type: 'solid'
+            }
+          },
+          axisTick: {
+            // 坐标轴 刻度
+            show: false, // 是否显示
+            inside: true, // 是否朝内
+            length: 3, // 长度
+            lineStyle: {
+              // 默认取轴线的样式
+              color: '#989898',
+              width: 2,
+              type: 'solid'
+            }
+          },
+          axisLabel: {
+            // 坐标轴标签
+            show: true, // 是否显示
+            inside: false, // 是否朝内
+            margin: 15,
+            rotate: 45,
+            color: '#989898' // 默认取轴线的颜色,
+          },
+          splitLine: {
+            show: false,
+            lineStyle: {
+              color: '#595959'
+            }
+          },
+          splitArea: {
+            show: false // 是否显示,默认为false
+          },
+          data: data.map(item => item.name)
+        },
+        yAxis: [
+          {
+            show: true, // 是否显示
+            offset: 0, // y轴相对于默认位置的偏移
+            type: 'value', // 轴类型,默认为 ‘category’
+            nameLocation: 'end', // 轴名称相对位置value
+            nameTextStyle: {
+              color: '#fff',
+              padding: [5, 0, 0, 5] // 坐标轴名称相对位置
+            },
+            nameGap: 15, // 坐标轴名称与轴线之间的距离
+            nameRotate: 270, // 坐标轴名字旋转
+
+            axisLine: {
+              show: false, // 是否显示
+              lineStyle: {
+                color: '#595959',
+                width: 1,
+                type: 'solid'
+              }
+            },
+            axisTick: {
+              show: false, // 是否显示
+              inside: true, // 是否朝内
+              length: 3, // 长度
+              lineStyle: {
+                color: '#989898', // 默认取轴线的颜色
+                width: 2,
+                type: 'solid'
+              }
+            },
+            axisLabel: {
+              show: true, // 是否显示
+              inside: false, // 是否朝内
+              rotate: 0, // 旋转角度
+              margin: 8, // 刻度标签与轴线之间的距离
+              color: '#989898', // 默认轴线的颜色
+              fontSize: 12,
+              fontFamily: 'Microsoft YaHei'
+            },
+            splitLine: {
+              show: true,
+              lineStyle: {
+                color: '#C3C3C3',
+                width: 0.5,
+                type: 'solid'
+              },
+              axisLabel: {
+                show: true, // 是否显示
+                inside: false, // 是否朝内
+                rotate: 0, // 旋转角度
+                margin: 8, // 刻度标签与轴线之间的距离
+                color: '#fff', // 默认轴线的颜色
+                fontSize: 12,
+                fontFamily: 'Microsoft YaHei'
+              }
+            }
+          },
+          {
+            show: true,
+            type: 'value',
+            min: 0,
+            splitLine: {
+              show: false,
+              lineStyle: {
+                color: '#595959'
+              }
+            },
+            nameTextStyle: {
+              color: '#999'
+            },
+            axisLine: {
+              show: false,
+            },
+            axisLabel: {
+              show: true, // 是否显示
+              inside: false, // 是否朝内
+              rotate: 0, // 旋转角度
+              margin: 8, // 刻度标签与轴线之间的距离
+              color: '#989898', // 默认轴线的颜色
+              fontSize: 12,
+              fontFamily: 'Microsoft YaHei',
+              formatter: function (value) {
+                return value + '%'
+              }
+            },
+            splitArea: {
+              show: false
+            }
+          }],
+        series: [
+          {
+            name: this.$t('totalAnalysis.ach_text7'),
+            type: 'bar',
+            itemStyle: {
+              color: '#bdbdbd',
+              width: 20
+            },
+            barMaxWidth: 40,
+            data: data.map(item => item.totalNum)
+          },
+          {
+            name: this.$t('totalAnalysis.ach_text8'),
+            type: 'bar',
+            itemStyle: {
+              /* color: '#42beda',
+              width: 20, */
+
+              color: {
+                type: 'linear',
+                x: 0,
+                y: 0,
+                x2: 0,
+                y2: 1,
+                colorStops: [{
+                  offset: 0,
+                  color: '#3DC3F0' // 0% 处的颜色
+                }, {
+                  offset: 1,
+                  color: '#3486b1' // 100% 处的颜色
+                }]
+              }
+            },
+            barGap: '-100%',
+            barMaxWidth: 40,
+            data: data.map(item => item.entryNum)
+          },
+          {
+            name: this.$t('totalAnalysis.ach_text9'),
+            type: 'line',
+            color: '#58b100',
+            itemStyle: {
+              color: '#58b100',
+              borderColor: '#efefef',
+              borderWidth: 3
+            },
+            symbolSize: 10,
+            yAxisIndex: 1,
+            lineStyle: {
+              normal: {
+                width: 2,
+                color: '#58b100',
+                shadowColor: 'rgba(245,128,128, 0.5)',
+                shadowBlur: 10,
+                shadowOffsetY: 7
+              }
+            },
+            barMaxWidth: 30,
+            data: data.map(item => item.overAverageRate)
+          }
+        ]
+      }
+
+      // 绘制图表
+      myBar.setOption(option)
+      window.addEventListener('resize', function () {
+        myBar.resize()
+      })
+    },
+
+    getEntryBarData(analysisJson) {
+      let result = []
+      analysisJson.classes.forEach((classItem, classIndex) => {
+        let totalNum = classItem.stuCount
+        let entryNum = classItem.lineCount
+        result.push({
+          name: classItem.className,
+          totalNum: totalNum,
+          entryNum: entryNum,
+          overAverageRate: ((entryNum / totalNum) * 100).toFixed(2)
+        })
+      })
+      return result
+    }
+  },
+  mounted() {
+    if (this.getAnalysisJson) {
+      this.drawLine(this.getEntryBarData(this.getAnalysisJson))
+    }
+  },
+  computed: {
+    // 获取最新柱状图数据
+    getAnalysisJson() {
+      return this.$store.state.totalAnalysis.analysisJsonJoint
+    }
+  },
+  watch: {
+    getAnalysisJson(val) {
+      if (val) {
+        this.$nextTick(() => {
+          this.drawLine(this.getEntryBarData(val))
+        })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.entryNumberBar {
+  width: 100%;
+  height: 400px;
+  margin: 0 auto;
+  display: block;
+  margin-top: 20px;
+}
+</style>

+ 285 - 0
TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseKnowledgeDetail.vue

@@ -0,0 +1,285 @@
+<template>
+  <div :id="echartsId" class="knowledgeBar"></div>
+</template>
+
+<script>
+export default {
+  name: 'hello',
+  props: ['echartsId', 'classIndex'],
+  data() {
+    return {
+      knowledgeData: [],
+      x: [],
+      y: [],
+      activeItemIndex: 0,
+      curClassIndex: -1,
+    }
+  },
+
+  created() {
+
+  },
+
+  methods: {
+    drawLine() {
+      let _this = this
+      // 基于准备好的dom,初始化echarts实例
+      let myBar = this.$echarts.init(document.getElementById(this.echartsId))
+      // 指定图表的配置项和数据
+      var option = {
+        tooltip: {
+          show: true, // 是否显示提示框,默认为true
+          trigger: 'axis', // 数据项图形触发
+          axisPointer: {
+            // 指示样式
+            type: 'shadow',
+            axis: 'auto',
+            shadowStyle: {
+              color: 'rgba(128,128,128,0.1)'
+            }
+          },
+          padding: 15,
+          textStyle: {
+            // 提示框内容的样式
+            color: '#fff'
+          },
+          formatter: function (value) {
+            // console.log(value);
+            return value[0].name + ' : ' + value[0].data + '%'
+          }
+        },
+        grid: {
+          show: false, // 是否显示直角坐标系网格
+          top: 80, // 相对位置 top\bottom\left\right
+          left: '0',
+          width: '90%',
+          height: 440,
+          containLabel: true // gird 区域是否包含坐标轴的刻度标签
+        },
+        dataZoom: [{
+          'show': true,
+          'height': 10,
+          'xAxisIndex': [
+            0
+          ],
+          bottom: 36,
+          'start': 0,
+          'end': 100,
+          handleIcon: 'M512 497.821538m-418.264615 0a418.264615 418.264615 0 1 0 836.52923 0 418.264615 418.264615 0 1 0-836.52923 0Z',
+          handleSize: '160%',
+          handleStyle: {
+            color: '#d3dee5'
+
+          },
+          textStyle: {
+            color: '#fff'
+          },
+          borderColor: '#90979c'
+        }],
+        xAxis: {
+          show: true, // 是否显示
+          position: 'bottom', // x轴的位置
+          offset: 0, // x轴相对于默认位置的偏移
+          type: 'category', // 轴类型, 默认为 'category'
+          name: '月份', // 轴名称
+          nameLocation: 'end', // 轴名称相对位置
+          nameTextStyle: {
+            color: 'transparent',
+            padding: [5, 0, 10, -5]
+          },
+          nameGap: 35, // 坐标轴名称与轴线之间的距离
+          nameRotate: 0, // 坐标轴名字旋转
+          axisLabel: {
+            // 坐标轴标签
+            show: true, // 是否显示
+            inside: false, // 是否朝内
+            margin: 15,
+            rotate: 60,
+            color: '#989898' // 默认取轴线的颜色,
+          },
+          splitLine: {
+            show: false,
+            lineStyle: {
+              color: '#4c504a'
+            }
+          },
+          splitArea: {
+            show: false // 是否显示,默认为false
+          },
+          data: this.x
+        },
+        yAxis: {
+          show: true, // 是否显示
+          position: 'left', // y轴位置
+          offset: 0, // y轴相对于默认位置的偏移
+          type: 'value', // 轴类型,默认为 ‘category’
+          nameLocation: 'end', // 轴名称相对位置value
+          nameTextStyle: {
+            color: '#fff',
+            padding: [5, 0, 0, 5] // 坐标轴名称相对位置
+          },
+          nameGap: 15, // 坐标轴名称与轴线之间的距离
+          nameRotate: 270, // 坐标轴名字旋转
+          axisLine: {
+            show: false, // 是否显示
+            lineStyle: {
+              color: '#595959',
+              width: 1,
+              type: 'solid'
+            }
+          },
+          axisLabel: {
+            show: true, // 是否显示
+            inside: false, // 是否朝内
+            rotate: 0, // 旋转角度
+            margin: 8, // 刻度标签与轴线之间的距离
+            color: '#989898', // 默认轴线的颜色
+            fontSize: 12,
+            formatter: '{value} %'
+
+          },
+          splitLine: {
+            show: true,
+            lineStyle: {
+              color: '#4c504a',
+              width: 0.5,
+              type: 'solid'
+            }
+          }
+        },
+        series: [
+          {
+            type: 'bar',
+            animation: false,
+            itemStyle: {
+              color: function (params) {
+                var key = params.dataIndex
+                if (key === _this.activeItemIndex) {
+                  let colorDemo1 = {
+                    type: 'linear',
+                    x: 0,
+                    y: 0,
+                    x2: 0,
+                    y2: 1,
+                    colorStops: [{
+                      offset: 0,
+                      color: '#ff9999' // 0% 处的颜色
+                    }, {
+                      offset: 1,
+                      color: '#eb8181' // 100% 处的颜色
+                    }]
+                  }
+                  return colorDemo1
+                  // return '#ff9999'
+                } else {
+                  let colorDemo = {
+                    type: 'linear',
+                    x: 0,
+                    y: 0,
+                    x2: 0,
+                    y2: 1,
+                    colorStops: [{
+                      offset: 0,
+                      color: '#3DC3F0' // 0% 处的颜色
+                    }, {
+                      offset: 1,
+                      color: '#3486b1' // 100% 处的颜色
+                    }]
+                  }
+                  return colorDemo
+                }
+              },
+            },
+            barMaxWidth: 40,
+            emphasis: {
+              itemStyle: {
+                color: '#ff9999'
+              }
+            },
+            data: this.y
+          }]
+      }
+
+      // 绘制图表
+      myBar.setOption(option)
+      myBar.resize()
+      window.addEventListener('resize', function () {
+        myBar.resize()
+      })
+
+      let that = this
+      myBar.on('click', function (params) {
+        that.activeItemIndex = params.dataIndex
+        that.$emit('handleItemClick', params)
+        myBar.setOption(option)
+      })
+
+
+    },
+
+    doRender(data, classIndexs) {
+      let classIndex = this.getCurClassIndex
+      // 区分全部班级以及单个班级的数据
+      this.x = data.pointList
+      this.y = classIndex === -1 ? data.stupercent.grade.map(item => (Number(item)).toFixed(2)) : data.pointList.map(pointName => data.classpercent[pointName][classIndex].toFixed(2))
+      this.drawLine()
+    }
+  },
+
+  mounted() {
+    if (this.getKnowledgeData) {
+      // this.classList = [this.$t('totalAnalysis.allClasses')].concat([...new Set(this.getAnalysisJson.classes.map(item => item.className))]) // 获取班级列表
+      this.doRender(this.getKnowledgeData, this.curClassIndex)
+    }
+
+    this.$EventBus.$on('onCollapseChange', val => {
+      const myBar = this.$echarts.init(document.getElementById(this.echartsId))
+      myBar.setOption({
+        grid: {
+          width: val ? '90%' : '80%'
+        }
+      })
+    })
+  },
+
+  computed: {
+    // 获取最新散点图数据
+    getKnowledgeData() {
+      let curclassIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.name).indexOf(this.$store.state.totalAnalysis.currentSubjectJoint)
+      let curJson = this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curclassIndex].pointKey
+      return curJson
+    },
+    getAnalysisJson() {
+      return this.$store.state.totalAnalysis.analysisJsonJoint
+    },
+    getCurClassIndex() {
+      return this.$store.state.totalAnalysis.curClassIndex
+    }
+  },
+
+  watch: {
+    getKnowledgeData: {
+      handler(val) {
+        if (val) {
+          this.doRender(val, this.curClassIndex)
+        }
+      }
+    },
+    classIndex(n, o) {
+      console.log('柱状图变化了', n - 1)
+      this.curClassIndex = n - 1
+      this.doRender(this.getKnowledgeData, n - 1)
+    },
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+.knowledgeBar {
+  width: 100%;
+  height: 600px;
+  margin: 0 auto;
+  display: block;
+}
+</style>

+ 318 - 0
TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseLevelDetail.vue

@@ -0,0 +1,318 @@
+<template>
+  <div :id="echartsId" class="knowledgeBar"></div>
+</template>
+
+<script>
+export default {
+  name: 'hello',
+  props: ['echartsId', 'classIndex'],
+  data() {
+    return {
+      knowledgeData: [],
+      x: [],
+      y: [],
+      activeItemIndex: 0,
+      curClassIndex: -1,
+    }
+  },
+
+  created() {
+
+  },
+
+  methods: {
+    drawLine() {
+      let _this = this
+      // 基于准备好的dom,初始化echarts实例
+      let myBar = this.$echarts.init(document.getElementById(this.echartsId))
+      // 指定图表的配置项和数据
+      var option = {
+        tooltip: {
+          show: true, // 是否显示提示框,默认为true
+          trigger: 'axis', // 数据项图形触发
+          axisPointer: {
+            // 指示样式
+            type: 'shadow',
+            axis: 'auto',
+            shadowStyle: {
+              color: 'rgba(128,128,128,0.1)'
+            }
+          },
+          padding: 15,
+          textStyle: {
+            // 提示框内容的样式
+            color: '#fff'
+          },
+          formatter: function (value) {
+            // console.log(value);
+            return value[0].name + ' : ' + value[0].data + '%'
+          }
+        },
+        grid: {
+          show: false, // 是否显示直角坐标系网格
+          top: 80, // 相对位置 top\bottom\left\right
+          left: '0',
+          width: '90%',
+          height: 440,
+          containLabel: true // gird 区域是否包含坐标轴的刻度标签
+        },
+        dataZoom: [{
+          'show': true,
+          'height': 10,
+          'xAxisIndex': [
+            0
+          ],
+          bottom: 36,
+          'start': 0,
+          'end': 100,
+          handleIcon: 'M512 497.821538m-418.264615 0a418.264615 418.264615 0 1 0 836.52923 0 418.264615 418.264615 0 1 0-836.52923 0Z',
+          handleSize: '160%',
+          handleStyle: {
+            color: '#d3dee5'
+
+          },
+          textStyle: {
+            color: '#fff'
+          },
+          borderColor: '#90979c'
+        }],
+        xAxis: {
+          show: true, // 是否显示
+          position: 'bottom', // x轴的位置
+          offset: 0, // x轴相对于默认位置的偏移
+          type: 'category', // 轴类型, 默认为 'category'
+          name: '月份', // 轴名称
+          nameLocation: 'end', // 轴名称相对位置
+          nameTextStyle: {
+            color: 'transparent',
+            padding: [5, 0, 10, -5]
+          },
+          nameGap: 35, // 坐标轴名称与轴线之间的距离
+          nameRotate: 0, // 坐标轴名字旋转
+          axisLabel: {
+            // 坐标轴标签
+            show: true, // 是否显示
+            inside: false, // 是否朝内
+            margin: 15,
+            rotate: 60,
+            color: '#989898' // 默认取轴线的颜色,
+          },
+          splitLine: {
+            show: false,
+            lineStyle: {
+              color: '#4c504a'
+            }
+          },
+          splitArea: {
+            show: false // 是否显示,默认为false
+          },
+          data: this.x
+        },
+        yAxis: {
+          show: true, // 是否显示
+          position: 'left', // y轴位置
+          offset: 0, // y轴相对于默认位置的偏移
+          type: 'value', // 轴类型,默认为 ‘category’
+          nameLocation: 'end', // 轴名称相对位置value
+          nameTextStyle: {
+            color: '#fff',
+            padding: [5, 0, 0, 5] // 坐标轴名称相对位置
+          },
+          nameGap: 15, // 坐标轴名称与轴线之间的距离
+          nameRotate: 270, // 坐标轴名字旋转
+          axisLine: {
+            show: false, // 是否显示
+            lineStyle: {
+              color: '#595959',
+              width: 1,
+              type: 'solid'
+            }
+          },
+          axisLabel: {
+            show: true, // 是否显示
+            inside: false, // 是否朝内
+            rotate: 0, // 旋转角度
+            margin: 8, // 刻度标签与轴线之间的距离
+            color: '#989898', // 默认轴线的颜色
+            fontSize: 12,
+            formatter: '{value} %'
+
+          },
+          splitLine: {
+            show: true,
+            lineStyle: {
+              color: '#4c504a',
+              width: 0.5,
+              type: 'solid'
+            }
+          }
+        },
+        series: [
+          {
+            type: 'bar',
+            animation: false,
+            itemStyle: {
+              color: function (params) {
+                var key = params.dataIndex
+                if (key === _this.activeItemIndex) {
+                  let colorDemo1 = {
+                    type: 'linear',
+                    x: 0,
+                    y: 0,
+                    x2: 0,
+                    y2: 1,
+                    colorStops: [{
+                      offset: 0,
+                      color: '#ff9999' // 0% 处的颜色
+                    }, {
+                      offset: 1,
+                      color: '#eb8181' // 100% 处的颜色
+                    }]
+                  }
+                  return colorDemo1
+                  // return '#ff9999'
+                } else {
+                  let colorDemo = {
+                    type: 'linear',
+                    x: 0,
+                    y: 0,
+                    x2: 0,
+                    y2: 1,
+                    colorStops: [{
+                      offset: 0,
+                      color: '#3DC3F0' // 0% 处的颜色
+                    }, {
+                      offset: 1,
+                      color: '#3486b1' // 100% 处的颜色
+                    }]
+                  }
+                  return colorDemo
+                }
+              },
+            },
+            barMaxWidth: 40,
+            emphasis: {
+              itemStyle: {
+                color: '#ff9999'
+              }
+            },
+            data: this.y
+          }]
+      }
+
+      // 绘制图表
+      myBar.setOption(option)
+      myBar.resize()
+      window.addEventListener('resize', function () {
+        myBar.resize()
+      })
+
+      let that = this
+      myBar.on('click', function (params) {
+        that.activeItemIndex = params.dataIndex
+        that.$emit('handleItemClick', params)
+        myBar.setOption(option)
+      })
+
+
+    },
+
+    doRender(data, classIndexs) {
+      let classIndex = this.getCurClassIndex
+      console.log(classIndex)
+      // 区分全部班级以及单个班级的数据
+      this.x = data.pointList
+      this.y = classIndex === -1 ? data.stupercent.grade.map(item => (Number(item)).toFixed(2)) : data.pointList.map(pointName => data.classpercent[pointName][classIndex].toFixed(2))
+      this.drawLine()
+    },
+
+    doSubjectChange(subject) {
+      let curclassIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.name).indexOf(subject)
+      let curJson = this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curclassIndex].levelKey
+      let transArr = this.$GLOBAL.EXERCISE_LEVELS()
+      curJson.pointList = curJson.pointList.map((i, index) => transArr[index])
+      for (let key in curJson.classpercent) {
+        if (!isNaN(key)) {
+          let newKey = transArr[+key - 1]
+          curJson.classpercent[newKey] = curJson.classpercent[key]
+          curJson.stupercent[newKey] = curJson.stupercent[key]
+        }
+      }
+      return curJson
+    }
+  },
+
+  mounted() {
+    if (this.getKnowledgeData) {
+      // this.classList = [this.$t('totalAnalysis.allClasses')].concat([...new Set(this.getAnalysisJson.classes.map(item => item.className))]) // 获取班级列表
+      this.doRender(this.getKnowledgeData, this.curClassIndex)
+    }
+
+    this.$EventBus.$on('onCollapseChange', val => {
+      const myBar = this.$echarts.init(document.getElementById(this.echartsId))
+      myBar.setOption({
+        grid: {
+          width: val ? '90%' : '80%'
+        }
+      })
+    })
+  },
+
+  computed: {
+    // 获取最新散点图数据
+    getKnowledgeData() {
+      let curclassIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.name).indexOf(this.$store.state.totalAnalysis.currentSubjectJoint)
+      let curJson = this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curclassIndex].levelKey
+      let transArr = this.$GLOBAL.EXERCISE_LEVELS()
+      curJson.pointList = curJson.pointList.map((i, index) => transArr[index])
+      for (let key in curJson.classpercent) {
+        if (!isNaN(key)) {
+          let newKey = transArr[+key - 1]
+          curJson.classpercent[newKey] = curJson.classpercent[key]
+          curJson.stupercent[newKey] = curJson.stupercent[key]
+        }
+      }
+      return curJson
+    },
+    getCurSubject() {
+      return this.$store.state.totalAnalysis.currentSubjectJoint
+    },
+    getAnalysisJson() {
+      return this.$store.state.totalAnalysis.analysisJsonJoint
+    },
+    getCurClassIndex() {
+      return this.$store.state.totalAnalysis.curClassIndex
+    }
+  },
+
+  watch: {
+    getCurSubject(n, o) {
+      console.log(n, o)
+      this.doRender(this.doSubjectChange(n), this.curClassIndex)
+    },
+    // getKnowledgeData: {
+    //     handler(n,o) {
+    //         if (n) {
+    // console.log(n,o)
+    //             this.doRender(n,this.curClassIndex)
+    //         }
+    //     }
+    // },
+    classIndex(n, o) {
+      console.log('柱状图变化了', n - 1)
+      this.curClassIndex = n - 1
+      this.doRender(this.getKnowledgeData, n - 1)
+    },
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+.knowledgeBar {
+  width: 100%;
+  height: 600px;
+  margin: 0 auto;
+  display: block;
+}
+</style>

+ 123 - 0
TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseLevelPie.vue

@@ -0,0 +1,123 @@
+<template>
+	<div class="myLevelPie" :id="pieId"></div>
+</template>
+
+<script>
+	export default {
+		name: 'BasePie',
+		props: ['pieId'],
+		data() {
+			return {
+				pieData: []
+			}
+		},
+		methods: {
+
+			drawLine(data) {
+				let that = this
+
+				// 基于准备好的dom,初始化echarts实例
+				let myLevelPie = this.$echarts.init(document.getElementById(this.pieId), 'chalk')
+
+				// 指定图表的配置项和数据
+				var option = {
+					tooltip: {
+						trigger: 'item',
+						formatter: '{a} <br/>{b} : {c} ({d}%)'
+					},
+					calculable: true,
+					roseType: true,
+					series: {
+						name: that.$t('totalAnalysis.le_title1'),
+						type: 'pie',
+						radius: [40, 140],
+						center: ['45%', '55%'],
+						max: 100, // for funnel
+						sort: 'ascending', // for funnel
+						data: data,
+                        emphasis: {
+                            label: {
+                                show: true,
+                                fontSize: '16',
+                                fontWeight: 'bold'
+                            },
+                            itemStyle: {
+                                shadowBlur: 10,
+                                shadowOffsetX: 0,
+                                shadowColor: 'rgba(0, 0, 0, 0.3)'
+                            }
+                        },
+					}
+				}
+
+				// 绘制图表
+				myLevelPie.setOption(option)
+				window.addEventListener('resize', function() {
+					myLevelPie.resize()
+				})
+			}
+		},
+		mounted() {
+			if (this.getPieData) {
+				let pointList = this.getPieData.pointList
+				let valList = this.getPieData.per
+				let arr = []
+				pointList.forEach((item, index) => {
+					let o = {}
+					o.name = item
+					o.value = valList[index].replace('%', '')
+					arr.push(o)
+				})
+				this.drawLine(arr)
+			}
+		},
+		computed: {
+			// 获取最新知识点占比饼图数据
+			getPieData() {
+				let curSubjectIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.name).indexOf(this.$store.state
+					.totalAnalysis.currentSubjectJoint)
+				let levelJson = this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].levelKey
+				let transArr = this.$GLOBAL.EXERCISE_LEVELS()
+				levelJson.pointList = levelJson.pointList.map((i, index) => transArr[index])
+				for (let key in levelJson.classpercent) {
+					if (key !== 'className') {
+						let newKey = transArr[+key - 1]
+						levelJson.classpercent[newKey] = levelJson.classpercent[key]
+					}
+				}
+				return levelJson
+			},
+			getKnowledgeData() {
+				let curSubjectIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.name).indexOf(this.$store.state
+					.totalAnalysis.currentSubjectJoint)
+				return this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].pointKey
+			},
+		},
+		watch: {
+			getKnowledgeData(val) {
+				if (!val) return
+				let pointList = this.getPieData.pointList
+				let valList = this.getPieData.per
+				let arr = []
+				pointList.forEach((item, index) => {
+					let o = {}
+					o.name = item
+					o.value = valList[index].replace('%', '')
+					arr.push(o)
+				})
+				this.drawLine(arr)
+			}
+		}
+
+	}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+	.myLevelPie {
+		width: 100%;
+		height: 380px;
+		margin: 0 auto;
+		display: block;
+	}
+</style>

+ 288 - 0
TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseLineBar.vue

@@ -0,0 +1,288 @@
+<template>
+  <div id="stuAverageBar2"></div>
+</template>
+
+<script>
+export default {
+  name: 'hello',
+  props: ['echartData', 'exerciseIndex'],
+  data() {
+    return {
+      echartDatas: [],
+      gradeSeries: [],
+      gradeLengend: [],
+      colorArr: ['#ff9f7f', '#37d2da', '#d4e676', '#ff9f7f', '#d4e676', '#ff9f7f', '#fb7293'],
+      currentExerciseIndex: 0
+    }
+  },
+  created() { },
+  methods: {
+    drawLine(echartData, exerciseIndex) {
+      console.log('年级得分率图表数据', echartData)
+      console.log(this.gradeSeries)
+      let that = this
+      let myBar = this.$echarts.init(document.getElementById('stuAverageBar2'))
+      var option = {
+        legend: {
+          top: '7%',
+          data: [{
+            name: that.$t('totalAnalysis.ta_chart_text4'),
+            textStyle: {
+              color: '#9d9d9d'
+            }
+          }].concat(this.gradeLengend)
+
+        },
+        tooltip: {
+          show: true, // 是否显示提示框,默认为true
+          trigger: 'axis', // 数据项图形触发
+          axisPointer: {
+            // 指示样式
+            type: 'shadow',
+            axis: 'auto',
+            shadowStyle: {
+              color: 'rgba(128,128,128,0.1)'
+            }
+          },
+          padding: 5,
+          textStyle: {
+            color: '#fff'
+          },
+          formatter: function (params) {
+            let result = params[0].name
+            for (let i = 0; i < params.length; i++) {
+              result += '<br>' + params[i].marker + params[i].seriesName + ':' + params[i].data + '%'
+            }
+            return result
+          }
+        },
+        grid: {
+          show: false,
+          containLabel: true,
+          height: 400,
+          width: '88%',
+          left: '3%',
+          bottom: '12%',
+          tooltip: {
+            show: true,
+            trigger: 'axis', // 触发类型
+            textStyle: {
+              color: '#666'
+            }
+          }
+        },
+        dataZoom: [{
+          'show': true,
+          'height': 10,
+          'xAxisIndex': [
+            0
+          ],
+          bottom: 30,
+          'start': 0,
+          'end': 100,
+          handleIcon: 'M512 497.821538m-418.264615 0a418.264615 418.264615 0 1 0 836.52923 0 418.264615 418.264615 0 1 0-836.52923 0Z',
+          handleSize: '160%',
+          handleStyle: {
+            color: '#899195'
+
+          },
+          textStyle: {
+            color: '#767676'
+          },
+          borderColor: '#90979c'
+        }],
+        xAxis: {
+          show: true,
+          offset: 0,
+          type: 'category',
+          axisLabel: {
+            show: true,
+            inside: false,
+            margin: 15,
+            color: '#989898'
+          },
+          data: echartData.map(item => item.className)
+        },
+        yAxis: {
+          show: true,
+          type: 'value',
+          min: 0,
+          max: 100,
+          axisLabel: {
+            show: true,
+            inside: false,
+            rotate: 0,
+            margin: 8,
+            color: '#989898',
+            formatter: '{value} %',
+            fontSize: 12,
+            fontFamily: 'Microsoft YaHei'
+          },
+          splitLine: {
+            show: true,
+            lineStyle: {
+              color: '#4c504a',
+              width: 0.5,
+              type: 'solid'
+            }
+          }
+        },
+        series: [{
+          name: that.$t('totalAnalysis.ta_chart_text4'),
+          type: 'bar',
+          itemStyle: {
+            normal: { // 渐变色
+              color: {
+                type: 'linear',
+                x: 0,
+                y: 0,
+                x2: 0,
+                y2: 1,
+                colorStops: [{
+                  offset: 0,
+                  color: '#3DC3F0' // 0% 处的颜色
+                }, {
+                  offset: 1,
+                  color: '#3486b1' // 100% 处的颜色
+                }]
+              }
+            },
+            width: 20
+          },
+          markPoint: {
+            data: [{
+              type: 'max',
+              name: that.$t('totalAnalysis.ta_chart_text7')
+            },
+            {
+              type: 'min',
+              name: that.$t('totalAnalysis.ta_chart_text8')
+            }
+            ]
+          },
+          barMaxWidth: 40,
+          data: echartData.map(item => item.classScoreRate)
+        }].concat(this.gradeSeries)
+      }
+
+      // 绘制图表
+      myBar.setOption(option)
+      window.addEventListener('resize', function () {
+        myBar.resize()
+      })
+    },
+
+    doAddGradeSeries(analysisJson, curSubjectIndex, echartsData) {
+      this.gradeSeries = []
+      this.gradeLengend = []
+      analysisJson.grades.forEach((grade, gradeIndex) => {
+        let seriesItem = {
+          name: grade.gradeName + this.$t('totalAnalysis.ta_table_text8'),
+          type: 'line',
+          itemStyle: {
+            color: this.colorArr[gradeIndex],
+            width: 0
+          },
+          symbol: 'none',
+          lineStyle: {
+            type: 'dashed',
+            width: 0
+          },
+          markLine: {
+            data: [{
+              type: 'average'
+            }],
+            lineStyle: {
+              color: this.colorArr[gradeIndex],
+              type: 'dashed'
+            }
+          },
+          data: new Array(analysisJson.classes.length).fill(grade.subjects[curSubjectIndex].item[this.currentExerciseIndex])
+        }
+        this.gradeSeries.push(seriesItem)
+        this.gradeLengend.push({
+          name: grade.gradeName + this.$t('totalAnalysis.ta_table_text8'),
+          textStyle: {
+            color: '#999999'
+          }
+        })
+        console.log(this.gradeLengend)
+        console.log(this.gradeSeries[0].data)
+
+      })
+
+      this.drawLine(echartsData)
+    },
+
+    // 调整图表所需数据结构格式
+    renderData(data) {
+      console.log(this.currentExerciseIndex)
+      let analysisJson = JSON.parse(JSON.stringify(this.getAnalysisJson))
+      let curSubjectIndex = analysisJson.subjects.map(i => i.name).indexOf(this.$store.state.totalAnalysis.currentSubjectJoint)
+      let result = []
+      analysisJson.paper[curSubjectIndex].value.forEach((exercise, exerciseIndex) => {
+        let obj = {}
+        analysisJson.paperKey.forEach((key, index) => {
+          obj[key] = exercise[index]
+        })
+        result.push(obj)
+      })
+
+
+      let echartsData = []
+      analysisJson.classes.forEach((classItem, classIndex) => {
+        echartsData.push({
+          className: classItem.className,
+          classScoreRate: classItem.subjects[curSubjectIndex].item[this.currentExerciseIndex]
+        })
+      })
+
+      this.doAddGradeSeries(analysisJson, curSubjectIndex, echartsData)
+    },
+
+    // 监听变化处理重新渲染
+    doRender(data) {
+      let analysisJson = data
+      if (!analysisJson) return
+      let exerciseData = analysisJson.grades[0].item
+      this.gradeRate = exerciseData[this.currentExerciseIndex]
+      this.drawLine(data)
+    }
+  },
+  mounted() {
+    if (this.getAnalysisJson) {
+      this.renderData(this.getAnalysisJson)
+    }
+  },
+  computed: {
+    // 获取最新柱状图数据
+    getAnalysisJson() {
+      return this.$store.state.totalAnalysis.analysisJsonJoint
+    }
+  },
+  watch: {
+    getAnalysisJson(val) {
+      if (!val) return
+      this.doRender(val)
+    },
+
+    exerciseIndex(val) {
+      if (val) {
+        this.currentExerciseIndex = +val - 1 // 拿到当前所选择的题号 确认数据下标
+        this.renderData(this.getAnalysisJson)
+      }
+    }
+
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style>
+#stuAverageBar2 {
+  width: 100%;
+  height: 600px;
+  margin: 0 auto;
+  display: block;
+}
+</style>

+ 813 - 0
TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseMyTable.vue

@@ -0,0 +1,813 @@
+<template>
+  <div class="myTable">
+    <!-- 科目选择以及表格备注 -->
+    <div class="table-subject-select">
+      <!-- <Select v-model="subjectSelectVal" style="width:150px" v-if="showSelect" @on-change="onSelectChange">
+                <Option v-for="(item,index) in subjectList" :value="index" :key="index">{{ item }}</Option>
+            </Select> -->
+      <div class="myTable-title">{{ tableName }}</div>
+      <div>
+        <span @click="exportData(3)" class="table-export-btn">
+          <Icon type="ios-share-alt" /> {{$t('totalAnalysis.exportTable')}}
+        </span>
+        <span class="table-tips" v-if="tips">{{tips}}</span>
+        <span class="table-tips" v-if="!tips"></span>
+      </div>
+
+    </div>
+    <!-- 表格组件 -->
+    <Table :border="isShowBorder" ref="table" :data="tableData" :columns="tableColumns" @on-sort-change="onSortChange" @on-filter-change="onFilterChange"></Table>
+
+    <!-- 表格分页组件 -->
+    <div style="margin: 10px;overflow: hidden" v-if="!noPage">
+      <div style="float: right;margin-top: 20px;">
+        <Page show-total size="small" :current="currentPage" :total="originData.length" :page-size="pageSize" :page-size-opts="pageSizeOpts" @on-change="pageChange" @on-page-size-change="pageSizeChange" show-sizer />
+      </div>
+    </div>
+
+  </div>
+</template>
+<script>
+import excel from '@/utils/excel.js'
+export default {
+  props: {
+    columns: {
+
+      type: Array,
+      default: () => []
+    },
+    isScroll: {
+      type: Boolean,
+      default: false
+    },
+    tableDatas: {
+      type: Array,
+      default: () => []
+    },
+    showSelect: {
+      type: Boolean,
+      default: false
+    },
+    noPage: {
+      type: Boolean,
+      default: false
+    },
+    tips: {
+      type: String,
+      default: null
+    },
+    tableTitle: {
+      type: String,
+      default: null
+    },
+    pageSize: {
+      type: Number,
+      default: 10
+    },
+    tableName: {
+      type: String,
+      default: 'Analysis Table'
+    },
+    tableRef: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      isShowBorder: true,
+      isFirst: true,
+      subjectSelectVal: 0,
+      tableData: [],
+      firstData: [],
+      propsData: [],
+      originData: [],
+      tableColumns: [],
+      subjectList: [],
+      currentPage: 1,
+      pageSizes: 10,
+      pageSizeOpts: [5, 10, 20, 30, 40],
+      poperData: []
+    }
+  },
+  created() {
+    this.tableColumns = this.columns
+    this.originData = this.tableDatas
+    this.columns.forEach(item => {
+      item.render = typeof item.renderType === 'function' ? item.renderType : this[item.renderType]
+    })
+
+
+  },
+  methods: {
+
+    // 导出表格
+    exportData(type) {
+      let classList = [this.$t('totalAnalysis.allClasses')].concat([...new Set(this.$store.state.totalAnalysis.analysisJsonJoint.classes.map(item => item.className))])
+      let subjectName = null
+      let className = classList[this.$store.state.totalAnalysis.curClassIndex + 1]
+      if (this.$route.path === '/total' || this.$route.path === '/total/achievement/earlyWarning') {
+        subjectName = this.$store.state.totalAnalysis.indexSubject
+      } else {
+        subjectName = this.$store.state.totalAnalysis.currentSubject
+      }
+
+      if (type === 3) {
+        const params = {
+          title: this.columns.map(i => i.title),
+          key: this.columns.map(i => i.key),
+          data: this.originData,
+          autoWidth: true,
+          filename: this.tableName + '(' + subjectName + '、' + className + ')'
+        }
+        excel.export_array_to_excel(params)
+      } else {
+        // 多个文件 打包下载
+        const params = {
+          title: this.columns.map(i => i.title),
+          key: this.columns.map(i => i.key),
+          data: this.originData,
+          autoWidth: true,
+          filename: this.tableName
+        }
+        this.$store.commit('updateExportParams', params)
+      }
+    },
+
+    // 排序操作
+    onSortChange(data) {
+      let order = data.order // 当前排序方式 升序、降序、正常
+      let key = data.key // 当前排序依据
+      switch (order) {
+        case 'asc':
+          this.originData = this.tableDatas.sort((a, b) => {
+            return Number(a[key]) - Number(b[key])
+          })
+          break
+        case 'desc':
+          this.originData = this.tableDatas.sort((a, b) => {
+            return Number(b[key]) - Number(a[key])
+          })
+          break
+        case 'normal':
+          this.originData = JSON.parse(JSON.stringify(this.propsData))
+          break
+        default:
+          break
+      }
+      this.pageChange(1)
+    },
+
+    // 筛选操作
+    onFilterChange(val) {
+      console.log(val)
+      let filterKey = val.key
+      let filterValue = val._filterChecked.length ? val._filterChecked[0] : null
+      if (filterKey === 'className') {
+        this.originData = filterValue ? this.tableDatas.filter(item => item.className === filterValue) : this
+          .firstData
+      } else if (filterKey === 'scatter') {
+        this.originData = filterValue ? this.tableDatas.filter(item => item.scatter === filterValue) : this
+          .firstData
+      } else if (filterKey === 'areaName') {
+        this.originData = filterValue ? this.tableDatas.filter(item => item.areaName === filterValue) : this
+          .firstData
+      } else if (filterKey === 'score') {        
+        const ipoint = this.$store.state.totalAnalysis.analysisJsonJoint.ipoint
+        const touchScore = this.$store.state.totalAnalysis.analysisJsonJoint.touchScore
+        switch (filterValue) {
+          case 1:
+            this.originData = this.tableDatas.filter(item => item.score >= ipoint)
+            break
+          case 2:
+            this.originData = this.tableDatas.filter(item => item.score >= ipoint && item.score < (ipoint +
+              touchScore))
+            break
+          default:
+            this.originData = this.firstData
+            break
+        }
+      }
+      // this.tableDatas = this.originData
+      this.pageChange(1)
+    },
+
+    // 分页操作
+    pageChange(page) {
+      let start = this.pageSizes * (page - 1)
+      let end = this.pageSizes * page
+      this.currentPage = page
+      this.tableData = this.originData.slice(start, end)
+      console.log(this.tableColumns)
+      console.log(this.tableData)
+    },
+
+    // 页码操作
+    pageSizeChange(val) {
+      this.pageSizes = val
+      this.pageChange(1)
+    },
+
+    // 下拉选择
+    onSelectChange(val) {
+      this.$emit('onSelectChange', val)
+    },
+
+    // 渲染班级标题以及相应点击事件
+    renderClassName(h, params) {
+      let that = this
+      let row = params.row
+      return h('span', {}, [
+        h('span', {
+          domProps: {
+            className: 'table-rank-value'
+          }
+        }, params.row.classId),
+      ])
+    },
+
+    /* 渲染学生姓名 */
+    renderStuName(h, params) {
+      let that = this
+      let row = params.row
+      return h('span', {}, [
+        h('span', {
+          on: {
+            click: function () {
+              that.$emit('onStuClick', row)
+            }
+          },
+          domProps: {
+            className: 'table-rank-value'
+          },
+          style: {
+            cursor: 'pointer',
+            color: '#70B1E7',
+            fontWeight: 'bold'
+          }
+        }, params.row.name),
+      ])
+    },
+
+    renderEventIndex(h, params) {
+      let that = this
+      return h('span', {
+        on: {
+          click: function () {
+            let isClouDAS = that.$route.name === 'privExam'
+            if (isClouDAS) {
+              that.$EventBus.$emit('cloudas-question-click', params.row.id)
+            } else {
+              that.$parent.$parent.$parent.isShowQuestions = true
+              that.$router.push({
+                path: '/total/questionList',
+                query: {
+                  QIndex: params.row.id
+                }
+              })
+            }
+          }
+        },
+        style: {
+          cursor: 'pointer',
+          color: '#70B1E7',
+          fontWeight: 'bold'
+        }
+      }, params.row.id)
+    },
+
+    // 总分排名Render
+    renderRank(h, params) {
+      const row = params.row
+      return h('span', {
+        domProps: {
+          className: 'badge-wrap'
+        }
+      }, [
+        h('span', {
+          domProps: {
+            className: 'table-rank-badge'
+          },
+          style: {
+            color: row.changesStatus === 1 ? '#0ccb0c' : '#ff5d5d'
+          }
+        }, (row.changesStatus === 1 && row.changesVal !== 0) ? '+' + row.changesVal : (row
+          .changesStatus === -1 && row.changesVal !== 0) ? '-' + row.changesVal : ''),
+        h('span', {
+          domProps: {
+            className: 'table-rank-value'
+          }
+        }, row.gradeRank)
+
+      ])
+    },
+
+    // 折线图趋势变化
+    renderLineChange(h, params) {
+      const row = params.row
+      return h('Poptip', {
+        props: {
+          trigger: 'hover',
+          placement: 'right',
+          transfer: true
+        },
+        on: {
+          // 鼠标触发气泡事件
+          'on-popper-show': () => {
+            console.log(row.poperData)
+            // this.poperData = row.poperData
+          }
+        }
+      }, [
+        h('Icon', {
+          props: {
+            type: row.classRank === 1 ? 'md-trending-up' : row.classRank === 2 ?
+              'md-trending-down' : 'md-git-commit',
+            color: row.classRank === 1 ? '#13ff13' : row.classRank === 2 ? '#fd4e4e' :
+              'yellow',
+            size: '22'
+          },
+          style: {
+            cursor: 'pointer'
+          }
+        }),
+        h('div', {
+          slot: 'content'
+        }, [h('BaseChangeLine', {
+          props: {
+            poperData: row.poperData,
+            isLoad: true,
+            lineColor: row.classRank === 1 ? 'green' : row.classRank === 2 ?
+              '#fd4e4e' : '#ffcc33'
+          }
+        })])
+      ])
+    },
+
+    // 排名变化
+    renderRankChange(h, params) {
+      const row = params.row
+      return h('span', [
+        h('Icon', {
+          props: {
+            type: (row.changesStatus === 1 && row.changesVal !== 0) ? 'md-arrow-up' : (row
+              .changesStatus === -1 && row.changesVal !== 0) ? 'md-arrow-down' :
+              'md-git-commit',
+            color: (row.changesStatus === 1 && row.changesVal !== 0) ? '#13ff13' : (row
+              .changesStatus === -1 && row.changesVal !== 0) ? '#fd4e4e' : 'yellow',
+            size: '22'
+          },
+          style: {
+            cursor: 'pointer'
+          }
+        }),
+        h('span', {
+          domProps: {
+            className: 'table-rank-value'
+          }
+        }, row.changesStatus === 0 ? '' : row.changesVal)
+      ])
+    },
+
+    // 是否进线
+    renderEntry(h, params) {
+      let that = this
+      const row = params.row
+      // 进线分数
+      const ipoint = this.$store.state.totalAnalysis.analysisJsonJoint.ipoint
+      // 踩线的最高分数与进线最后一名的分差
+      const touchScore = parseInt(this.$store.state.totalAnalysis.analysisJsonJoint.touchScore * ipoint * 0.01)
+      return h('span', [
+        h('span', {
+          domProps: {
+            className: 'table-entry-status'
+          },
+          style: {
+            cursor: 'pointer',
+            background: row.score >= (ipoint + touchScore) ? '#4bd75e' : row.score >= ipoint ?
+              '#31bcc4' : 'transparent'
+          }
+        }, row.score >= (ipoint + touchScore) ? that.$t('totalAnalysis.myTable.inner') : row.score >=
+          ipoint ? that.$t('totalAnalysis.myTable.outer') : ''),
+        h('span', {
+          domProps: {
+            className: 'table-rank-value'
+          }
+        }, row.score)
+      ])
+    },
+
+    // 变化状态
+    renderStauts(h, params) {
+      const row = params.row
+      return h('span', [
+        h('Icon', {
+          props: {
+            type: row.classRank === 1 ? 'md-arrow-up' : row.classRank === 2 ? 'md-arrow-down' :
+              'md-git-commit',
+            color: row.classRank === 1 ? '#13ff13' : row.classRank === 2 ? '#fd4e4e' :
+              'yellow',
+            size: '22'
+          },
+          style: {
+            cursor: 'pointer'
+          }
+        })
+      ])
+    },
+
+    // 渲染应努力题号
+    renderHard(h, params) {
+      let that = this
+      const list = params.row.hardList ? params.row.hardList.split(',') : params.row.itemNO.split(',')
+      return h('span', {
+        style: {
+          textAlign: 'left'
+        }
+      }, list.sort((a, b) => a - b).map(function (item, index) {
+        return h('span', {
+          style: {
+            fontSize: '16px',
+            fontWeight: '600',
+            color: '#70B1E7',
+            cursor: 'pointer',
+            display: 'inline-block',
+            float: 'left'
+          },
+          on: {
+            'click': function () {
+              let isClouDAS = that.$route.name === 'privExam'
+              if (isClouDAS) {
+                that.$EventBus.$emit('cloudas-question-click', isNaN(item) ? 1 : item)
+              } else {
+                that.$parent.$parent.$parent.isShowQuestions = true
+                that.$router.push({
+                  path: '/total/questionList',
+                  query: {
+                    QIndex: isNaN(item) ? 1 : item
+                  }
+                })
+              }
+            }
+          }
+        }, item + (index === list.length - 1 ? '' : ' , '))
+      }))
+    },
+
+    // 渲染应小心题号
+    renderCareful(h, params) {
+      let that = this
+      const row = params.row
+      const list = params.row.carefulList ? params.row.carefulList.split(',') : []
+      return h('span', list.sort((a, b) => a - b).map(function (item, index) {
+        return h('span', {
+          style: {
+            fontSize: '16px',
+            fontWeight: '600',
+            color: '#70B1E7',
+            cursor: 'pointer',
+            display: 'inline-block',
+            float: 'left'
+            // textDecoration: "underline"
+          },
+          on: {
+            'click': function () {
+              let isClouDAS = that.$route.name === 'privExam'
+              if (isClouDAS) {
+                that.$EventBus.$emit('cloudas-question-click', isNaN(item) ? 1 : item)
+              } else {
+                that.$parent.$parent.$parent.isShowQuestions = true
+                that.$router.push({
+                  path: '/total/questionList',
+                  query: {
+                    QIndex: isNaN(item) ? 1 : item
+                  }
+                })
+              }
+            }
+          }
+        }, item + (index === list.length - 1 ? '' : ' , '))
+      }))
+    },
+
+    // 渲染百分比数据
+    renderPercent(h, params) {
+      return h('span', params.row.scoreRate + '%')
+    },
+
+    renderCsRate(h, params) {
+      return h('span', params.row.csRate + '%')
+    },
+
+    renderOverRate(h, params) {
+      return h('span', params.row.overAverageRate + '%')
+    },
+  },
+
+  mounted() {
+    if (this.noPage) {
+      this.pageSizes = 999999
+      this.pageChange(1)
+      this.currentPage = 1
+    }
+  },
+
+  computed: {
+    exportTableRefs() {
+      return this.$store.state.totalAnalysis.exportTableRefs
+    }
+  },
+
+  watch: {
+    tableDatas: {
+      handler(data, oldData) {
+        let earlyFlag = data.length && oldData.length && Object.keys(data[0]).length > Object.keys(oldData[0]).length
+        this.originData = JSON.parse(JSON.stringify(data))
+        this.firstData = JSON.parse(JSON.stringify(data))
+        if (this.isFirst || earlyFlag) this.propsData = JSON.parse(JSON.stringify(data))
+        // 获取当前测评班级数据
+        this.subjectList = this.$store.state.totalAnalysis.subjectList
+        this.pageChange(1)
+        this.isFirst = false
+      },
+    },
+    columns: {
+      handler(n, o) {
+        n.forEach(item => {
+          item.render = item.renderType ? (typeof item.renderType === 'function' ? item.renderType : this[item.renderType]) : undefined
+          item.filterRemote = function (value, row) {
+          }
+        })
+        this.tableColumns = n
+      },
+      deep: true
+    },
+    tableName: {
+      handler(n, o) {
+        console.log(n)
+      },
+      deep: true
+    },
+    exportTableRefs: {
+      handler(n) {
+        // 如果导出的表格包含当前表格 则直接运行下载
+        if (n.length && n.indexOf(this.tableRef) > -1) {
+          // 如果选择下载的表格数量大于1 则进行打包下载
+          if (n.length > 1) {
+            this.exportData(4)
+          } else {
+            this.exportData(3)
+          }
+        }
+      },
+      deep: true
+    }
+  }
+
+}
+</script>
+
+<style>
+.myTable {
+  width: 100%;
+  /*height: 400px;*/
+  /*padding: 20px 0;*/
+  padding-right: 20px;
+  margin-top: 20px;
+  margin: 0 auto;
+  display: block;
+  user-select: none !important;
+}
+
+.myTable .ivu-table td,
+.myTable .ivu-table th {
+  /*border: none;*/
+  /* color: #e4eadb; */
+  /* border-color: #595959; */
+}
+
+.myTable .ivu-table-wrapper {
+  /*border:none;*/
+  /* border-color: #595959; */
+  /* border-right: 1px solid #595959; */
+}
+
+.myTable .ivu-table::before,
+.myTable .ivu-table::after {
+  height: 1px;
+  /* background: #595959; */
+}
+
+.myTable .ivu-table {
+  box-sizing: border-box;
+  /*border:1px solid #595959;*/
+}
+
+.myTable .ivu-table,
+.myTable .ivu-table td {
+  /* background: #343434; */
+  text-align: center;
+  position: relative;
+}
+
+.myTable .ivu-table th {
+  /* background: rgba(107, 107, 107, 0.19); */
+  text-align: center;
+  position: relative;
+}
+
+.myTable .ivu-table-sort {
+  margin-left: -6px;
+}
+
+.myTable .ivu-table-sort i {
+  /* color: #fff; */
+  margin-left: 1px;
+}
+
+.myTable .ivu-table-sort i.on {
+  /* color: #4cf8da; */
+}
+
+.myTable .ivu-table-header .ivu-table-cell {
+  font-size: 14px;
+  font-weight: bold;
+  word-break: break-word;
+}
+
+.myTable .ivu-table-header {
+  /* background: rgba(228, 234, 219, 0.08); */
+}
+
+.myTable .table-rank-value {
+  margin-left: 10px;
+  vertical-align: middle;
+  font-size: 14px;
+}
+
+.myTable .table-rank-badge {
+  position: absolute;
+  right: 10px;
+  top: 12px;
+  font-size: 14px;
+  padding: 2px 14px;
+  color: #e4eadb;
+}
+
+.myTable .badge-wrap .ivu-icon,
+.myTable .badge-wrap img {
+  position: absolute;
+  left: 7px;
+  top: 12px;
+}
+
+.myTable .ivu-page-item {
+  /* background: rgba(40, 40, 40, .5); */
+}
+
+.myTable .ivu-page-item:hover {
+  /* border-color: #e4eadb; */
+}
+
+.myTable .ivu-page-item-active,
+.myTable .ivu-page-item:hover {
+  background: #2d8cf0;
+}
+
+.myTable .ivu-page-item-active a,
+.myTable .ivu-page-item:hover a {
+  /* border-color: #e4eadb; */
+  color: #fff;
+}
+
+.myTable .ivu-page-next,
+.myTable .ivu-page-prev {
+  /* background: rgba(0, 0, 0, 0); */
+}
+
+.myTable .ivu-page-next:hover a,
+.myTable .ivu-page-prev:hover a {
+  color: #fff;
+}
+
+.myTable .ivu-page-next:hover,
+.myTable .ivu-page-prev:hover {
+  background: #2d8cf0;
+  /* border-color: #e4eadb; */
+}
+
+.myTable .ivu-table-fixed {
+  /*padding-top:3px;*/
+  background: #282828;
+}
+
+.myTable .ivu-table-fixed-right::before,
+.ivu-table-fixed::before {
+  height: 0;
+}
+
+.table-subject-select {
+  position: relative;
+  display: flex;
+  justify-content: space-between;
+  padding-right: 5px;
+  /* height: 40px; */
+  margin-bottom: 20px;
+}
+
+.table-subject-select .table-tips {
+  font-size: 14px;
+  color: #70b1e7;
+  font-weight: bold;
+  position: absolute;
+  right: 120px;
+  top: 40px;
+}
+
+.table-subject-select .ivu-select {
+  margin: 5px 20px 15px 0;
+  height: 30px;
+}
+
+.table-subject-select .ivu-select-single .ivu-select-selection {
+  height: 30px;
+  background: transparent;
+  border: 1px solid #595959;
+  box-shadow: none;
+  color: #cecece;
+}
+
+.table-subject-select .ivu-select-single .ivu-select-arrow {
+  /*top:76%;*/
+}
+
+.table-subject-select .ivu-select-single .ivu-select-placeholder {
+  height: 30px;
+  line-height: 30px;
+  font-size: 16px;
+}
+
+.table-subject-select .table-export-btn {
+  font-size: 14px;
+  font-weight: bold;
+  position: absolute;
+  right: 5px;
+  top: 40px;
+  cursor: pointer;
+}
+
+.table-subject-select .table-export-btn:hover {
+  color: var(--normal-icon-color);
+}
+
+.myTable .table-entry-status {
+  position: absolute;
+  right: 20px;
+  top: 15px;
+  background: #209a31;
+  color: #fff;
+  width: 40px;
+  height: 20px;
+  padding: 5px;
+  font-size: 12px;
+  border-radius: 5px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.ivu-table-overflowX::-webkit-scrollbar {
+  height: 12px !important;
+}
+
+.ivu-table-overflowX::-webkit-scrollbar-track {
+  border-radius: 0px;
+  display: none;
+}
+
+.ivu-table-overflowX::-webkit-scrollbar-thumb {
+  border-radius: 0px;
+  background: #949494;
+}
+
+.myTable
+  .ivu-select-small.ivu-select-single
+  .ivu-select-selection
+  .ivu-select-selected-value {
+  height: 27px;
+  line-height: 27px;
+  font-size: 12px;
+}
+
+.myTable .myTable-title {
+  font-size: 16px;
+  font-weight: 500;
+  color: var(--primary-text-color);
+  border-left: 5px solid var(--tabs-bottom-color);
+  padding: 5px 0;
+  padding-left: 10px;
+  margin-bottom: 10px;
+  width: 100%;
+  background: var(--active-item-start);
+}
+</style>

+ 120 - 0
TEAMModelOS/ClientApp/src/components/student-analysis/total/htBasePie.vue

@@ -0,0 +1,120 @@
+<template>
+    <div :id="pieId" class="myPie"></div>
+</template>
+
+<script>
+    export default {
+        name: 'BasePie',
+        props: ['pieId'],
+        data() {
+            return {
+                pieData: []
+            }
+        },
+        methods: {
+
+
+            drawLine(data) {
+                let that = this
+                // 基于准备好的dom,初始化echarts实例
+                let myPie = this.$echarts.init(document.getElementById(this.pieId), 'chalk')
+
+                // 指定图表的配置项和数据
+                var option = {
+                    tooltip: {
+                        trigger: 'item',
+                        formatter: '{a} <br/>{b} : {c} ({d}%)'
+                    },
+                    calculable: true,
+                    roseType: true,
+                    series: {
+                        name: that.$t('totalAnalysis.ka_title1'),
+                        type: 'pie',
+                        radius: [40, 140],
+                        center: ['45%', '50%'],
+                        max: 100, // for funnel
+                        sort: 'ascending', // for funnel
+                        data: data,
+                        emphasis: {
+                            label: {
+                                show: true,
+                                fontSize: '16',
+                                fontWeight: 'bold'
+                            },
+                            itemStyle: {
+                                shadowBlur: 10,
+                                shadowOffsetX: 0,
+                                shadowColor: 'rgba(0, 0, 0, 0.3)'
+                            }
+                        },
+                    }
+                }
+
+                // 绘制图表
+                myPie.setOption(option)
+
+                window.addEventListener('resize', function() {
+                    myPie.resize()
+                })
+            }
+        },
+        mounted() {
+            if (this.getPieData) {
+                console.log(this.getPieData)
+                    if (this.getPieData.pointList.length) {
+                            let pointList = this.getPieData.pointList
+                            let valList = this.getPieData.per
+                            let arr = []
+                            pointList.forEach((item, index) => {
+                                let o = {}
+                                o.name = item
+                                o.value = valList[index].replace('%', '')
+                                arr.push(o)
+                            })
+                            this.drawLine(arr)
+                    }
+                    
+                    
+            }
+        },
+        computed: {
+            // 获取最新知识点占比饼图数据
+            getPieData() {
+                let curSubjectIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.name).indexOf(this.$store.state.totalAnalysis.currentSubjectJoint)
+                return this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].pointKey
+            }
+        },
+        watch: {
+            getPieData: {
+                deep: true,
+                handler(val) {
+                    // this.drawLine(newName)
+                    console.log(val)
+                    let pointList = val.pointList
+                    let valList = val.per
+                    let arr = []
+                    pointList.forEach((item, index) => {
+                        let o = {}
+                        // 测试使用 后面需要接口换取名称
+                        o.name = item
+                        o.value = valList[index]
+                        arr.push(o)
+                    })
+                    this.drawLine(arr)
+                }
+            }
+        }
+
+    }
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+
+    .myPie {
+        width: 100%;
+        height: 450px;
+        margin: 0 auto;
+        display: block;
+    }
+</style>

+ 197 - 0
TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseRadar.vue

@@ -0,0 +1,197 @@
+<template>
+  <div>
+    <div :id="echartsId" class="myRadar"></div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'helloLine',
+  props: ['echartsId', 'classIndex'],
+  data() {
+    return {
+      curClassIndex: -1,
+      radarData: []
+    }
+  },
+  created() {
+  },
+  methods: {
+    drawLine(indicator, data) {
+      // 基于准备好的dom,初始化echarts实例
+      let that = this
+      let myRadar = this.$echarts.init(document.getElementById(this.echartsId), 'chalk')
+
+      // 指定图表的配置项和数据
+      var option = {
+        tooltip: {
+          formatter: function (params) {
+            let result = params.name
+            let pointList = that.getKnowledgeData.pointList
+            for (let i = 0; i < pointList.length; i++) {
+              result += '<br>' + params.marker + pointList[i] + ':' + params.value[i] + '%'
+            }
+            return result
+          }
+        },
+        radar: {
+          triggerEvent: true,
+          name: {
+            textStyle: {
+              color: '#303030',
+              borderRadius: 3,
+              fontSize: 14,
+              padding: [13, 15]
+            }
+          },
+          center: ['50%', '45%'],
+          radius: '55%',
+          tooltip: {
+            trigger: 'item'
+          },
+          indicator: indicator,
+          splitArea: {
+            areaStyle: {
+              // color: [
+              //  'rgba(222,134,85, 0.1)', 'rgba(222,134,85, 0.2)',
+              //  'rgba(222,134,85, 0.4)', 'rgba(222,134,85, 0.6)',
+              //  'rgba(222,134,85, 0.8)', 'rgba(222,134,85, 1)'
+              // ].reverse()
+            }
+          },
+          axisLine: { // 指向外圈文本的分隔线样式
+            lineStyle: {
+              color: '#f1f1f1'
+            }
+          },
+          splitLine: {
+            lineStyle: {
+              width: 2,
+              color: [
+                'rgba(224,134,82, 0.1)', 'rgba(224,134,82, 0.2)',
+                'rgba(224,134,82, 0.4)', 'rgba(224,134,82, 0.6)',
+                'rgba(224,134,82, 0.8)', 'rgba(224,134,82, 1)'
+              ].reverse()
+            }
+          }
+        },
+        series: [
+          {
+            name: '',
+            type: 'radar',
+            itemStyle: {
+              color: '#15B8C3'
+            },
+            areaStyle: {
+              color: {
+                type: 'linear',
+                x: 0,
+                y: 0,
+                x2: 0,
+                y2: 1,
+                colorStops: [
+                  {
+                    offset: 0,
+                    color: '#2cc7cb' // 0% 处的颜色
+                  },
+                  {
+                    offset: 1,
+                    color: '#59b2ef' // 100% 处的颜色
+                  }
+                ],
+                global: false // 缺省为 false
+              }
+            },
+            lineStyle: {
+              color: '#9FACE6'
+            },
+            data: [
+              {
+                value: data,
+                name: that.echartsId === 'knowRadar' ? that.$t('totalAnalysis.ka_title4') : that.$t('totalAnalysis.le_title4')
+              }
+            ]
+          }
+        ]
+      }
+
+      // 绘制图表
+      myRadar.setOption(option)
+      window.addEventListener('resize', function () {
+        myRadar.resize()
+      })
+    },
+
+    doRender(data, classIndexs) {
+      let classIndex = this.getCurClassIndex
+      let indicator = []
+      let rateArr = classIndex === -1 ? data.stupercent.grade.map(item => (Number(item)).toFixed(2)) : data.pointList.map(pointName => data.classpercent[pointName][classIndex].toFixed(2))
+      console.log(data)
+      // 获取最大值 +5 设置到雷达边界最大值
+      let maxNum = Math.max(...rateArr.map(i => Number(i))) + 5
+      data.pointList.forEach(item => {
+        indicator.push({
+          name: item,
+          max: maxNum
+        })
+      })
+      this.drawLine(indicator, rateArr)
+    }
+  },
+  mounted() {
+    if (this.getKnowledgeData) {
+      console.log(this)
+      this.doRender(this.getKnowledgeData, this.curClassIndex)
+    }
+  },
+
+  computed: {
+    // 获取最新雷达图数据
+    getKnowledgeData() {
+      let curSubjectIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.name).indexOf(this.$store.state.totalAnalysis.currentSubjectJoint)
+      let curJson = this.echartsId === 'knowRadar' ? this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].pointKey :
+        this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].levelKey
+      if (this.echartsId !== 'knowRadar') {
+        let transArr = this.$GLOBAL.EXERCISE_LEVELS()
+        curJson.pointList = curJson.pointList.map((i, index) => transArr[index])
+        for (let key in curJson.classpercent) {
+          if (!isNaN(key)) {
+            let newKey = transArr[+key - 1]
+            curJson.classpercent[newKey] = curJson.classpercent[key]
+            curJson.stupercent[newKey] = curJson.stupercent[key]
+          }
+        }
+      }
+      return curJson
+    },
+    getCurClassIndex() {
+      return this.$store.state.totalAnalysis.curClassIndex
+    }
+  },
+
+  watch: {
+    getKnowledgeData: {
+      handler(n, o) {
+        this.doRender(this.getKnowledgeData, this.curClassIndex)
+      }
+    },
+    classIndex(n, o) {
+      console.log('雷达图变化了', n - 1)
+      this.curClassIndex = n - 1
+      this.doRender(this.getKnowledgeData, n - 1)
+    },
+    immediate: true,
+    deep: true
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+.myRadar {
+  width: 100%;
+  height: 480px;
+  margin: 0 auto;
+  display: block;
+}
+</style>

+ 359 - 0
TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseScatter.vue

@@ -0,0 +1,359 @@
+<template>
+  <div id="myScatter"></div>
+</template>
+
+<script>
+export default {
+  name: 'myScatter',
+  props: ['scatterData'],
+  data() {
+    return {
+      originArr: [],
+      dataArr: [],
+      childIndex: 0,
+      activeItemIndex: 0
+    }
+  },
+  created() {
+
+  },
+  mounted() {
+
+  },
+  methods: {
+
+    // 调整图表所需数据结构格式
+    renderData(data) {
+      let analysisJson = data
+      let newArr = []
+      analysisJson.forEach(item => {
+        let arr2 = []
+        arr2.push((+item.x).toFixed(2))
+        arr2.push((item.y))
+        arr2.push(item.name)
+        arr2.push(item.memberId)
+        newArr.push(arr2)
+      })
+      return newArr
+    },
+
+    // 渲染散点图
+    drawLine(data) {
+      // 基于准备好的dom,初始化echarts实例
+      let myScatter = this.$echarts.init(document.getElementById('myScatter'), 'chalk')
+      let _this = this
+      // 指定图表的配置项和数据
+      var option = {
+        tooltip: {
+          trigger: 'item',
+          showDelay: 0,
+          axisPointer: {
+            show: true,
+            lineStyle: {
+              type: 'dashed',
+              width: 1
+            }
+          },
+          textStyle: {
+            height: '160px'
+          },
+          formatter: function (params) {
+            const item = params
+            return `${_this.$t('totalAnalysis.base_name')}:${item.data[2]}
+                        <br/>${_this.$t('totalAnalysis.sca_chart_text1')}:${item.data[1]}%
+                        <br/>${_this.$t('totalAnalysis.sca_chart_text2')}:${item.data[0]}
+                       `
+          }
+        },
+        legend: {
+          data: [_this.$t('totalAnalysis.sca_text4')]
+        },
+        grid:{
+          containLabel:true
+        },
+        // toolbox: {
+        //    show: true,
+        //    right: '70px',
+        //    feature: {
+        //        dataZoom: { show: true },
+        //        restore: { show: false }
+        //    }
+        // },
+        xAxis: [{
+          type: 'value',
+          splitNumber: 2,
+          min: 0,
+          max: 1,
+          name: _this.$t('totalAnalysis.sca_chart_text2'),
+          nameTextStyle: {
+            color: '#757575'
+          },
+          scale: true,
+          splitLine: {
+            show: true,
+            lineStyle: {
+              color: '#595959'
+            }
+          }
+        }],
+        yAxis: [{
+          type: 'value',
+          name: _this.$t('totalAnalysis.sca_chart_text1'),
+          nameTextStyle: {
+            color: '#757575'
+          },
+          max: 100,
+          min: 0,
+          splitLine: {
+            show: false,
+            lineStyle: {
+              color: '#595959'
+            }
+          },
+          interval: 25,
+          axisLabel: {
+            show: true,
+            formatter: function (value) {
+              let val = []
+              if (value !== 25) {
+                val.push(value + '%')
+              }
+              return val
+            }
+          },
+          spiltArea: {
+            show: true
+          }
+        }],
+        // dataZoom: [{
+        // 		show: true,
+        // 		type: 'inside',
+        // 		start: 0,
+        // 		end: 100,
+        // 		bottom: 10,
+        // 		height: 15
+        // 	},
+        // 	{
+        // 		show: true,
+        // 		type: 'slider',
+        // 		start: 0,
+        // 		end: 100,
+        // 		bottom: 0,
+        // 		height: 35,
+        // 		textStyle: {
+        // 			color: '#BBBBBB'
+        // 		}
+        // 	}
+        // ],
+        series: [{
+          name: _this.$t('totalAnalysis.sca_text4'),
+          type: 'scatter',
+          symbolSize: 15,
+          z: 0,
+          data: data,
+          itemStyle: {
+            color: '#79c8e8'
+          },
+          emphasis: {
+            itemStyle: {
+              color: '#ff99cc'
+            }
+          },
+          markLine: {
+            silent: true,
+            animation: false,
+            data: [{
+              yAxis: 75
+            }, {
+              yAxis: 50
+            }, {
+              yAxis: 100
+            }],
+            label: {
+              color: '#757575',
+              formatter: '{c}%'
+            },
+            lineStyle: {
+              color: '#595959',
+              type: 'solid'
+            }
+          },
+          markArea: {
+            silent: true,
+            data: [
+              [{
+                xAxis: '0.5',
+                yAxis: '100',
+                itemStyle: {
+                  color: 'rgba(90,90,90,.1)'
+
+                },
+                label: {
+                  show: true,
+                  position: ['90%', '10%'],
+                  color: '#008955',
+                  fontSize: 20,
+                  formatter: 'A'
+                }
+
+              }, {
+                xAxis: '0',
+                yAxis: '75',
+                itemStyle: {
+                  color: ''
+
+                }
+
+              }],
+              [{
+                xAxis: '1',
+                yAxis: '100',
+                itemStyle: {
+                  color: 'rgba(128,128,128,0)'
+
+                },
+                label: {
+                  show: true,
+                  position: ['5%', '10%'],
+                  color: '#008955',
+                  fontSize: 20,
+                  formatter: "A'"
+                }
+
+              }, {
+                xAxis: '0.5',
+                yAxis: '75'
+              }],
+              [{
+                xAxis: '0.5',
+                yAxis: '75',
+                itemStyle: {
+                  color: 'rgba(128,128,128,0)'
+
+                },
+                label: {
+                  show: true,
+                  position: ['90%', '10%'],
+                  color: '#008955',
+                  fontSize: 20,
+                  formatter: 'B'
+                }
+
+              }, {
+                xAxis: '0',
+                yAxis: '50'
+              }],
+              [{
+                xAxis: '1',
+                yAxis: '75',
+                itemStyle: {
+                  color: 'rgba(90,90,90,.1)'
+
+                },
+                label: {
+                  show: true,
+                  position: ['5%', '10%'],
+                  color: '#008955',
+                  fontSize: 20,
+                  zIndex: 99999,
+                  formatter: "B'"
+                }
+
+              }, {
+                xAxis: '0.5',
+                yAxis: '50'
+              }],
+              [{
+                xAxis: '0.5',
+                yAxis: '50',
+                itemStyle: {
+                  color: 'rgba(90,90,90,.1)'
+
+                },
+                label: {
+                  show: true,
+                  position: ['90%', '10%'],
+                  color: '#008955',
+                  fontSize: 20,
+                  formatter: 'C'
+                }
+
+              }, {
+                xAxis: '0',
+                yAxis: '0'
+              }],
+              [{
+                xAxis: '1',
+                yAxis: '50',
+                itemStyle: {
+                  color: 'rgba(128,128,128,0)'
+
+                },
+                label: {
+                  show: true,
+                  position: ['5%', '10%'],
+                  color: '#008955',
+                  fontSize: 20,
+                  formatter: "C'"
+                }
+
+              }, {
+                xAxis: '0.5',
+                yAxis: '0'
+              }]
+            ]
+          }
+        }]
+      }
+
+      // 绘制图表
+      myScatter.setOption(option)
+      window.addEventListener('resize', function () {
+        myScatter.resize()
+      })
+
+      // 散点图元素点击事件
+      myScatter.on('click', function (item) {
+        _this.activeItemIndex = item.dataIndex
+      })
+    }
+  },
+
+  mounted() {
+    // 缓存有数据则渲染缓存数据
+    // if (this.getAnalysisJson) {
+    // 	this.drawLine(this.renderData(this.getAnalysisJson))
+    // }
+  },
+
+  computed: {
+    // 获取最新散点图数据
+    getAnalysisJson() {
+      return this.$store.state.totalAnalysis.analysisJsonJoint
+    }
+  },
+  watch: {
+    scatterData: {
+      deep: true,
+      handler(val) {
+        if (val.length) {
+          this.drawLine(this.renderData(val)) // 获取最新落点图数据 进行渲染
+        } else {
+          this.drawLine([])
+        }
+      }
+
+    }
+  }
+}
+</script>
+
+<style scoped>
+#myScatter {
+  width: 100%;
+  height: 500px;
+  margin: 0 auto;
+  display: block;
+  margin-top: 20px;
+}
+</style>

+ 238 - 0
TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseScoreRateBar.vue

@@ -0,0 +1,238 @@
+<template>
+  <div class="myScoreRateBar" :id="echartsId"></div>
+</template>
+
+<script>
+export default {
+  name: 'hello',
+  props: {
+    echartsId: {
+      type: String,
+      default: ''
+    },
+    echartsData: {
+      type: Array,
+      default: () => []
+    },
+    subjectIndex: {
+      type: Number,
+      default: NaN
+    }
+  },
+  data() {
+    return {
+      scoreRateCountArr: [],
+      splitArr: [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
+    }
+  },
+  created() { },
+
+  methods: {
+
+    // 执行图表渲染工作
+    drawLine(data) {
+      // 基于准备好的dom,初始化echarts实例
+      let myBar = this.$echarts.init(document.getElementById(this.echartsId))
+      let that = this
+
+      // 指定图表的配置项和数据
+      var option = {
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            type: 'shadow',
+            textStyle: {
+              color: '#6d6d6d',
+              fontSize: '26'
+            },
+          },
+          formatter: function (value) {
+            let range = (that.splitArr[value[0].dataIndex] - 10) + '%-' + that.splitArr[value[0]
+              .dataIndex] + '%:'
+            return range + '<br>' + value[0].data + that.$t('unit.text7')
+          }
+        },
+        legend: {
+          top: '5%',
+          right: '50%',
+          data: [that.$t('unit.text13')],
+          textStyle: {
+            fontSize: 12,
+            color: '#808080'
+          },
+          icon: 'rect'
+        },
+        grid: {
+          show: false,
+          containLabel: true,
+          height: 300,
+          width: '75%',
+          right: '18%',
+          tooltip: {
+            show: true,
+            trigger: 'axis', // 触发类型
+            textStyle: {
+              color: '#666'
+            }
+          }
+        },
+        dataZoom: [{
+          'show': true,
+          'height': 10,
+          'xAxisIndex': [
+            0
+          ],
+          bottom: 10,
+          'start': 0,
+          'end': 100,
+          handleIcon: 'M512 497.821538m-418.264615 0a418.264615 418.264615 0 1 0 836.52923 0 418.264615 418.264615 0 1 0-836.52923 0Z',
+          handleSize: '160%',
+          handleStyle: {
+            color: '#d3dee5'
+
+          },
+          textStyle: {
+            color: '#717171'
+          },
+          // borderColor: '#90979c'
+        }],
+        xAxis: [{
+          type: 'category',
+          axisTick: {
+            show: true
+          },
+          axisLine: {
+            show: false
+          },
+          axisLabel: {
+            color: '#aaaaaa',
+            margin: 10,
+            align: 'left'
+          },
+          splitLine: {
+            show: false,
+            lineStyle: {
+              color: '#4c504a',
+              width: 0.5,
+              type: 'solid'
+            }
+          },
+          data: ['     10%', '     20%', '     30%', '     40%', '     50%', '     60%',
+            '     70%', '     80%', '     90%', '   100%'
+          ],
+
+        }],
+        yAxis: [{
+          name: that.$t('unit.text13'),
+          nameTextStyle: {
+            color: '#808080',
+            fontSize: 12,
+          },
+          type: 'value',
+          axisLine: {
+            show: false
+          },
+          axisLabel: {
+            color: '#afafaf',
+            fontSize: 12,
+          },
+          splitLine: {
+            show: true,
+            lineStyle: {
+              color: '#4c504a',
+              width: 0.5,
+              type: 'solid'
+            }
+          },
+          axisTick: {
+            show: true
+          }
+        }],
+        series: [{
+          name: that.$t('unit.text13'),
+          type: 'bar',
+          label: {
+            show: true,
+            position: 'top',
+            fontSize: 14,
+            color: '#3DC3F0',
+            fontWeight: 'bold'
+          },
+          barMaxWidth: 40,
+          itemStyle: {
+            color: {
+              type: 'linear',
+              x: 0,
+              y: 0,
+              x2: 0,
+              y2: 1,
+              colorStops: [{
+                offset: 0,
+                color: '#3DC3F0' // 0% 处的颜色
+              }, {
+                offset: 1,
+                color: '#3486b1' // 100% 处的颜色
+              }]
+            }
+          },
+          data: that.scoreRateCountArr
+        },]
+      };
+      // 绘制图表
+      myBar.clear()
+      myBar.setOption(option)
+      window.addEventListener('resize', function () {
+        myBar.resize()
+      })
+    },
+
+
+    /* 根据学生总体数据来换取得分率区间分布数据 */
+    doRender(list, isAll) {
+      let sRateArr = isAll ? list.map(i => i.sRate) : list.map(i => i.subjects[this.subjectIndex].sRate)
+      let splitArr = [...new Array(10).keys()]
+      let result = new Array(10).fill(0)
+      sRateArr.forEach(rate => {
+        if (rate === 100) {
+          result[9]++
+        } else if (rate < 10) {
+          result[0]++
+        } else {
+          splitArr.forEach((j, index) => {
+            if (rate.toString().slice(0, 1) === j.toString()) {
+              result[index]++
+            }
+          })
+        }
+
+      })
+      this.scoreRateCountArr = result
+      this.drawLine()
+    }
+  },
+  mounted() {
+    this.doRender(this.echartsData, isNaN(this.subjectIndex))
+  },
+  watch: {
+    echartsData: {
+      handler(n, o) {
+        this.doRender(n, isNaN(this.subjectIndex))
+      },
+    },
+    subjectIndex: {
+      handler(n, o) {
+        this.doRender(this.echartsData, isNaN(n))
+      },
+    }
+  }
+}
+</script>
+
+<style scoped>
+.myScoreRateBar {
+  width: 100%;
+  height: 400px;
+  margin: 0 auto;
+  display: block;
+}
+</style>

+ 354 - 0
TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseSingleStuScatter.vue

@@ -0,0 +1,354 @@
+<template>
+  <div id="singleScatter"></div>
+</template>
+
+<script>
+export default {
+  name: 'myScatter',
+  props: ['scatterData', 'stuData'],
+  data() {
+    return {
+      stuId: '',
+      originArr: [],
+      dataArr: [],
+      childIndex: 0,
+      activeItemIndex: 0
+    }
+  },
+  created() {
+
+  },
+  mounted() {
+
+  },
+  methods: {
+
+    // 调整图表所需数据结构格式
+    renderData(data) {
+      let analysisJson = data
+      let newArr = []
+      analysisJson.forEach(item => {
+        let arr2 = []
+        arr2.push((+item.x).toFixed(2))
+        arr2.push((item.y))
+        arr2.push(item.name)
+        arr2.push(item.memberId)
+        arr2.push(item.id)
+        newArr.push(arr2)
+      })
+      return newArr
+    },
+
+    // 渲染散点图
+    drawLine(data) {
+      // 基于准备好的dom,初始化echarts实例
+      if(!document.getElementById('singleScatter')){return}
+      let myScatter = this.$echarts.init(document.getElementById('singleScatter'), 'chalk')
+      let _this = this
+      // 指定图表的配置项和数据
+      var option = {
+        title: {
+          text: this.$t('studentWeb.exam.chart.pointPlot'),
+          left: 'left',
+          textStyle: {
+            fontSize: "14",
+          }
+        },
+        tooltip: {
+          trigger: 'item',
+          showDelay: 0,
+          axisPointer: {
+            show: true,
+            lineStyle: {
+              type: 'dashed',
+              width: 1
+            }
+          },
+          textStyle: {
+            height: '160px'
+          },
+          formatter: function (params) {
+            const item = params
+            return `${_this.$t('totalAnalysis.base_name')}:${item.data[2]}
+                        <br/>${_this.$t('totalAnalysis.sca_chart_text1')}:${item.data[1]}%
+                        <br/>${_this.$t('totalAnalysis.sca_chart_text2')}:${item.data[0]}
+                       `
+          }
+        },
+        grid: {
+          left: 40
+        },
+        legend: {
+          data: [_this.$t('totalAnalysis.sca_text4')]
+        },
+        xAxis: [{
+          type: 'value',
+          splitNumber: 2,
+          min: 0,
+          max: 1,
+          name: _this.$t('totalAnalysis.sca_chart_text2'),
+          nameTextStyle: {
+            color: '#757575'
+          },
+          scale: true,
+          splitLine: {
+            show: true,
+            lineStyle: {
+              color: '#595959'
+            }
+          }
+        }],
+        yAxis: [{
+          type: 'value',
+          name: _this.$t('totalAnalysis.sca_chart_text1'),
+          nameTextStyle: {
+            color: '#757575'
+          },
+          max: 100,
+          min: 0,
+          splitLine: {
+            show: false,
+            lineStyle: {
+              color: '#595959'
+            }
+          },
+          interval: 25,
+          axisLabel: {
+            show: true,
+            formatter: function (value) {
+              let val = []
+              if (value !== 25) {
+                val.push(value + '%')
+              }
+              return val
+            }
+          },
+          spiltArea: {
+            show: true
+          }
+        }],
+        series: [{
+          name: _this.$t('totalAnalysis.sca_text4'),
+          type: 'scatter',
+          symbolSize: 15,
+          z: 0,
+          data: data,
+          itemStyle: {
+            color: function (params) {
+              if (params.data[4] === _this.stuId) {
+                console.log(params.data)
+              }
+              return params.data[4] === _this.stuId ? '#ff99cc' : '#ccc'
+            }
+          },
+          markLine: {
+            silent: true,
+            animation: false,
+            data: [{
+              yAxis: 75
+            }, {
+              yAxis: 50
+            }, {
+              yAxis: 100
+            }],
+            label: {
+              color: '#757575',
+              formatter: '{c}%'
+            },
+            lineStyle: {
+              color: '#595959',
+              type: 'solid'
+            }
+          },
+          markArea: {
+            silent: true,
+            data: [
+              [{
+                xAxis: '0.5',
+                yAxis: '100',
+                itemStyle: {
+                  color: 'rgba(90,90,90,.1)'
+
+                },
+                label: {
+                  show: true,
+                  position: ['90%', '10%'],
+                  color: '#008955',
+                  fontSize: 20,
+                  formatter: 'A'
+                }
+
+              }, {
+                xAxis: '0',
+                yAxis: '75',
+                itemStyle: {
+                  color: ''
+
+                }
+
+              }],
+              [{
+                xAxis: '1',
+                yAxis: '100',
+                itemStyle: {
+                  color: 'rgba(128,128,128,0)'
+
+                },
+                label: {
+                  show: true,
+                  position: ['5%', '10%'],
+                  color: '#008955',
+                  fontSize: 20,
+                  formatter: "A'"
+                }
+
+              }, {
+                xAxis: '0.5',
+                yAxis: '75'
+              }],
+              [{
+                xAxis: '0.5',
+                yAxis: '75',
+                itemStyle: {
+                  color: 'rgba(128,128,128,0)'
+
+                },
+                label: {
+                  show: true,
+                  position: ['90%', '10%'],
+                  color: '#008955',
+                  fontSize: 20,
+                  formatter: 'B'
+                }
+
+              }, {
+                xAxis: '0',
+                yAxis: '50'
+              }],
+              [{
+                xAxis: '1',
+                yAxis: '75',
+                itemStyle: {
+                  color: 'rgba(90,90,90,.1)'
+
+                },
+                label: {
+                  show: true,
+                  position: ['5%', '10%'],
+                  color: '#008955',
+                  fontSize: 20,
+                  zIndex: 99999,
+                  formatter: "B'"
+                }
+
+              }, {
+                xAxis: '0.5',
+                yAxis: '50'
+              }],
+              [{
+                xAxis: '0.5',
+                yAxis: '50',
+                itemStyle: {
+                  color: 'rgba(90,90,90,.1)'
+
+                },
+                label: {
+                  show: true,
+                  position: ['90%', '10%'],
+                  color: '#008955',
+                  fontSize: 20,
+                  formatter: 'C'
+                }
+
+              }, {
+                xAxis: '0',
+                yAxis: '0'
+              }],
+              [{
+                xAxis: '1',
+                yAxis: '50',
+                itemStyle: {
+                  color: 'rgba(128,128,128,0)'
+
+                },
+                label: {
+                  show: true,
+                  position: ['5%', '10%'],
+                  color: '#008955',
+                  fontSize: 20,
+                  formatter: "C'"
+                }
+
+              }, {
+                xAxis: '0.5',
+                yAxis: '0'
+              }]
+            ]
+          }
+        }]
+      }
+
+      // 绘制图表
+      myScatter.setOption(option)
+      window.addEventListener('resize', function () {
+        myScatter.resize()
+      })
+
+      // 散点图元素点击事件
+      myScatter.on('click', function (item) {
+        _this.activeItemIndex = item.dataIndex
+      })
+    }
+  },
+
+  mounted() {
+    // 缓存有数据则渲染缓存数据
+    // if (this.getAnalysisJson) {
+    // 	this.drawLine(this.renderData(this.getAnalysisJson))
+    // }
+  },
+
+  computed: {
+    // 获取最新散点图数据
+    getAnalysisJson() {
+      return this.$store.state.totalAnalysis.analysisJsonJoint
+    }
+  },
+  watch: {
+    stuData: {
+      handler(n, o) {
+        if (n && n.id) {
+          this.scatterData = this.scatterData.filter(i => i.id !== n.id)
+          this.scatterData.push(this.stuData)
+          this.stuId = n.id
+          this.drawLine(this.renderData(this.scatterData))
+        }
+      },
+      deep: true,
+      immediate: true,
+    },
+    scatterData: {
+      deep: true,
+      immediate: true,
+      handler(val) {
+        if (val.length) {
+          this.drawLine(this.renderData(val)) // 获取最新落点图数据 进行渲染
+        } else {
+          this.drawLine([])
+        }
+      }
+
+    }
+  }
+}
+</script>
+
+<style scoped>
+#singleScatter {
+  width: 420px;
+  height: 420px;
+  margin: 0 auto;
+  display: block;
+  margin-top: 20px;
+}
+</style>

+ 291 - 0
TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseTestScatter.vue

@@ -0,0 +1,291 @@
+<template>
+  <div id="testScatter"></div>
+</template>
+
+<script>
+export default {
+  name: 'testScatter',
+  props: ['currentIndex', 'scatterData'],
+  data() {
+    return {
+      originArr: [],
+      dataArr: [],
+      activeItemIndex: 0,
+      exersicesType: this.$GLOBAL.EXERCISE_TYPES(),
+    }
+  },
+  created() {
+
+  },
+
+  methods: {
+    drawLine(data) {
+      // 基于准备好的dom,初始化echarts实例
+      let myScatter = this.$echarts.init(document.getElementById('testScatter'), 'chalk')
+      let _this = this
+      console.log('试题落点数据', data)
+
+      // 指定图表的配置项和数据
+      var option = {
+        tooltip: {
+          trigger: 'item',
+          showDelay: 0,
+          axisPointer: {
+            show: true,
+            lineStyle: {
+              type: 'dashed',
+              width: 1
+            }
+          },
+          textStyle: {
+            height: '160px'
+          },
+          formatter: function (params) {
+            const item = params
+            return `${_this.$t('totalAnalysis.ta_table_text1')}:${item.data[3]}
+                        <br/>${_this.$t('totalAnalysis.ta_table_text2')}:${that.exersicesType[item.data[2]]}
+                        <br/>${_this.$t('totalAnalysis.ta_chart_text2')}:${item.data[1]}%
+                       `
+          }
+        },
+        legend: {
+          data: [_this.$t('totalAnalysis.ta_chart_text1')]
+        },
+        grid:{
+          containLabel:true
+        },
+        // dataZoom: [{
+        // 		show: true,
+        // 		type: 'inside',
+        // 		start: 0,
+        // 		end: 100,
+        // 		bottom: 10,
+        // 		height: 15
+        // 	},
+        // 	{
+        // 		show: true,
+        // 		type: 'slider',
+        // 		start: 0,
+        // 		end: 100,
+        // 		bottom: 0,
+        // 		height: 35,
+        // 		textStyle: {
+        // 			color: '#BBBBBB'
+        // 		}
+        // 	}
+        // ],
+        xAxis: [{
+          type: 'value',
+          splitNumber: 2,
+          min: 0,
+          max: 1,
+          name: _this.$t('totalAnalysis.ta_chart_text3'),
+          nameTextStyle: {
+            color: '#757575'
+          },
+          scale: false,
+          splitLine: {
+            show: true,
+            lineStyle: {
+              color: '#595959'
+            }
+          }
+        }],
+        yAxis: [{
+          type: 'value',
+          name: _this.$t('totalAnalysis.ta_chart_text2'),
+          nameTextStyle: {
+            color: '#757575'
+          },
+          max: 100,
+          min: 0,
+          splitLine: {
+            show: false,
+            lineStyle: {
+              color: '#595959'
+            }
+          },
+          interval: 50,
+          axisLabel: {
+            show: true,
+            formatter: function (value) {
+              let val = []
+              if (value !== 25) {
+                val.push(value + '%')
+              }
+              return val
+            }
+          },
+          spiltArea: {
+            show: true
+          }
+        }],
+        series: [{
+          name: _this.$t('totalAnalysis.ta_chart_text1'),
+          type: 'scatter',
+          data: data,
+          symbolSize: 20,
+          emphasis: {
+            itemStyle: {
+              color: '#ff99cc'
+            }
+          },
+          // itemStyle: {
+          //  color: "#79c8e8"
+          // },
+          itemStyle: {
+            color: function (params) {
+              var key = params.dataIndex
+              if (key === _this.activeItemIndex) {
+                return '#ff99cc'
+              } else {
+                return '#79c8e8'
+              }
+            },
+            width: 20
+          },
+          markLine: {
+            silent: true,
+            animation: false,
+            data: [{
+              yAxis: 50
+            }, {
+              yAxis: 100
+            }],
+            label: {
+              color: '#757575',
+              formatter: '{c}%'
+            },
+            lineStyle: {
+              color: '#595959',
+              type: 'solid'
+            }
+          },
+          markArea: {
+            silent: true,
+            data: [
+              [{
+                xAxis: '0.5',
+                yAxis: '50',
+                itemStyle: {
+                  color: 'rgba(128,28,128,0)'
+                },
+                label: {
+                  show: true,
+                  position: ['90%', '10%'],
+                  color: '#008955',
+                  fontSize: 20,
+                  formatter: 'B'
+                }
+              }, {
+                xAxis: '0',
+                yAxis: '0'
+              }],
+              [{
+                xAxis: '1',
+                yAxis: '50',
+                itemStyle: {
+                  color: 'rgba(90,90,90,.1)'
+
+                },
+                label: {
+                  show: true,
+                  position: ['5%', '10%'],
+                  color: '#008955',
+                  fontSize: 20,
+                  formatter: "B'"
+                }
+
+              }, {
+                xAxis: '0.5',
+                yAxis: '0'
+              }],
+              [{
+                xAxis: '0.5',
+                yAxis: '100',
+                itemStyle: {
+                  color: 'rgba(90,90,90,.1)'
+
+                },
+                label: {
+                  show: true,
+                  position: ['90%', '80%'],
+                  color: '#008955',
+                  fontSize: 20,
+                  formatter: 'A'
+                }
+
+              }, {
+                xAxis: '0',
+                yAxis: '50'
+              }],
+              [{
+                xAxis: '1',
+                yAxis: '100',
+                itemStyle: {
+                  color: 'rgba(128,128,128,0)'
+
+                },
+                label: {
+                  show: true,
+                  position: ['5%', '40%'],
+                  color: '#008955',
+                  fontSize: 20,
+                  formatter: "A'"
+                }
+              }, {
+                xAxis: '0.5',
+                yAxis: '0'
+              }]
+            ]
+          }
+        }]
+      }
+
+      // 绘制图表
+      myScatter.setOption(option)
+      window.addEventListener('resize', function () {
+        myScatter.resize()
+      })
+
+      let that = this
+      myScatter.on('click', function (item) {
+        that.activeItemIndex = item.dataIndex
+        that.$emit('handleIndexClick', item.dataIndex + 1)
+        myScatter.setOption(option)
+      })
+    }
+  },
+  watch: {
+    scatterData: {
+      deep: true,
+      handler(val) {
+        if (val) {
+          console.error('val========', val)
+          this.drawLine(val)
+        }
+      }
+    },
+    currentIndex: {
+      deep: true,
+      handler(val) {
+        if (val && this.scatterData) {
+          this.activeItemIndex = +val - 1
+          this.drawLine(this.scatterData)
+        }
+      }
+    }
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+#testScatter {
+  width: 100%;
+  height: 500px;
+  margin: 0 auto;
+  display: block;
+  margin-top: 20px;
+}
+</style>

+ 35 - 26
TEAMModelOS/ClientApp/src/store/module/totalAnalysis.js

@@ -91,32 +91,41 @@ export default {
 			let pointLevelKey = []
 			if(!val) return
 			/* 对知识点模块和认知层次模块进行数据格式化 */
-			val.subjects.forEach((subject,subjectIndex) => {
-				pointLevelKey.push({
-					subjectId:subject.id,
-					levelKey:{
-						level:val.fieldName[subjectIndex].value,
-						pointList:val.fieldName[subjectIndex].value,
-						per:val.fieldPer[subjectIndex].value,
-						wrong:{
-							datas:val.fieldwrong[subjectIndex].value,
-							keys:val.wrongKey
-						},
-						stupercent:tools.getLevelPercent(val,subjectIndex).stuResult,
-						classpercent:tools.getLevelPercent(val,subjectIndex).classResult
-					},
-					pointKey:{
-						level:val.knowName[subjectIndex].value,
-						pointList:val.knowName[subjectIndex].value,
-						per:val.knowPer[subjectIndex].value,
-						wrong:{
-							datas:val.wrong[subjectIndex].value,
-							keys:val.wrongKey
-						},
-						stupercent:tools.getKnowPercent(val,subjectIndex).stuResult,
-						classpercent:tools.getKnowPercent(val,subjectIndex).classResult
-					}
-				})
+			val.subjects.forEach((subject) => {
+
+                let fieldNameItem = val.fieldName.find(i => i.key === subject.id)
+                let fieldPerItem = val.fieldPer.find(i => i.key === subject.id)
+                let fieldwrongItem = val.fieldwrong.find(i => i.key === subject.id)
+                let knowNameItem = val.knowName.find(i => i.key === subject.id)
+                let knowPerItem = val.knowPer.find(i => i.key === subject.id)
+                let wrongItem = val.wrong.find(i => i.key === subject.id)
+                if(fieldNameItem && fieldPerItem && fieldwrongItem && knowNameItem && knowPerItem && wrongItem){
+                    pointLevelKey.push({
+                        subjectId:subject.id,
+                        levelKey:{
+                            level:fieldNameItem.value,
+                            pointList:fieldNameItem.value,
+                            per:fieldPerItem.value,
+                            wrong:{
+                                datas:fieldwrongItem.value,
+                                keys:val.wrongKey
+                            },
+                            stupercent:tools.getLevelPercentJoint(val,subject).stuResult,
+                            classpercent:tools.getLevelPercentJoint(val,subject).classResult
+                        },
+                        pointKey:{
+                            level:knowNameItem.value,
+                            pointList:knowNameItem.value,
+                            per:knowPerItem.value,
+                            wrong:{
+                                datas:wrongItem.value,
+                                keys:val.wrongKey
+                            },
+                            stupercent:tools.getKnowPercentJoint(val,subject).stuResult,
+                            classpercent:tools.getKnowPercentJoint(val,subject).classResult
+                        }
+                    })
+                }				
 			})
 			val.pointLevelKey = pointLevelKey
 			state.currentSubject = null

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

@@ -1851,7 +1851,7 @@ export default {
 				}
 		})
 		return result
-	},
+	},   
 	/* 学情认知层次年级班级得分率数据转换 */
 	getLevelPercent(val, subjectIndex) {
 		let stuResult = {}
@@ -1871,6 +1871,131 @@ export default {
 		}
 		return obj
 	},
+
+	//================活動版 start================//
+	/* 学情认知层次年级班级得分率数据转换 活動版 */
+	getLevelPercentJoint(val, subject) {
+		let stuResult = {}
+		let classResult = {}
+		let fieldNameItem = val.fieldName.find(i => i.key === subject.id)
+		let fieldAllPerItem = val.fieldAllPer.find(i => i.key === subject.id)
+		let fieldwrongItem = val.fieldwrong.find(i => i.key === subject.id)
+		fieldNameItem.value.forEach((item, index) => {
+			stuResult[item] = this.getLevelStuPercentJoint(val, subject, index)
+			classResult[item] = this.getLevelClassPercentJoint(val, subject, index)
+		})
+		stuResult.grade = fieldAllPerItem.value.map((score, index) => (fieldwrongItem.value[
+			index][1] == 0 ? 0 : Number(score / fieldwrongItem.value[index][1]).toFixed(2)) * 100)
+		stuResult.keys = val.knowKey
+		classResult.keys = val.knowkey
+		classResult.className = val.classes.map(i => i.className)
+		let obj = {
+			stuResult: stuResult,
+			classResult: classResult
+		}
+		return obj
+	},
+	/* 学情认知层次模块数据转换 活動版 */
+	getLevelStuPercentJoint(val, subject, index) {
+		let result = []
+		val.students.forEach(stu => {
+			// if(stu.name==="沈仕程"){
+			// 	console.log("沈仕程 : "+JSON.stringify(stu));
+			// }
+			// 如果該學生的課程/科目存在  而且 id 相同才加入
+			let fScoresItem = val.fScores.find(i => i.key === subject.id)
+			let stuSubjectItem = stu.subjects.find(i => i.id === subject.id)
+			if(stuSubjectItem){
+				result.push([
+					stu.name,
+					stu.className,
+					stu.no || '-',
+					fScoresItem.value[index],
+					stuSubjectItem.fieldPoint[index],
+					fScoresItem.value[index] == 0 ? 0 : (stuSubjectItem.fieldPoint[index] / fScoresItem.value[index]).toFixed(2)
+				])
+			}			
+		})
+		return result
+	},
+	/* 学情认知层次年级班级得分率数据转换 活動版 */
+	getLevelClassPercentJoint(val, subject, index) {
+		let result = []
+		val.classes.forEach(classItem => {
+			// 如果該班級的課程/科目存在  而且 id 相同才加入
+			let classSubjectItem = classItem.subjects.find(i => i.id === subject.id)
+			let fieldwrongItem = val.fieldwrong.find(i => i.key === subject.id)
+			if(classSubjectItem){			
+			   result.push(classSubjectItem.field.map((score, index) => 
+				fieldwrongItem.value[index][1] == 0 ? 0 : Number(score / fieldwrongItem.value[index][1]) * 100)[index])
+			}
+		})
+		return result
+	},
+	/* 学情知识点年级班级得分率数据转换 活動版*/
+	getKnowPercentJoint(val, subject) {
+		let stuResult = {}
+		let classResult = {}
+		let knowNameItem = val.knowName.find(i => i.key === subject.id)
+		let knowAllperItem = val.knowAllper.find(i => i.key === subject.id)
+		let wrongItem = val.wrong.find(i => i.key === subject.id)
+		knowNameItem.value.forEach((item, index) => {
+			stuResult[item] = this.getKnowStuPercentJoint(val, subject, index)
+			classResult[item] = this.getKnowClassPercentJoint(val, subject, index)
+		})
+		// 取当前年级在每个知识点的得分 除以知识点的总分 得到年级在该知识点的得分率
+		stuResult.grade = knowAllperItem.value.map((score, index) => wrongItem.value[index][
+			1
+		] == 0 ? 0 : Number(score / wrongItem.value[index][1]) * 100)
+		stuResult.keys = val.knowKey
+		classResult.keys = val.knowkey
+		classResult.className = val.classes.map(i => i.className)
+		let obj = {
+			stuResult: stuResult,
+			classResult: classResult
+		}
+		return obj
+	},   
+	/* 学情知识点模块数据转换 活動版 */
+	getKnowStuPercentJoint(val, subject, index) {
+		let result = []
+		val.students.forEach(stu => {
+			// 如果該學生的課程/科目存在  而且 id 相同才加入
+			let stuSubjectItem = stu.subjects.find(i => i.id === subject.id)
+			let kScoresItem = val.kScores.find(i => i.key === subject.id)
+			if(stuSubjectItem){
+
+			result.push([
+				stu.name,
+				stu.className,
+				stu.no || '-',
+				kScoresItem.value[index],
+				stuSubjectItem.point[index],
+				kScoresItem.value[index] == 0 ? 0 : (stuSubjectItem.point[
+					index] / kScoresItem.value[index])
+					.toFixed(2)
+			])
+			}
+		})
+		return result
+	},
+	/* 学情知识点班级得分率数据转换 活動版 */
+	getKnowClassPercentJoint(val, subject, index) {
+		let result = []
+		val.classes.forEach(classItem => {
+			// 如果該 班級的課程/科目存在  而且 id 相同才加入
+			let classSubjectItem = classItem.subjects.find(i => i.id === subject.id)
+			let wrongItem = val.wrong.find(i => i.key === subject.id)
+			if(classSubjectItem){
+			// 取当前班级在每个知识点的得分 除以知识点的总分 得到每个班在该知识点的得分率 index=>知识点下标
+			result.push(classSubjectItem.point.map((score, pointIndex) => 
+				wrongItem.value[pointIndex][1] == 0 ? 0 : Number(score / wrongItem.value[pointIndex][1]) * 100)[index])
+					}
+		})
+		return result
+	},
+	//================活動版 end================//
+
 	/* 学情知识点模块数据转换 */
 	getKnowStuPercent(val, subjectIndex, index) {
 		let result = []

+ 114 - 9
TEAMModelOS/ClientApp/src/view/artexam/ExamSetting.vue

@@ -3,12 +3,16 @@
     <!-- 评测试卷 -->
     <div class="attr-item">
       <span>{{$t('ae.ae16')}}</span>
-      <Tag v-for="paper in subjectSetting.paper" color="blue">
-        {{ paper.name }}
-      </Tag>
+      <span v-for="(paper, index) in subjectSetting.paper" :key="index">
+        <Tag color="blue" v-if="index < 2">{{ paper.name }}</Tag>
+      </span>
+      <span v-show="subjectSetting.paper.length > 2">...</span>
       <span class="choose-paper" @click="selectPaper()">
         {{ subjectSetting.paper.length ? $t('ae.ae17') : $t('ae.ae18') }}
       </span>
+      <span v-show="subjectSetting.paper.length">
+        (已选{{ subjectSetting.paper.length }}张试卷)
+      </span>
     </div>
     <!-- 作答方式 -->
     <div class="attr-item">
@@ -45,6 +49,24 @@
             </Radio>
           </RadioGroup>
         </p>
+        <div style="margin-top: 15px;">
+          <span>{{ $t('evaluation.paperTag') }}:</span>
+          <Select v-model="selectTags" multiple style="width: 300px" @on-change="onfilterChange">
+            <Option v-for="item in tags" :value="item" :key="item">{{ item }}</Option>
+          </Select>
+        </div>
+        <div style="margin: 15px 0px 20px;">
+          <span>{{ $t('evaluation.paperList.paperName') }}:</span>
+          <Input v-special-char suffix="ios-search" v-model="searchVal" clearable
+            :placeholder="$t('evaluation.paperList.searchPaper')" style="width: 300px" @on-click="onfilterChange"
+            @on-change="onfilterChange" />
+        </div>
+        <div style="display: flex; float: right; width: 70px;">
+          <input type="checkbox" name="" id="" v-model="allCheck">
+          <label for="" style="margin-left: 5px;">全选</label>
+          <!-- <Checkbox v-model="allCheck" @on-change="checkAllPaper">全选</Checkbox> -->
+          <!-- <Button type="primary" @click="allCheck = true">Primary</Button> -->
+        </div>
         <div class="papaer-list-wrap">
           <vuescroll>
             <div :class="['paper-item', subjectSetting.paper.find(i => i.id === item.id) ? 'paper-item-active' : '']" v-for="(item,index) in paperListShow" :key="index">
@@ -69,11 +91,15 @@
                     {{ item.itemSort === 1 ? $t('evaluation.paperList.sortByOrder') : $t('evaluation.paperList.sortByType') }}
                   </span>
                 </span>
+                <span class="info-item">
+                  {{ $t('evaluation.tag') }}:
+                  <span class="info-bold" v-for="(tag, tagIndex) in item.tags" :key="tagIndex"><Tag color="blue">{{ tag }}</Tag></span>
+                </span>
               </div>
               <span v-if="item.id == selectedId" style="margin-left:20px;display:block;" class="paper-item-tools">
                 <Icon custom="iconfont icon-choose" style="margin-right:5px;" :title="$t('learnActivity.manual.stdPaper')" />
               </span>
-              <div v-else class="paper-item-tools">
+              <div v-else class="paper-item-tools" v-show="!isCheckAll">
                 <span @click.stop="choosePaper(index)">
                   <Icon custom="iconfont icon-choose" style="margin-right:5px;" />
                   {{ subjectSetting.paper.find(i => i.id === item.id) ? '取消选择' :  $t('learnActivity.manual.stPaper')}}
@@ -132,6 +158,11 @@ export default {
         tmd: [],
         school: []
       },
+      tags: [],
+      selectTags: [],
+      searchVal: '',
+      allCheck: false,
+      isCheckAll: false,
     }
   },
   methods: {
@@ -211,14 +242,19 @@ export default {
       this.findPaperList()
     },
     findPaperList() {
+      this.selectTags = []
+      this.searchVal = ''
+      this.tags = []
       if (!this.paperResource) {
         this.$Message.error(this.$t('ae.ae27'))
       }
       if (this.paperResource == 'hbcn' && this.paperList.tmd.length) {
-        return this.paperListShow = this.paperList.tmd.filter(item => item.subjectBindId === this.subject)
+        this.paperListShow = this.paperList.tmd.filter(item => item.subjectBindId === this.subject)
+        return this.tags = [...new Set(this.paperListShow.map(i => i.tags).flat(1))]
       }
       if (this.paperList.school.length) {
-        return this.paperListShow = this.paperList.school.filter(item => item.subjectBindId === this.subject)
+        this.paperListShow = this.paperList.school.filter(item => item.subjectBindId === this.subject)
+        return this.tags = [...new Set(this.paperListShow.map(i => i.tags).flat(1))]
       }
       let params = {
         schoolCode: this.paperResource
@@ -231,21 +267,89 @@ export default {
             this.paperList.school = res.papers
           }
           this.paperListShow = res.papers.filter(item => item.subjectBindId === this.subject)
+          this.tags = [...new Set(this.paperListShow.map(i => i.tags).flat(1))]
         }
       )
     },
+    /* 标签选择发生变化 */
+    onfilterChange() {
+      let subList = (this.paperResource == 'hbcn' && this.paperList.tmd.length) ? this.paperList.tmd.filter(item => item.subjectBindId === this.subject) : this.paperList.school.filter(item => item.subjectBindId === this.subject)
+      this.paperListShow = subList.filter(item => (this.searchVal ? item.name.includes(this.searchVal) : true) && (this.selectTags.length ? this.selectTags.find(j => item.tags.includes(j)) : true))
+      // this.allCheck = false
+    },
+    async checkAllPaper(value) {
+      let sasInfo
+      if (this.isArea) {
+        sasInfo = await this.$tools.getBlobSas("hbcn")
+        sasInfo.sas = "?" + sasInfo.sas
+      } else if (this.paperResource == this.$store.state.userInfo.schoolCode) {
+        sasInfo = {
+          sas: "?" + this.$store.state.user.schoolProfile.blob_sas,
+          url: this.$store.state.user.schoolProfile.blob_uri.slice(0, this.$store.state.user.schoolProfile.blob_uri.lastIndexOf('/')),
+          name: this.$store.state.userInfo.schoolCode
+        }
+      } else {
+        sasInfo = await this.$tools.getBlobSas("hbcn")
+        sasInfo.sas = "?" + sasInfo.sas
+      }
+      let noType = [] //试卷题目不符
+      this.isCheckAll = true
+      let promiseArr = []
+      this.paperListShow.forEach(item => {
+        try {
+          promiseArr.push(new Promise(async (resolve, reject) => {
+            let indexData = await this.$tools.getFile(`${sasInfo.url}/${sasInfo.name}${item.blob}/index.json${sasInfo.sas}`)
+            if (indexData) {
+              let paperIndex = JSON.parse(indexData)
+              let quTyep = ['single', 'multiple', 'judge']
+              if (paperIndex.slides.some(s => !quTyep.includes(s.type))) {
+                noType.push(item.name)
+              } else {
+                // this.subjectSetting.paper = this.paperListShow[index]
+                let findIndex = this.subjectSetting.paper.findIndex(i => i.id === item.id)
+                if(findIndex > -1 && !value){
+                  this.subjectSetting.paper.splice(findIndex,1)
+                } else if(findIndex === -1 && value) {
+                  this.subjectSetting.paper.push(item)
+                }
+              }
+            }
+            resolve(true)
+          }))
+        } catch (e) {
+          console.log(e)
+          // this.$Message.error(this.$t('ae.ae44'))
+        }
+      })
+      Promise.all(promiseArr).then(res => {
+        if(noType.length) {
+          let message = noType.join('、')
+          this.$Modal.info({
+            title: this.$t('ae.ae42'),
+            content: `${this.$t('ae.ae43')}(${message})`
+          })
+        }
+        this.isCheckAll = false
+        this.$Message.success('加入成功')
+        sessionStorage.setItem('art_paper_resource', this.paperResource)
+      })
+    },
   },
   watch: {
     subjectSetting: {
       deep: true,
       handler(n, o) {
-        console.error(this.subjectSetting)
         this.$emit('on-exam-change', {
           data: this.subjectSetting,
           subject: this.subject
         })
       }
-    }
+    },
+    allCheck: {
+      handler(n, o) {
+        this.checkAllPaper(n)
+      },
+    },
   },
   created() {
     // 判断是否为区级发布
@@ -348,7 +452,8 @@ export default {
   }
 }
 .papaer-list-wrap {
-  height: 600px;
+  height: 550px;
+  margin-bottom: 15px;
 }
 .choose-paper {
   color: #2d8cf0;

+ 17 - 6
TEAMModelOS/ClientApp/src/view/evaluation/bank/index.vue

@@ -1,5 +1,6 @@
 <template>
 	<div class="bank-container" ref="bankContainer">
+		<Loading v-show="isLoading" bgColor="rgba(0, 0, 0, 0.3)" style="z-index: 1368;"></Loading>
 		<div class="back-to-top flex-col-center" :title="$t('evaluation.backToTop')" @click="onBackToTop">
 			<Icon type="ios-arrow-up" />
 		</div>
@@ -95,7 +96,7 @@
 									<span>{{ $t("evaluation.df") }}</span>
 								</span>
 							</DropdownItem>
-							<DropdownItem name="go_art" v-if="isSchool && currentTab === 'paper' && showType">
+							<DropdownItem name="go_art" v-if="isSchool && currentTab === 'paper'">
 								<span @click="goPaperArt()" class="bank-tools-btn">
 									<Icon custom="iconfont icon-yishu" size="16" />
 									<span>艺术评测随机组卷</span>
@@ -114,10 +115,10 @@
 			</div>
 		</Modal>
 		<Modal v-model="paperArtModal" width="950px" title="艺术评测随机组卷" class-name="preview-modal">
-			<BaseQuickArtPaper ref="quickPaperArtRef" v-if="paperArtModal"></BaseQuickArtPaper>
+			<BaseQuickArtPaper ref="quickPaperArtRef" @finish="onArtFinish" v-if="paperArtModal"></BaseQuickArtPaper>
 			<div slot="footer">
 				<Button @click="paperArtModal = false">{{ $t('homework.form.cancel') }}</Button>
-				<Button type="primary" @click="doSaveQuickPaper" :loading="quickLoading">{{ $t('homework.form.save') }}</Button>
+				<Button type="primary" @click="doSaveArtPaper" :loading="quickLoading">{{ $t('homework.form.save') }}</Button>
 			</div>
 		</Modal>
 	</div>
@@ -138,7 +139,7 @@
 				fullPaperJson:null,
 				quickLoading: false,
 				checkItemArr: [],
-				isLoading: true,
+				isLoading: false,
 				tabName: "exercise",
 				currentTab: "exercise",
 				isAllOpen: false,
@@ -150,20 +151,30 @@
 		},
 		methods: {
 			doSaveQuickPaper() {
-				this.$refs.quickPaperArtRef.onConfirmSave();
+				this.$refs.quickPaperRef.onConfirmSave();
+			},
+			doSaveArtPaper() {
+				this.$refs.quickPaperArtRef.onConfirmSave()
 			},
 			editQuickPaper(fullPaperJson){
 				this.paperExamModal = true
 				this.fullPaperJson = fullPaperJson
 				console.error(fullPaperJson.attachments)
 			},
-			onQuickFinish(){
+			onQuickFinish() {
 				this.quickLoading = false
 				this.paperExamModal = false
 				this.fullPaperJson = null
 				this.$refs.paperList.doFilter()
 				this.$Message.success(this.$t('result.tip4'))
  			},
+			 onArtFinish() {
+				this.isLoading = false
+				this.quickLoading = false
+				this.paperArtModal = false
+				this.paperArtModal = null
+				this.$refs.paperList.doFilter()
+			 },
 			onItemCheckChange(arr) {
 				this.checkItemArr = arr;
 			},

+ 2 - 1
TEAMModelOS/ClientApp/src/view/htcommunity/htMgtExam.vue

@@ -714,7 +714,8 @@ export default {
 		// 點擊左側個人評量列表項目的動作
 		selectEvaluation(index) {
 			console.log('點擊左側個人評量列表項目的動作');
-			this.checkScoreSave(this.toEvaluation, index);
+			this.checkScoreSave(this.toEvaluation, index);			
+			this.examDetaiInfo = JSON.parse(JSON.stringify(this.htEvaListShow[index]));
 			this.$EventBus.$emit("onEvaChange", this.htEvaListShow[index]);
 		},
 		// 

+ 20 - 14
TEAMModelOS/ClientApp/src/view/learnactivity/tabs/htCloudDAS.vue

@@ -16,7 +16,11 @@
           <p class="info-date-person">
             <span class="info-item">{{$t('totalAnalysis.echarts_text11')}}: <span style="font-weight:bold" v-show="currentExamItem">{{ $tools.formatTime(currentExamItem.startTime,'yyyy-MM-dd')}}</span></span>
             <span class="info-item">{{$t('totalAnalysis.echarts_text12')}}: <span style="font-weight:bold" v-show="currentExamItem">{{currentExamItem.stuCount}}</span></span>
-            <span class="info-item">{{$t('totalAnalysis.echarts_text13')}}: <span style="font-weight:bold" v-if="currentExamItem && currentExamItem.stuCount != null && currentExamItem.lostStu">{{currentExamItem.stuCount - currentExamItem.lostStu.length}}</span></span>
+            <span class="info-item">{{$t('totalAnalysis.echarts_text13')}}: 
+              <span style="font-weight:bold" 
+                 v-if="currentExamItem && currentExamItem.stuCount != null && currentExamItem.lostStu">{{currentExamItem.stuCount - currentExamItem.lostStu}}
+              </span>
+            </span>
             <span class="info-item">{{$t('totalAnalysis.echarts_text14')}}: <span style="font-weight:bold">{{getJoinRate}}</span></span>
             <span class="info-item">{{$t('totalAnalysis.echarts_text15')}}: <span style="font-weight:bold">{{totalAverage}}</span></span>
           </p>
@@ -25,9 +29,9 @@
           <div class="data-select-items" style="display:inline-block">
             <span :class="activeBar == 0 ? 'data-select-active' : ''" @click="handleDataSelect(0)">{{$t('totalAnalysis.module1')}}</span>
             <span :class="activeBar == 1 ? 'data-select-active' : ''" @click="handleDataSelect(1)">{{$t('totalAnalysis.module2')}}</span>
-            <!-- <span :class="activeBar == 2 ? 'data-select-active' : ''" @click="handleDataSelect(2)">{{$t('totalAnalysis.module3')}}</span>
+            <span :class="activeBar == 2 ? 'data-select-active' : ''" @click="handleDataSelect(2)">{{$t('totalAnalysis.module3')}}</span>
             <span :class="activeBar == 3 ? 'data-select-active' : ''" @click="handleDataSelect(3)">{{$t('totalAnalysis.module4')}}</span>
-            <span :class="activeBar == 4 ? 'data-select-active' : ''" @click="handleDataSelect(4)">{{$t('totalAnalysis.module5')}}</span> -->
+            <span :class="activeBar == 4 ? 'data-select-active' : ''" @click="handleDataSelect(4)">{{$t('totalAnalysis.module5')}}</span>
           </div>
         </div>
         <div class="class-tab" v-if="!(activeBar === 2 || isShowQuestionList)">
@@ -35,11 +39,11 @@
         </div>
         <AchievementAnalysis v-if="activeBar == 0 && activeClassIndex == 0"></AchievementAnalysis>
         <SingleClassView v-else-if="activeBar == 0 && activeClassIndex > 0"></SingleClassView>
-        <!-- <QuestionListView v-else-if="isShowQuestionList"></QuestionListView> -->
+        <QuestionListView v-else-if="isShowQuestionList"></QuestionListView>
         <ScatterAnalysis v-else-if="activeBar == 1"></ScatterAnalysis>
-        <!-- <TestAnalysis v-else-if="activeBar == 2"></TestAnalysis>
+        <TestAnalysis v-else-if="activeBar == 2"></TestAnalysis>
         <KnowledgeAnalysis v-else-if="activeBar == 3"></KnowledgeAnalysis>
-        <LevelAnalysis v-else></LevelAnalysis> -->
+        <LevelAnalysis v-else></LevelAnalysis>
       </vuescroll>
     </div>
   </div>
@@ -47,12 +51,12 @@
 
 <script>
 import AchievementAnalysis from '@/view/student-analysis/total-analysis/AchievementAnalysis/htAchievementAnalysis.vue'
-import ScatterAnalysis from '@/view/student-analysis/total-analysis/ScatterAnalysis/ScatterAnalysis.vue'
-import TestAnalysis from '@/view/student-analysis/total-analysis/TestAnalysis/TestAnalysis.vue'
-import KnowledgeAnalysis from '@/view/student-analysis/total-analysis/KnowledgeAnalysis/KnowledgeAnalysis.vue'
-import LevelAnalysis from '@/view/student-analysis/total-analysis/LevelAnalysis/LevelAnalysis.vue'
-import SingleClassView from '@/view/student-analysis/total-analysis/AchievementAnalysis/EarlyWarning.vue'
-import QuestionListView from '@/view/student-analysis/total-analysis/TestAnalysis/QuestionList.vue'
+import ScatterAnalysis from '@/view/student-analysis/total-analysis/ScatterAnalysis/htScatterAnalysis.vue'
+import TestAnalysis from '@/view/student-analysis/total-analysis/TestAnalysis/htTestAnalysis.vue'
+import KnowledgeAnalysis from '@/view/student-analysis/total-analysis/KnowledgeAnalysis/htKnowledgeAnalysis.vue'
+import LevelAnalysis from '@/view/student-analysis/total-analysis/LevelAnalysis/htLevelAnalysis.vue'
+import SingleClassView from '@/view/student-analysis/total-analysis/AchievementAnalysis/htEarlyWarning.vue'
+import QuestionListView from '@/view/student-analysis/total-analysis/TestAnalysis/htQuestionList.vue'
 export default {
   props: {
     evInfo: {
@@ -142,6 +146,8 @@ export default {
         // id: "888bcf9d-2428-442e-9630-cbe09e19d84d"
       }).then(res => {
         console.log(res)
+        this.currentExamItem.lostStu = res.all.lost;
+        this.currentExamItem.stuCount = res.all.total;
         this.showAnalysis = true
         this.getTotalAverage()
 
@@ -167,7 +173,7 @@ console.log(err)
     /* 获取当前评测的平均分 */
     getTotalAverage(data) {
       let analysisJson = this.$store.state.totalAnalysis.analysisJsonJoint
-      this.totalAverage = analysisJson.all.average
+      this.totalAverage = analysisJson.all.allAverage
 
       this.classList = [this.$t('totalAnalysis.allClasses')].concat([...new Set(analysisJson.classes.map(item => item.className))])
     },
@@ -199,7 +205,7 @@ console.log(err)
       }
     },
     getJoinRate() {
-      return this.currentExamItem.stuCount ? (((this.currentExamItem.stuCount - this.currentExamItem.lostStu.length) / this.currentExamItem.stuCount) * 100).toFixed(2) + '%' : 0
+      return this.currentExamItem.stuCount ? (((this.currentExamItem.stuCount - this.currentExamItem.lostStu) / this.currentExamItem.stuCount) * 100).toFixed(2) + '%' : 0
     }
   },
   mounted() {    

+ 5 - 5
TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/AchievementAnalysis/htAchievementAnalysis.vue

@@ -29,7 +29,7 @@
       <div v-if="isAllSubject">
         <span class="component-title">{{ isCloudas ? $t('totalAnalysis.scoreRate') : $t('totalAnalysis.ach_title3')}}</span>
         <BaseEntryBar v-if="!isCloudas" :echartData="classDatas" echartsId="entryNumberBar"></BaseEntryBar>
-        <BaseScoreRateBar v-if="isCloudas" echartsId="sRateBar11" :echartsData="getAnalysisJsonJoint.students"></BaseScoreRateBar>
+        <BaseScoreRateBar v-if="isCloudas" echartsId="sRateBar11" :echartsData="getAnalysisJsonJoint.students"></BaseScoreRateBar>        
       </div>
       <div v-else>
         <span class="component-title">{{$t('totalAnalysis.scoreRate')}}</span>
@@ -54,10 +54,10 @@
 </template>
 
 <script>
-import BaseBar from '@/components/student-analysis/total/BaseBar'
-import BaseEntryBar from '@/components/student-analysis/total/BaseEntryBar.vue'
-import BaseTable from '@/components/student-analysis/total/BaseMyTable.vue'
-import EntryTables from '@/view/student-analysis/total-analysis/AchievementAnalysis/EntryTables.vue'
+import BaseBar from '@/components/student-analysis/total/htBaseBar'
+import BaseEntryBar from '@/components/student-analysis/total/htBaseEntryBar.vue'
+import BaseTable from '@/components/student-analysis/total/htBaseMyTable.vue'
+import EntryTables from '@/view/student-analysis/total-analysis/AchievementAnalysis/htEntryTables.vue'
 import BaseScoreRateBar from '@/components/student-analysis/total/BaseScoreRateBar.vue'
 export default {
   components: {

+ 627 - 0
TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/AchievementAnalysis/htEarlyWarning.vue

@@ -0,0 +1,627 @@
+<template>
+  <div class="achievement-container">
+    <Row class-name="base-table-row">
+      <span class="component-title" style="width: 100%">{{ this.$t('totalAnalysis.classBaseInfo') }}</span>
+    </Row>
+    <Row class-name="component-percents">
+      <div class="percent-item warning-info-item" v-show="isAllSubject">
+        <div class="fl-col-center">
+          <span class="percent-name">{{ this.$t('totalAnalysis.ach_text11') }}</span>
+          <span class="percent-value">{{ currentClass.stuCount || 0 }}</span>
+        </div>
+      </div>
+      <div class="percent-item warning-info-item" v-show="isAllSubject && !isClouDAS">
+        <div class="fl-col-center">
+          <span class="percent-name">{{ this.$t('totalAnalysis.ach_text8') }}</span>
+          <span class="percent-value">{{ currentClass.lineCount || 0 }}</span>
+        </div>
+      </div>
+      <div class="percent-item" v-show="isAllSubject && !isClouDAS">
+        <div class="fl-col-center">
+          <span class="percent-name">{{ this.$t('totalAnalysis.ach_text9') }}</span>
+          <span class="percent-value">{{ currentClass.stuCount ? ((currentClass.lineCount / currentClass.stuCount) * 100).toFixed(1) : 0 }}%</span>
+        </div>
+      </div>
+      <div class="percent-item">
+        <div class="fl-col-center">
+          <span class="percent-name">{{ this.$t('totalAnalysis.ach_table_text3') }}</span>
+          <span class="percent-value">{{ currentClass.totalAverage.toFixed(1) }}</span>
+        </div>
+      </div>
+      <div class="percent-item">
+        <div class="fl-col-center">
+          <span class="percent-name">{{ this.$t('totalAnalysis.ach_table_text4') }}</span>
+          <span class="percent-value">{{ currentClass.standardDeviation.toFixed(1) }}</span>
+        </div>
+      </div>
+      <div class="percent-item">
+        <div class="fl-col-center">
+          <span class="percent-name">{{ this.$t('totalAnalysis.sca_chart_text1') }}</span>
+          <span class="percent-value">{{ currentClass.csRate }}%</span>
+        </div>
+      </div>
+    </Row>
+
+    <!-- 班级均分分析 -->
+    <Row class-name="averageBarRow base-table-row">
+      <Col span="12">
+      <span class="component-title" style="margin-right: 55px;">{{$t('totalAnalysis.ach_title8')}}</span>
+      <div class="sort-box">
+        <!-- <span
+						style="font-size:14px;margin-right:10px;color: #a9a9a9;">{{$t('totalAnalysis.ach_chart_text1')}}:</span> -->
+        <Select v-model="sortValue" style="width:120px" @on-change="handleSort">
+          <Option v-for="(item,index) in sortArr" :value="index" :label="item" :key="index"></Option>
+        </Select>
+      </div>
+      <div id="stuAverageBar"></div>
+      </Col>
+      <Col span="12">
+      <span class="component-title">{{ this.$t('totalAnalysis.scoreRate') }}</span>
+      <BaseScoreRateBar echartsId="sRateBar2" :echartsData="scoreRateBarData"></BaseScoreRateBar>
+      </Col>
+    </Row>
+    <!-- PR排名表格 -->
+    <Row class-name="base-table-row">
+      <BaseTable :columns="earlyPercentColumns" :tableDatas="tableData" :tableName="$t('totalAnalysis.ach_title7')" noPage tableRef="earlyWarningTable" ref="percentTable" v-show="warningIndex === 0"></BaseTable>
+    </Row>
+  </div>
+</template>
+
+<script>
+import BaseTable from '@/components/student-analysis/total/BaseMyTable.vue'
+import BaseScoreRateBar from '@/components/student-analysis/total/BaseScoreRateBar.vue'
+export default {
+  components: {
+    BaseTable, BaseScoreRateBar
+  },
+  data() {
+    return {
+      isAllSubject: true,
+      scoreRateBarData: [],
+      currentClass: {
+        stuCount: 0,
+        lineCount: 0,
+        totalAverage: 0,
+        standardDeviation: 0
+      },
+      echartData: [],
+      tableData: [],
+      currentPR: [],
+      subjectList: [],
+      classAverage: 0,
+      gradeAverage: 0,
+      areaAverage: 0,
+      warningIndex: 0,
+      sortValue: 0,
+      className: '',
+      sortArr: [this.$t('totalAnalysis.ach_chart_text2'), this.$t('totalAnalysis.ach_chart_text3'), this.$t(
+        'totalAnalysis.ach_chart_text4')],
+      earlyPercentColumns: [{
+        title: this.$t('totalAnalysis.base_name'),
+        key: 'name',
+        fixed: 'left',
+        minWidth: 100
+      },
+      {
+        title: this.$t('totalAnalysis.base_id'),
+        key: 'setNo',
+        minWidth: 100,
+        renderType: function (h, params) {
+          return h('span', params.row.setNo || '-')
+        }
+      },
+      {
+        title: this.$t('totalAnalysis.ach_table_text2'),
+        sortable: 'custom',
+        key: 'score',
+        minWidth: 100
+      },
+      {
+        title: this.$t('totalAnalysis.sca_chart_text1'),
+        sortable: 'custom',
+        key: 'sRate',
+        minWidth: 100,
+        renderType: function (h, params) {
+          return h('span', (Number(params.row.sRate)).toFixed(2) + '%')
+        },
+      },
+      {
+        title: this.$t('totalAnalysis.ach_table_text8'),
+        key: 'classRank',
+        sortable: 'custom',
+        minWidth: 200,
+        renderType: function (h, params) {
+          return h('span', `${params.row.classRank} (${params.row.classPR})`)
+        }
+      },
+      {
+        title: this.$t('totalAnalysis.ach_table_text9'),
+        key: 'gradeRank',
+        sortable: 'custom',
+        minWidth: 200,
+        renderType: function (h, params) {
+          return h('span', `${params.row.gradeRank} (${params.row.gradePR})`)
+        }
+      },
+      ]
+    }
+  },
+
+  created() {
+    this.className = this.$route.query.name
+  },
+
+  methods: {
+    warningTabClick(index) {
+      this.warningIndex = index
+    },
+
+    handleSort(index) {
+      let memberList = JSON.parse(JSON.stringify(this.tableData))
+      if (index === 0) {
+        this.echartData = memberList
+      } else if (index === 1) {
+        this.echartData = memberList.sort(function (a, b) {
+          return Number(b.score) - Number(a.score)
+        })
+      } else {
+        this.echartData = memberList.sort(function (a, b) {
+          return Number(b.score) - Number(a.score)
+        }).reverse()
+      }
+      this.drawLine(this.echartData)
+    },
+
+    onSelectChange(val) {
+      this.tableData = this.onGetClassTable(val)
+      this.drawLine(this.tableData)
+    },
+
+    goBack() {
+      this.$store.commit('updateExportTable', [{
+        tableRef: 'entryTable',
+        tableName: '进线情况统计'
+      }, {
+        tableRef: 'entryRateTable',
+        tableName: '进线率统计'
+      }])
+      this.$router.back(-1)
+      this.$parent.isShowQuestions = false
+    },
+
+    /**
+     * 获取指定科目的表格数据
+     * @param subjectIndex
+     */
+    onGetClassTable(subjectIndex) {
+      let result = []
+      let analysisJson = JSON.parse(JSON.stringify(this.getAnalysisJson))
+      let curClassItem = analysisJson.classes.filter(i => i.className === this.className)[0]
+      let studentIds = curClassItem.studentIds
+      let singleSubjectItem = curClassItem.subjects[subjectIndex - 1]
+      this.currentClass = curClassItem
+      this.currentClass.standardDeviation = subjectIndex === 0 ? curClassItem.standardDeviation : singleSubjectItem.standard
+      this.currentClass.totalAverage = subjectIndex === 0 ? curClassItem.totalAverage : singleSubjectItem.average
+      this.currentClass.csRate = subjectIndex === 0 ? curClassItem.csRate : singleSubjectItem.sRate
+      this.classAverage = subjectIndex === 0 ? curClassItem.totalAverage.toFixed(1) : singleSubjectItem.average
+
+      // 判断当前选择科目是全科还是单科 赋予不同的值
+      studentIds.forEach((stuId, stuIndex) => {
+        let stuItem = analysisJson.students.filter(i => i.id === stuId)[0]
+        let stuSubjectItem = subjectIndex === 0 ? null : analysisJson.students.filter(i => i.id === stuId)[0].subjects[subjectIndex - 1]
+        let studentItemScores = subjectIndex === 0 ? {} : this.getStudentItemScore(stuIndex, analysisJson, subjectIndex - 1)
+        let baseObj = {
+          name: stuItem.name,
+          setNo: stuItem.no,
+          sRate: subjectIndex === 0 ? stuItem.sRate : stuSubjectItem.sRate,
+          score: subjectIndex === 0 ? stuItem.total : stuSubjectItem.score,
+          classPR: subjectIndex === 0 ? stuItem.cpr : stuSubjectItem.cpr,
+          classRank: subjectIndex === 0 ? stuItem.csort : stuSubjectItem.csort,
+          gradePR: subjectIndex === 0 ? stuItem.gpr : stuSubjectItem.gpr,
+          gradeRank: subjectIndex === 0 ? stuItem.gsort : stuSubjectItem.gsort,
+          areaPR: 0,
+          areaRank: 0
+        }
+        result.push(Object.assign(baseObj, studentItemScores))
+      })
+      console.log(result)
+      this.scoreRateBarData = result
+
+      if (subjectIndex !== 0) {
+        // this.earlyPercentColumns.push
+        let itemsLength = analysisJson.paper[subjectIndex - 1].value
+        let itemsColumns = []
+        itemsLength.forEach((i, index) => {
+          itemsColumns.push({
+            title: this.$t('survey.question') + (index + 1),
+            key: 's' + (index + 1),
+            minWidth: 100
+          })
+        })
+        this.earlyPercentColumns.splice(6, this.earlyPercentColumns.length - 6, ...itemsColumns)
+      } else {
+        this.earlyPercentColumns.splice(6, this.earlyPercentColumns.length - 6)
+      }
+      return result
+    },
+
+    /* 获取每个学生每道题目的得分详细表 */
+    getStudentItemScore(stuIndex, analysisJson, subjectIndex) {
+      let itemsLength = analysisJson.paper[subjectIndex].value
+      let result = {}
+      itemsLength.forEach((i, index) => {
+        result['s' + (index + 1)] = analysisJson.students[stuIndex].subjects[subjectIndex].scores[index]
+      })
+      return result
+    },
+
+    /**
+     * 渲染柱状图
+     * @param echartData
+     */
+    drawLine(echartData) {
+      let that = this
+      let myBar = this.$echarts.init(document.getElementById('stuAverageBar'))
+      var option = {
+        legend: {
+          top: 40,
+          data: [{
+            name: this.$t('totalAnalysis.ach_table_text2'),
+            textStyle: {
+              color: '#7d7d7d'
+            }
+          },
+          {
+            name: this.$t('totalAnalysis.ach_text4'),
+            color: 'red',
+            textStyle: {
+              color: '#7d7d7d'
+            }
+          },
+          {
+            name: this.$t('totalAnalysis.ach_text5'),
+            color: 'red',
+            textStyle: {
+              color: '#7d7d7d'
+            }
+          },
+          {
+            name: this.$t('totalAnalysis.ach_text6'),
+            color: 'red',
+            textStyle: {
+              color: '#7d7d7d'
+            }
+          }
+          ]
+
+        },
+        tooltip: {
+          show: true, // 是否显示提示框,默认为true
+          trigger: 'axis', // 数据项图形触发
+          axisPointer: {
+            // 指示样式
+            type: 'shadow',
+            axis: 'auto',
+            shadowStyle: {
+              color: 'rgba(128,128,128,0.1)'
+            }
+          },
+          padding: 5,
+          textStyle: {
+            color: '#fff'
+          },
+          formatter: function (params) {
+            let name = echartData[params[0].dataIndex].name
+            let result = name
+            for (let i = 0; i < params.length; i++) {
+              result += '<br>' + params[i].marker + params[i].seriesName + ':' + params[i].data
+                .toFixed(2)
+            }
+            return result
+          }
+        },
+        grid: {
+          show: false,
+          containLabel: true,
+          height: 300,
+          width: '75%',
+          right: '18%',
+          tooltip: {
+            show: true,
+            trigger: 'axis', // 触发类型
+            textStyle: {
+              color: '#666'
+            }
+          }
+        },
+        dataZoom: [{
+          'show': true,
+          'height': 10,
+          'xAxisIndex': [
+            0
+          ],
+          bottom: 10,
+          'start': 0,
+          'end': 100,
+          handleIcon: 'M512 497.821538m-418.264615 0a418.264615 418.264615 0 1 0 836.52923 0 418.264615 418.264615 0 1 0-836.52923 0Z',
+          handleSize: '160%',
+          handleStyle: {
+            color: '#d3dee5'
+
+          },
+          textStyle: {
+            color: '#7d7d7d'
+          },
+          borderColor: '#90979c'
+        }, {
+          'type': 'inside',
+          'show': true,
+          'height': 15,
+          'start': 0,
+          'end': 100
+        }],
+        // toolbox: {
+        //  show: true,
+        //  feature: {
+        //    magicType: { show: true, type: ['line', 'bar'] },
+        //    saveAsImage: { show: true }
+        //  }
+        // },
+        xAxis: {
+          show: true,
+          offset: 0,
+          type: 'category',
+          nameTextStyle: {
+            color: '#a2a2a2'
+          },
+          name: this.$t('totalAnalysis.base_id'),
+          axisLabel: {
+            show: true,
+            inside: false,
+            margin: 15,
+            color: '#989898'
+          },
+          data: echartData.map(item => item.setNo || '-')
+        },
+        yAxis: {
+          name: this.$t('totalAnalysis.ach_table_text2'),
+          show: true,
+          nameTextStyle: {
+            color: '#a2a2a2'
+          },
+          type: 'value',
+          axisLabel: {
+            show: true,
+            inside: false,
+            rotate: 0,
+            margin: 8,
+            color: '#989898',
+            fontSize: 12,
+            fontFamily: 'Microsoft YaHei'
+          },
+          splitLine: {
+            show: true,
+            lineStyle: {
+              color: '#4c504a',
+              width: 0.5,
+              type: 'solid'
+            }
+          }
+        },
+        series: [{
+          name: this.$t('totalAnalysis.ach_table_text2'),
+          type: 'bar',
+          itemStyle: {
+            normal: { // 渐变色
+              color: {
+                type: 'linear',
+                x: 0,
+                y: 0,
+                x2: 0,
+                y2: 1,
+                colorStops: [{
+                  offset: 0,
+                  color: '#3DC3F0' // 0% 处的颜色
+                }, {
+                  offset: 1,
+                  color: '#3486b1' // 100% 处的颜色
+                }]
+              }
+            },
+          },
+          barMaxWidth: 40,
+          markPoint: {
+            data: [{
+              type: 'max',
+              name: '最大值',
+              itemStyle: {
+                color: '#0099ff'
+              }
+            },
+            {
+              type: 'min',
+              name: '最小值',
+              itemStyle: {
+                color: 'red'
+              }
+            }
+            ],
+
+          },
+          data: echartData.map(item => item.score)
+        },
+        {
+          name: this.$t('totalAnalysis.ach_text4'),
+          type: 'line',
+          itemStyle: {
+            color: '#9d9d00',
+            width: 0
+          },
+          symbol: 'none',
+          lineStyle: {
+            type: 'dashed',
+            width: 0
+          },
+          markLine: {
+            data: [{
+              type: 'average'
+            }],
+            lineStyle: {
+              color: '#9d9d00',
+              type: 'dashed'
+            }
+          },
+          data: echartData.map(item => Number(this.classAverage))
+        },
+        ]
+      }
+
+      // 绘制图表
+      myBar.setOption(option)
+      window.addEventListener('resize', function () {
+        myBar.resize()
+      })
+    }
+
+  },
+
+  mounted() {
+    this.$parent.isShowQuestions = true
+    this.$refs.percentTable.$el.childNodes[1].style.borderRight = '0'
+
+    if (this.getAnalysisJson) {
+      this.tableData = this.onGetClassTable(0)
+      this.subjectList = this.getAnalysisJson.subjects.map(item => item.name)
+      this.drawLine(this.tableData)
+    }
+
+    this.$EventBus.$off('changeClassName')
+    this.$EventBus.$on('changeClassName', val => {
+      this.className = val
+      this.tableData = this.onGetClassTable(0)
+      this.subjectList = this.getAnalysisJson.subjects.map(item => item.name)
+      this.drawLine(this.tableData)
+      this.handleSort(this.sortValue)
+    })
+
+    this.$EventBus.$off('onEarlySubjectChange')
+    this.$EventBus.$on('onEarlySubjectChange', index => {
+      this.isAllSubject = index === 0
+      this.onSelectChange(index)
+      this.handleSort(this.sortValue)
+    })
+  },
+
+  computed: {
+    getAnalysisJson() {
+      return this.$store.state.totalAnalysis.analysisJsonJoint
+    },
+    isClouDAS() {
+      let curExam = JSON.parse(localStorage.getItem('curExam'))
+      let isPrivateExam = curExam.owner === 'teacher'
+      return this.$route.name === 'privExam' || isPrivateExam
+    }
+  },
+  watch: {
+    getAnalysisJson(val) {
+      if (!val) return
+      this.tableData = this.onGetClassTable(0)
+      this.subjectList = val.subjects.map(item => item.name)
+      this.drawLine(this.tableData)
+    }
+
+  }
+}
+</script>
+
+<style src="./AchievementAnalysis.less" lang="less" scoped></style>
+<style lang="less" scoped>
+.warning-info-item {
+  .percent-name {
+    font-size: 14px !important;
+  }
+
+  .percent-value {
+    padding-left: 0 !important;
+  }
+}
+
+.early-warning-tab {
+  margin-left: 60px;
+  font-size: 16px;
+}
+
+.early-warning-tab span {
+  border-bottom: 2px solid #808080;
+  margin-left: 20px;
+  padding: 5px 0;
+  cursor: pointer;
+}
+
+.early-warning-tab .early-warning-active {
+  font-weight: bold;
+  color: #07cdd8;
+  border-color: #07cdd8;
+}
+
+#stuAverageBar {
+  height: 400px;
+}
+
+.averageBarRow {
+  position: relative;
+}
+
+.sort-box {
+  position: absolute;
+  right: 100px;
+  top: 50px;
+  font-size: 12px;
+  z-index: 999;
+}
+</style>
+
+<style>
+.sort-box .ivu-input::-webkit-input-placeholder {
+  color: #a9a9a9;
+}
+
+.sort-box .ivu-select {
+  height: 25px;
+  line-height: 25px;
+  font-size: 12px;
+}
+
+.sort-box .ivu-select-single .ivu-select-selection {
+  height: 25px;
+  line-height: 25px;
+  background: transparent;
+  border: 1px solid #a9a9a9;
+  box-shadow: none;
+}
+
+.sort-box .ivu-select-selected-value {
+  line-height: 25px !important;
+  height: 25px !important;
+  font-size: 12px !important;
+}
+
+.sort-box .ivu-select-single .ivu-select-dropdown {
+  /*background: #595959;*/
+  font-size: 12px;
+}
+
+.sort-box .ivu-select-single .ivu-select-placeholder {
+  height: 25px;
+  line-height: 25px;
+  font-size: 16px;
+}
+
+.sort-box .ivu-select-single .ivu-select-selected-value {
+  color: #a9a9a9;
+}
+
+.sort-box .ivu-select-single .ivu-select-arrow {
+  color: #a9a9a9;
+}
+</style>

+ 292 - 0
TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/AchievementAnalysis/htEntryTables.vue

@@ -0,0 +1,292 @@
+<template>
+  <div class="achievement-container">
+    <!-- 进线率统计 -->
+    <Row class-name="base-table-row">
+      <BaseTable :columns="entryRateColumns" :tableName="(isAllSubject && !isClouDAS) ? $t('totalAnalysis.ach_title5') : $t('totalAnalysis.ach_title5_2')" :tableDatas="entryBarData" tableRef="entryRateTable" :isScroll="false" :tableTitle="isAllSubject ? $t('totalAnalysis.ach_title5') : $t('totalAnalysis.ach_title5_2')" ref="rateTable"></BaseTable>
+    </Row>
+    <!-- 进线情况统计 -->
+    <Row class-name="base-table-row">         
+      <BaseTable ref="entryTable" :columns="entryNumberColumns" :tableName="$t('totalAnalysis.ach_title4')" :tableDatas="entryTableData" tableRef="entryTable" :pageSize="10" :tableTitle="$t('totalAnalysis.ach_title4')"></BaseTable>
+    </Row>
+  </div>
+</template>
+
+<script>
+import BaseTable from '@/components/student-analysis/total/htBaseMyTable.vue'
+export default {
+  props: {
+    subjectIndex: {
+      type: Number,
+      default: NaN
+    }
+  },
+  components: {
+    BaseTable
+  },
+  data() {
+    return {
+      entryBarData: [],
+      entryTableData: [],
+      entryData: [],
+      footfaultData: [],
+      tipContent: '',
+      classList: [],
+      joinNum: 0,
+      isAllSubject: true,
+      curSubjectIndex: 0,
+      analysisJson: '',
+      originNumberColumns: [],
+      originRateColumns: [],
+      entryNumberColumns: [{
+        title: this.$t('totalAnalysis.base_name'),
+        key: 'name',
+        minWidth: 100,
+        fixed: 'left'
+      },
+      {
+        title: this.$t('totalAnalysis.base_class'),
+        key: 'className',
+        filters: [],
+        filterMultiple: false,
+      },
+      {
+        title: this.$t('totalAnalysis.ach_table_text1'),
+        key: 'gradeRank',
+        renderType: 'renderRank'
+      },
+      {
+        title: this.$t('totalAnalysis.ach_table_text2'),
+        sortable: 'custom',
+        key: 'score',
+        renderType: 'renderEntry',
+        filters: [{
+          label: this.$t('totalAnalysis.ach_table_text6'),
+          value: 1
+        },
+        {
+          label: this.$t('totalAnalysis.ach_table_text7'),
+          value: 2
+        }
+        ],
+        filterMultiple: false,
+        // filterRemote(value, row) {}
+      }
+      ],
+      entryRateColumns: [{
+        title: this.$t('totalAnalysis.ach_table_text1'),
+        key: 'gradeRank',
+        sortable: 'custom'
+      },
+      {
+        title: this.$t('totalAnalysis.base_class'),
+        key: 'className',
+        filterMultiple: false,
+      },
+      {
+        title: this.$t('totalAnalysis.sca_chart_text1'),
+        key: 'csRate',
+        sortable: 'custom',
+        minWidth: 50,
+        renderType: 'renderCsRate',
+      },
+      {
+        title: this.$t('totalAnalysis.ach_text7'),
+        key: 'totalNum',
+        sortable: true
+      },
+      {
+        title: this.$t('totalAnalysis.ach_text8'),
+        key: 'entryNum',
+        sortable: true,
+      },
+      {
+        title: this.$t('totalAnalysis.ach_text9'),
+        key: 'overAverageRate',
+        sortable: 'custom',
+        renderType: 'renderOverRate',
+      },
+      {
+        title: this.$t('totalAnalysis.ach_table_text3'),
+        key: 'average',
+        renderType: function (h, params) {
+          return h('span', (Number(params.row.average)).toFixed(1))
+        },
+        sortable: 'custom'
+      },
+      {
+        title: this.$t('totalAnalysis.ach_table_text4'),
+        key: 'standardDeviation',
+        sortable: 'custom'
+      },
+      ]
+    }
+  },
+
+  created() {
+    if (this.getAnalysisJson) {
+      let filterArr = []
+      this.getAnalysisJson.classes.forEach(i => {
+        filterArr.push({
+          label: i.className,
+          value: i.className
+        })
+      })
+      this.entryRateColumns[1].filters = filterArr
+      this.entryNumberColumns[1].filters = filterArr
+    }
+    this.originNumberColumns = JSON.parse(JSON.stringify(this.entryNumberColumns))
+    this.originRateColumns = JSON.parse(JSON.stringify(this.entryRateColumns))
+    let scatter = this.$store.state.totalAnalysis.scatter
+    this.classList = this.$store.state.totalAnalysis.classList
+
+    this.analysisJson = this.getAnalysisJson
+    console.log(this.analysisJson)
+
+  },
+
+  methods: {
+
+    /* 添加科目的表头 */
+    renderColumns(data) {
+      // 渲染进线表格
+      let subjectList = data.classes[0].subjects
+      let subjectColumns = []
+      subjectList.forEach((item, index) => {
+        let subjectColumn = {
+          title: item.name,
+          sortable: 'custom',
+          key: item.name
+        }
+        if (this.isAllSubject || (!this.isAllSubject && this.curSubjectIndex === index)) {
+          subjectColumns.push(subjectColumn)
+        }
+      })
+      // 清除之前的科目columns 添加当前评测的科目columns
+      let fixColumns = this.isAllSubject ? 4 : 3
+      if (!this.isAllSubject) {
+        this.entryRateColumns = this.entryRateColumns.filter(i => i.key !== 'entryNum' && i.key !== 'overAverageRate')
+        this.entryNumberColumns = this.entryNumberColumns.filter(i => i.key !== 'score')
+      } else {
+        this.entryRateColumns = this.originRateColumns
+        this.entryNumberColumns = this.originNumberColumns
+      }
+      //this.entryNumberColumns.splice(fixColumns, this.entryNumberColumns.length - fixColumns, ...subjectColumns)
+
+      if (this.isClouDAS) {
+        this.entryRateColumns = this.entryRateColumns.filter(i => i.key !== 'entryNum' && i.key !== 'overAverageRate' && i.key !== 'standardDeviation')
+        this.entryNumberColumns = this.entryNumberColumns.filter(i => i.key !== 'score')
+      }
+
+    },
+
+    /* 获取进线情况统计表格数据 */
+    getTableDatas(analysisJson) {
+      let result = []
+      analysisJson.students.forEach((stu, stuIndex) => {
+        result.push({
+          name: stu.name,
+          className: stu.className,
+          gradeRank: stu.gsort,
+          score: this.isAllSubject ? stu.total : stu.subjects[this.curSubjectIndex].score,
+        })
+        // 动态添加学生每个科目的得分
+        stu.subjects.forEach((subject, subjectIndex) => {
+          if (this.isAllSubject || (!this.isAllSubject && this.curSubjectIndex ===
+            subjectIndex)) {
+            result[stuIndex][subject.name] = subject.score
+          }
+        })
+      })
+      /* 根据表格每个学生的总分来进行排序 */
+      // let rateRanks = result.sort((a, b) => {
+      // 	return Number(b.score) - Number(a.score)
+      // })
+      // result.forEach(item => {
+      // 	item.gradeRank = rateRanks.map(i => i.name).indexOf(item.name) + 1
+      // })
+      
+      return result
+    },
+
+    /* 获取进线率统计数据 */
+    getEntryBarData(analysisJson) {
+      let result = []
+      analysisJson.classes.forEach((classItem, classIndex) => {
+        result.push({
+          gradeRank: 0,
+          className: classItem.className,
+          entryNum: classItem.lineCount,
+          totalNum: classItem.stuCount,
+          csRate: this.isAllSubject ? classItem.csRate : classItem.subjects[this
+            .curSubjectIndex].sRate,
+          overAverageRate: classItem.stuCount > 0 ? ((classItem.lineCount / classItem
+            .stuCount) * 100).toFixed(2) : 0.00,
+          average: this.isAllSubject ? classItem.totalAverage.toFixed(1) : classItem
+            .subjects[this.curSubjectIndex].average.toFixed(1),
+          standardDeviation: this.isAllSubject ? classItem.standardDeviation.toFixed(1) :
+            classItem.subjects[this.curSubjectIndex].standard.toFixed(1),
+        })
+      })
+      /* 根据超均率 来对班级进行年级排名 */
+      let rateRanks = result.sort((a, b) => {
+        return Number(b.average) - Number(a.average)
+      })
+      result.forEach(item => {
+        item.gradeRank = rateRanks.map(i => i.className).indexOf(item.className) + 1
+      })
+      
+      return result
+    }
+  },
+  mounted() {
+    this.$refs.rateTable.$el.childNodes[1].style.borderRight = '0'
+    this.$refs.entryTable.$el.childNodes[1].style.borderRight = '0'
+
+    if (this.getAnalysisJson) {
+      this.entryTableData = this.getTableDatas(this.getAnalysisJson)
+      this.entryBarData = this.getEntryBarData(this.getAnalysisJson)
+
+      let filterArr = []
+      this.getAnalysisJson.classes.forEach(i => {
+        filterArr.push({
+          label: i.className,
+          value: i.className
+        })
+      })
+      this.entryRateColumns[1].filters = filterArr
+      this.entryNumberColumns[1].filters = filterArr
+      this.renderColumns(this.getAnalysisJson)
+    }
+
+  },
+  computed: {
+    getAnalysisJson() {
+      return this.$store.state.totalAnalysis.analysisJsonJoint
+    },
+    isClouDAS() {
+      let curExam = JSON.parse(localStorage.getItem('curExam'))
+      let isPrivateExam = curExam.owner === 'teacher'
+      return this.$route.name === 'privExam' || isPrivateExam
+    }
+  },
+  watch: {
+    getAnalysisJson(val) {
+      if (!val) return
+      this.renderColumns(val)
+      this.entryTableData = this.getTableDatas(val)
+      this.entryBarData = this.getEntryBarData(val)
+    },
+    subjectIndex: {
+      handler(n, o) {
+        this.isAllSubject = isNaN(n)
+        this.curSubjectIndex = n
+        let val = this.$store.state.totalAnalysis.analysisJsonJoint
+        this.renderColumns(val)
+        this.entryTableData = this.getTableDatas(val)
+        this.entryBarData = this.getEntryBarData(val)
+      },
+    }
+  }
+}
+</script>
+<style src="./AchievementAnalysis.less" lang="less" scoped></style>

+ 172 - 0
TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/KnowledgeAnalysis/htKnowledgeAnalysis.vue

@@ -0,0 +1,172 @@
+<template>
+    <div class="scatter-container">
+		<div v-show="!hasKnowledge" class="analysis-no-data">
+			<EmptyData :top="200" :textContent="$t('totalAnalysis.noKnowPointTip')"></EmptyData>
+		</div>
+		<div v-show='hasKnowledge'>
+			<Row class-name="base-table-row">
+			    <Col span="12">
+			        <span class="component-title" style="margin-right: 55px">{{$t('totalAnalysis.ka_title1')}}</span>
+			        <BasePie pieId="knowPie"></BasePie>
+			    </Col>
+			    <Col span="12" style="position:relative">
+			        <!-- <BaseKnowledgeRadar echartsId="knowPie2"></BaseKnowledgeRadar> -->
+					<span class="component-title">{{$t('totalAnalysis.ka_title7')}}</span>
+					<BaseRadar echartsId="knowRadar" :classIndex="isAllClasses"></BaseRadar>
+			    </Col>
+			</Row>
+			<Row class-name="base-table-row" v-show="isAllClasses === 0">
+			    <!-- 得分率关系表 -->
+			    <div style="width:100%">
+			        <BaseMyTable :columns="tableColumns"
+			                     :tableName="$t('totalAnalysis.ka_title3')"
+			                      tableRef="pointScoreRateTable"
+			                     :tableDatas="knowledgeData"></BaseMyTable>
+			    </div>
+			</Row>
+			<Divider />
+			<ScoreDetails ref="detailsRef" :classIndex="isAllClasses"></ScoreDetails>
+		</div>
+        
+    </div>
+</template>
+
+<script>
+    import BasePie from '@/components/student-analysis/total/htBasePie.vue'
+	import BaseRadar from '@/components/student-analysis/total/htBaseRadar.vue'
+    import BaseKnowledgeRadar from '@/components/student-analysis/total/BaseKnowledgeRadar.vue'
+    import BaseMyTable from '@/components/student-analysis/total/htBaseMyTable.vue'
+    import ScoreDetails from '@/view/student-analysis/total-analysis/KnowledgeAnalysis/htScoreDetails.vue'
+    export default {
+        components: {
+            BasePie, BaseKnowledgeRadar, BaseMyTable, ScoreDetails,BaseRadar
+        },
+        data() {
+            return {
+				isAllClasses:0,
+                isShowPie: true,
+                currentBlock: '全部',
+				classList:[],
+                tableData: [],
+                classDatas: [],
+                barData: [],
+                knowledgeData: [],
+                tableColumns: [
+                    {
+                        title: this.$t('totalAnalysis.ka_table_text1'),
+                        key: 'name',
+                        width: 200
+                    },
+                    {
+                        title: this.$t('totalAnalysis.ka_table_text3'),
+                        key: 'gradeRate',
+                        sortable: true,
+                        renderType: function(h, params) {
+                            return h('span', (Number(params.row.gradeRate)).toFixed(2) + '%')
+                        },
+                        width: 150
+                    }
+                ]
+            }
+        },
+        created() {
+            this.isShowPie = !(this.knowledgeData.length > 10)
+			this.$parent.$parent.$parent.isShowQuestions = false
+        },
+
+        methods: {
+            renderClassColumns(classData,origin) {
+				let classColumns = []
+                // 渲染得分率关系表格的班级数据
+				classData.classes.forEach((item,index) => {
+				    let classColumn = {
+				        title: item.className,
+				        sortable: 'custom',
+				        key: item.className,
+				        renderType: (h, params) => {
+				            return h('span', (( isNaN(origin[params.row.name][index]) ? 0 : Number(origin[params.row.name][index]))).toFixed(2) + '%')
+				        },
+				        minWidth: 150
+				    }
+				    classColumns.push(classColumn)
+				})
+				
+				// 清除之前的科目columns 添加当前评测的科目columns
+				this.tableColumns.splice(2,this.tableColumns.length - 2,...classColumns)
+            },
+
+            changePieOrBar() {
+                this.isShowPie = !this.isShowPie
+            },
+
+            doRender(data) {
+                let origin = data.classpercent
+                let pointList = data.pointList
+                let arr = []
+                for (let i = 0; i < pointList.length; i++) {
+                    let o = {}
+                    o.name = pointList[i]
+                    o.gradeRate = data.stupercent.grade[i]
+                    for (let j = 0; j < origin.className.length; j++) {
+                        o[origin.className[j]] = origin[pointList[i]][j]
+                    }
+                    arr.push(o)
+                }
+                this.renderClassColumns(this.getAnalysisJson,origin)
+				console.log(arr)
+                this.knowledgeData = arr
+            }
+
+        },
+        mounted() {
+            if (this.getKnowledgeData && this.hasKnowledge) {
+                this.doRender(this.getKnowledgeData)
+				this.classList = [this.$t('totalAnalysis.allClasses')].concat([...new Set(this.getAnalysisJson.classes.map(item => item.className))]) // 获取班级列表
+            }
+			
+			this.isAllClasses = this.$store.state.totalAnalysis.curClassIndex + 1
+			this.$EventBus.$off('changeClassName')
+			this.$EventBus.$on('changeClassName', val => {
+				this.isAllClasses = this.classList.indexOf(val)
+			})
+        },
+
+        computed: {
+            // 获取最新散点图数据
+            getKnowledgeData() {                                           
+				let curSubjectIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.name).indexOf(this.$store.state.totalAnalysis.currentSubjectJoint)
+                let AllpointKey = this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].pointKey
+                let pointList = this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[6].pointKey.pointList                
+                let pointLevelKey = this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey
+                for(let i = 0;i<pointLevelKey.length;i++){
+                    for(let j = 0;j<pointList.length;j++){
+                        if(pointLevelKey[i].pointKey.classpercent[pointList[j]] && pointLevelKey[i].pointKey.classpercent[pointList[j]].length>0){
+                            AllpointKey.classpercent[pointList[j]].push(pointLevelKey[i].pointKey.classpercent[pointList[j]][0]);                       
+                        }                      
+                   }
+                }
+
+                //return this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].pointKey
+                return AllpointKey
+            },
+			
+			getAnalysisJson() {
+			    return this.$store.state.totalAnalysis.analysisJsonJoint
+			},
+			
+			hasKnowledge(){
+				let curSubjectIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.name).indexOf(this.$store.state.totalAnalysis.currentSubjectJoint)
+				return this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].pointKey.pointList.length
+			}
+        },
+		
+		watch: {
+		    getKnowledgeData(val) {
+		        if (!val) return
+				this.doRender(val)
+		    }
+		}
+    }
+</script>
+
+<style src="./KnowledgeAnalysis.css" scoped></style>

+ 250 - 0
TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/KnowledgeAnalysis/htScoreDetails.vue

@@ -0,0 +1,250 @@
+<template>
+  <div class="scatter-container">
+    <Row class-name="base-table-row">
+      <Col span="12">
+      <span class="component-title" style="margin-right: 55px">{{$t('totalAnalysis.ka_title4')}}</span>
+      <!-- <span class="pie-to-bar" @click="changeRadarOrBar"><Icon :type="isShowRadar ? 'ios-podium' : 'ios-pie'" />{{isShowRadar?$t('totalAnalysis.ka_chart_text1'):$t('totalAnalysis.ka_chart_text2')}}</span> -->
+      <div>
+        <BaseDetailBar echartsId="knowDetailBar" :classIndex="classIndex" @handleItemClick="handleItemClick" ref="detailsBar"></BaseDetailBar>
+      </div>
+      <!-- <div v-if="isShowRadar">
+                    <BaseRadar echartsId="knowRadar" :classIndex="classIndex"></BaseRadar>
+                </div> -->
+      </Col>
+      <Col span="12">
+      <!-- 知识点得分详情 -->
+      <span class="component-title-point"><span>{{$t('totalAnalysis.ka_text2')}}:{{currentPoint}}</span></span>
+      <div>
+        <BaseMyTable :columns="detailsColumns" :tableName="$t('totalAnalysis.ka_title5') + '(' + currentPoint + ')'" tableRef="pointScoreTable" :tableDatas="tableData" ref="detailsTable"></BaseMyTable>
+      </div>
+      </Col>
+    </Row>
+    <Divider />
+    <Row class-name="base-table-row">
+      <div>
+        <BaseMyTable :columns="tableColumns" :tableName="$t('totalAnalysis.ka_title6')" tableRef="pointWrongTable" :tableDatas="numData" ref="numTable" :tips="$t('totalAnalysis.ka_tip1')"></BaseMyTable>
+      </div>
+      <!--<span style="/*font-size:14px;font-weight:bold;margin-left:60px;color:#66cccc*/"></span>-->
+    </Row>
+  </div>
+</template>
+
+<script>
+import BaseDetailBar from '@/components/student-analysis/total/htBaseKnowledgeDetail.vue'
+import BaseMyTable from '@/components/student-analysis/total/BaseMyTable.vue'
+import BaseRadar from '@/components/student-analysis/total/BaseRadar.vue'
+export default {
+  props: {
+    classIndex: {
+      type: Number,
+      default: -1
+    }
+  },
+  components: {
+    BaseDetailBar, BaseMyTable, BaseRadar
+  },
+  data() {
+    return {
+      isShowRadar: false,
+      curClassIndex: -1,
+      classList: [],
+      tableData: [],
+      classDatas: [],
+      currentPoint: '',
+      tipContent: '* RH:高分区段  /  RL:低分区段 (模拟数据,仅供参考)',
+      knowledgeData: [],
+      numData: [],
+      tableColumns: [
+        {
+          title: this.$t('totalAnalysis.ka_table_text1'),
+          key: 'name',
+          minWidth: 150
+        },
+        {
+          title: this.$t('totalAnalysis.ka_table_text4'),
+          key: 'point',
+          minWidth: 100,
+          sortable: 'custom',
+          renderType: function (h, params) {
+            return h('span', Number(params.row.point).toFixed(2))
+          }
+        },
+        {
+          title: this.$t('totalAnalysis.ka_table_text7'),
+          key: 'itemNO',
+          renderType: 'renderHard',
+          width: 250
+        },
+        {
+          title: this.$t('totalAnalysis.ka_table_text8'),
+          key: 'persent',
+          renderType: function (h, params) {
+            return h('span', (Number(params.row.persent) * 100).toFixed(2) + '%')
+          },
+          sortable: 'custom',
+          minWidth: 100
+        },
+        {
+          title: this.$t('totalAnalysis.ka_table_text9'),
+          key: 'wrong',
+          sortable: 'custom',
+          minWidth: 100
+        },
+        {
+          title: this.$t('totalAnalysis.ka_table_text10'),
+          key: 'rhw',
+          sortable: 'custom',
+          minWidth: 100
+        },
+        {
+          title: this.$t('totalAnalysis.ka_table_text11'),
+          key: 'rlw',
+          sortable: 'custom',
+          minWidth: 100
+        }
+      ],
+      detailsColumns: [
+        {
+          title: this.$t('totalAnalysis.base_name'),
+          key: 'id',
+          minWidth: 100
+        },
+        {
+          title: this.$t('totalAnalysis.base_class'),
+          key: 'className',
+          width: 120
+        },
+        {
+          title: this.$t('totalAnalysis.base_id'),
+          key: 'seatNO',
+          width: 100
+        },
+        {
+          title: this.$t('totalAnalysis.ka_table_text4'),
+          key: 'point',
+          sortable: 'custom',
+          renderType: function (h, params) {
+            return h('span', Number(params.row.point).toFixed(1))
+          },
+          minWidth: 100
+        },
+        {
+          title: this.$t('totalAnalysis.ka_table_text5'),
+          key: 'anwPoint',
+          minWidth: 100,
+          sortable: 'custom',
+          renderType: function (h, params) {
+            return h('span', Number(params.row.anwPoint).toFixed(1))
+          }
+        },
+        {
+          title: this.$t('totalAnalysis.ka_table_text6'),
+          key: 'persent',
+          minWidth: 100,
+          sortable: 'custom',
+          renderType: function (h, params) {
+            return h('span', (Number(params.row.persent) * 100).toFixed(2) + '%')
+          }
+        }
+      ]
+    }
+  },
+  created() {
+
+  },
+
+  methods: {
+
+    changeRadarOrBar() {
+      this.isShowRadar = !this.isShowRadar
+    },
+
+    // 点击柱状图某个点事件
+    handleItemClick(item) {
+      console.log(item)
+      this.currentPoint = item.name
+      this.doRender(this.getKnowledgeData, this.currentPoint, this.curClassIndex)
+    },
+
+    doRender(data, point) {
+      let classIndex = this.$store.state.totalAnalysis.curClassIndex
+      let origin = data.stupercent
+      let keys = origin.keys
+      let datas = classIndex === -1 ? (origin[point] || []) : origin[point].filter(i => i[1] === this.classList[classIndex + 1])
+      this.currentPoint = point
+      this.tableData = this.$tools.jsonTransform({ datas: datas, keys: keys })
+    },
+
+    doRenderWrong(data) {
+      let classIndex = this.$store.state.totalAnalysis.curClassIndex
+      let origin = data.wrong
+      let allWrongData = this.$tools.jsonTransform({ datas: origin.datas, keys: origin.keys })
+      this.numData = classIndex === -1 ? allWrongData : this.getClassWrongData(allWrongData, classIndex)
+    },
+
+    getClassWrongData(allWrongData) {
+      let classIndex = this.$store.state.totalAnalysis.curClassIndex
+      let curSubjectIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.name).indexOf(this.$store.state.totalAnalysis.currentSubjectJoint)
+      let curItem = this.getAnalysisJson.classes[classIndex].subjects[curSubjectIndex]
+      allWrongData.forEach((item, index) => {
+        item.persent = curItem.krate[index]
+        item.rhw = curItem.phc[index]
+        item.rlw = curItem.plc[index]
+        item.wrong = curItem.pc[index]
+      })
+      return allWrongData
+    }
+
+
+  },
+  mounted() {
+    this.$refs.detailsTable.$el.childNodes[1].style.borderRight = '0'
+    this.$refs.numTable.$el.childNodes[1].style.borderRight = '0'
+    if (this.getKnowledgeData) {
+      this.doRender(this.getKnowledgeData, this.getKnowledgeData.pointList[0], this.curClassIndex)
+      this.doRenderWrong(this.getKnowledgeData, this.curClassIndex)
+      this.classList = [this.$t('totalAnalysis.allClasses')].concat([...new Set(this.getAnalysisJson.classes.map(item => item.className))]) // 获取班级列表
+    }
+
+
+  },
+  computed: {
+    // 获取最新散点图数据
+    getKnowledgeData() {
+      let curSubjectIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.name).indexOf(this.$store.state.totalAnalysis.currentSubjectJoint)
+      return this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].pointKey
+    },
+    getAnalysisJson() {
+      return this.$store.state.totalAnalysis.analysisJsonJoint
+    },
+  },
+  watch: {
+    getKnowledgeData: {
+      deep: true,
+      handler(val) {
+        if (val) {
+          this.doRender(val, val.pointList[0], this.curClassIndex)
+          this.doRenderWrong(val, this.curClassIndex)
+        }
+      }
+    },
+    classIndex(n, o) {
+      this.curClassIndex = n - 1
+      this.doRender(this.getKnowledgeData, this.getKnowledgeData.pointList[0], n - 1)
+      this.doRenderWrong(this.getKnowledgeData, n - 1)
+    }
+  }
+}
+</script>
+
+<style src="./KnowledgeAnalysis.css" scoped></style>
+<style>
+.component-title-point {
+  position: absolute;
+  font-size: 14px;
+  font-weight: 600;
+  right: 220px;
+  top: 60px;
+  color: #70b1e7;
+}
+</style>

+ 159 - 0
TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/LevelAnalysis/htLevelAnalysis.vue

@@ -0,0 +1,159 @@
+<template>
+    <div class="scatter-container">
+        <Row class-name="base-table-row">
+            <Col span="12">
+                <span class="component-title" style="margin-right: 55px">{{$t('totalAnalysis.le_title1')}}</span>
+                <BasePie pieId="levelPie"></BasePie>
+            </Col>
+            <Col span="12" style="position:relative">
+                <!-- <BaseLevelRadar echartsId="levelPie2" v-if="isShowPie"></BaseLevelRadar> -->
+				<span class="component-title">{{$t('totalAnalysis.le_title7')}}</span>
+				<BaseRadar echartsId="levelRadar" :classIndex="isAllClasses"></BaseRadar>
+            </Col>
+        </Row>
+        <Row class-name="base-table-row"  v-show="isAllClasses === 0">
+            <div style="width:100%">
+                <BaseMyTable :columns="tableColumns"
+                             :tableName="$t('totalAnalysis.le_title3')"
+                              tableRef="levelScoreRateTable"
+                             :tableDatas="levelData"></BaseMyTable>
+            </div>
+        </Row>
+        <Divider />
+        <ScoreDetails ref="detailsRef" :classIndex="isAllClasses"></ScoreDetails>
+    </div>
+</template>
+
+<script>
+	import BaseRadar from '@/components/student-analysis/total/htBaseRadar.vue'
+    import BasePie from '@/components/student-analysis/total/htBaseLevelPie.vue'
+    import BaseLevelRadar from '@/components/student-analysis/total/BaseLevelRadar.vue'
+    import BaseMyTable from '@/components/student-analysis/total/BaseMyTable.vue'
+    import ScoreDetails from '@/view/student-analysis/total-analysis/LevelAnalysis/htScoreDetails.vue'
+    export default {
+        components: {
+            BasePie,BaseRadar, BaseLevelRadar, BaseMyTable, ScoreDetails
+        },
+        data() {
+            return {
+				isAllClasses:0,
+                isShowPie: true,
+                currentBlock: '全部',
+                tableData: [],
+                classDatas: [],
+                barData: [],
+                levelData: [],
+                tableColumns: [
+                    {
+                        title: this.$t('totalAnalysis.ka_table_text2'),
+                        key: 'name',
+                        width: 200
+                    },
+                    {
+                        title: this.$t('totalAnalysis.ka_table_text3'),
+                        key: 'gradeRate',
+                        sortable: true,
+                        renderType: function(h, params) {
+                            return h('span', (Number(params.row.gradeRate)).toFixed(2) + '%')
+                        },
+                        width: 150
+                    }
+                ]
+            }
+        },
+        created() {
+            this.isShowPie = !(this.levelData.length > 10)
+			this.$parent.$parent.$parent.isShowQuestions = false
+        },
+
+        methods: {
+            renderClassColumns(classData,origin) {
+				let classColumns = []
+                // 渲染得分率关系表格的班级数据
+            	classData.classes.forEach((item,index) => {
+            	    let classColumn = {
+            	        title: item.className,
+            	        sortable: 'custom',
+            	        key: item.className,
+            	        renderType: (h, params) => {
+            	            return h('span', ((Number(origin[params.row.name][index]))).toFixed(2) + '%')
+            	        },
+            	        minWidth: 150
+            	    }
+            	    classColumns.push(classColumn)
+            	})
+				
+				// 清除之前的科目columns 添加当前评测的科目columns
+				this.tableColumns.splice(2,this.tableColumns.length - 2,...classColumns)
+            },
+
+            changePieOrBar() {
+                this.isShowPie = !this.isShowPie
+            },
+
+            doRender(data) {
+                let origin = data.classpercent
+                let pointList = data.pointList
+                let arr = []
+                for (let i = 0; i < pointList.length; i++) {
+                    let o = {}
+                    o.name = pointList[i]
+                    o.gradeRate = data.stupercent.grade[i]
+                    for (let j = 0; j < origin.className.length; j++) {
+                        o[origin.className[j]] = origin[pointList[i]][j]
+                    }
+                    arr.push(o)
+                }
+                this.renderClassColumns(this.getAnalysisJson,origin)
+                this.levelData = arr
+            }
+
+        },
+        mounted() {
+            if (this.getLevelData) {
+                this.doRender(this.getLevelData)
+				this.classList = [this.$t('totalAnalysis.allClasses')].concat([...new Set(this.getAnalysisJson.classes.map(item => item.className))]) // 获取班级列表
+            }
+			
+			this.isAllClasses = this.$store.state.totalAnalysis.curClassIndex + 1
+			
+			this.$EventBus.$off('changeClassName')
+			this.$EventBus.$on('changeClassName', val => {
+				this.isAllClasses = this.classList.indexOf(val)
+			})
+        },
+
+        computed: {
+            // 获取最新散点图数据
+            getLevelData() {
+				let curSubjectIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.name).indexOf(this.$store.state.totalAnalysis.currentSubjectJoint)
+				let levelJson = this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].levelKey
+				let transArr = this.$GLOBAL.EXERCISE_LEVELS()
+				levelJson.pointList = levelJson.pointList.map((i,index) => transArr[index])
+				for(let key in levelJson.classpercent){
+					if(!isNaN(key)){
+						let newKey = transArr[+key - 1]
+						levelJson.classpercent[newKey] = levelJson.classpercent[key]
+						levelJson.stupercent[newKey] = levelJson.stupercent[key]
+					}
+				}
+				return levelJson
+            },
+			getAnalysisJson() {
+			    return this.$store.state.totalAnalysis.analysisJsonJoint
+			},
+			getKnowledgeData() {
+				let curSubjectIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.name).indexOf(this.$store.state.totalAnalysis.currentSubjectJoint)
+			    return this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].pointKey
+			},
+        },
+        watch: {
+            getKnowledgeData(val) {
+                if (!val) return
+        		this.doRender(this.getLevelData)
+            }
+        }
+    }
+</script>
+
+<style src="./LevelAnalysis.css" scoped></style>

+ 276 - 0
TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/LevelAnalysis/htScoreDetails.vue

@@ -0,0 +1,276 @@
+<template>
+  <div class="scatter-container">
+    <Row class-name="base-table-row">
+      <Col span="12">
+      <span class="component-title" style="margin-right: 55px">{{$t('totalAnalysis.le_title4')}}</span>
+      <!-- <span class="pie-to-bar" @click="changeRadarOrBar">
+				<Icon :type="isShowRadar ? 'ios-podium' : 'ios-pie'" />
+				{{isShowRadar?$t('totalAnalysis.ka_chart_text1'):$t('totalAnalysis.ka_chart_text2')}}
+			</span> -->
+      <div>
+        <BaseDetailBar echartsId="levelDetailBar" :classIndex="classIndex" @handleItemClick="handleItemClick" ref="levelDetailBar"></BaseDetailBar>
+      </div>
+      <!-- 			<div v-if="isShowRadar">
+				<BaseRadar echartsId="levelRadar" :classIndex="classIndex"></BaseRadar>
+			</div> -->
+      </Col>
+      <Col span="12">
+      <span class="component-title-point"><span>{{$t('totalAnalysis.ka_text3')}}:{{transArr[+currentPoint-1]}}</span></span>
+      <div>
+        <BaseMyTable :columns="detailsColumns" :tableName="$t('totalAnalysis.le_title5') + '(' + pointName + ')'" tableRef="levelScoreTable" :tableDatas="tableData" ref="detailsTable"></BaseMyTable>
+      </div>
+      </Col>
+    </Row>
+    <Divider />
+    <Row class-name="base-table-row">
+      <BaseMyTable :columns="tableColumns" :tableName="$t('totalAnalysis.le_title6')" tableRef="levelWrongTable" :tableDatas="numData" ref="numTable" :tips="$t('totalAnalysis.ka_tip1')"></BaseMyTable>
+    </Row>
+  </div>
+</template>
+
+<script>
+import BaseDetailBar from '@/components/student-analysis/total/htBaseLevelDetail.vue'
+import BaseMyTable from '@/components/student-analysis/total/BaseMyTable.vue'
+import BaseRadar from '@/components/student-analysis/total/BaseRadar.vue'
+export default {
+  props: {
+    classIndex: {
+      type: Number,
+      default: -1
+    }
+  },
+  components: {
+    BaseDetailBar,
+    BaseMyTable,
+    BaseRadar
+  },
+  data() {
+    return {
+      pointName: this.$GLOBAL.EXERCISE_LEVELS()[0],
+      isShowRadar: false,
+      curClassIndex: -1,
+      tableData: [],
+      classDatas: [],
+      classList: [],
+      currentPoint: 1,
+      transArr: this.$GLOBAL.EXERCISE_LEVELS(),
+      tipContent: '* RH:高分区段  /  RL:低分区段 (模拟数据,仅供参考)',
+      levelData: [],
+      numData: [],
+      tableColumns: [{
+        title: this.$t('totalAnalysis.ka_table_text2'),
+        key: 'name',
+        minWidth: 150
+      },
+      {
+        title: this.$t('totalAnalysis.ka_table_text4'),
+        key: 'point',
+        minWidth: 100,
+        sortable: 'custom',
+        renderType: function (h, params) {
+          return h('span', Number(params.row.point).toFixed(2))
+        }
+      },
+      {
+        title: this.$t('totalAnalysis.ka_table_text7'),
+        key: 'itemNO',
+        renderType: 'renderHard',
+        width: 250
+      },
+      {
+        title: this.$t('totalAnalysis.ka_table_text8'),
+        key: 'persent',
+        renderType: function (h, params) {
+          return h('span', (Number(params.row.persent) * 100).toFixed(2) + '%')
+        },
+        sortable: 'custom',
+        minWidth: 100
+      },
+      {
+        title: this.$t('totalAnalysis.ka_table_text9'),
+        key: 'wrong',
+        sortable: 'custom',
+        minWidth: 100
+      },
+      {
+        title: this.$t('totalAnalysis.ka_table_text10'),
+        key: 'rhw',
+        sortable: 'custom',
+        minWidth: 100
+      },
+      {
+        title: this.$t('totalAnalysis.ka_table_text11'),
+        key: 'rlw',
+        sortable: 'custom',
+        minWidth: 100
+      }
+      ],
+      detailsColumns: [{
+        title: this.$t('totalAnalysis.base_name'),
+        key: 'id',
+        minWidth: 100
+      },
+      {
+        title: this.$t('totalAnalysis.base_class'),
+        key: 'className',
+        width: 120
+      },
+      {
+        title: this.$t('totalAnalysis.base_id'),
+        key: 'seatNO',
+        width: 100
+      },
+      {
+        title: this.$t('totalAnalysis.ka_table_text4'),
+        key: 'point',
+        sortable: 'custom',
+        renderType: function (h, params) {
+          return h('span', Number(params.row.point).toFixed(1))
+        },
+        minWidth: 100
+      },
+      {
+        title: this.$t('totalAnalysis.ka_table_text5'),
+        key: 'anwPoint',
+        minWidth: 100,
+        sortable: 'custom',
+        renderType: function (h, params) {
+          return h('span', Number(params.row.anwPoint).toFixed(1))
+        }
+      },
+      {
+        title: this.$t('totalAnalysis.ka_table_text6'),
+        key: 'persent',
+        minWidth: 100,
+        sortable: 'custom',
+        renderType: function (h, params) {
+          return h('span', (Number(params.row.persent) * 100).toFixed(2) + '%')
+        }
+      }
+      ]
+    }
+  },
+  created() {
+
+  },
+
+  methods: {
+
+    changeRadarOrBar() {
+      this.isShowRadar = !this.isShowRadar
+    },
+
+    // 点击柱状图某个点事件
+    handleItemClick(item) {
+      this.pointName = item.name
+      this.currentPoint = this.transArr.indexOf(item.name) + 1
+      this.doRender(this.getLevelData, this.currentPoint, this.curClassIndex)
+    },
+
+    doRender(data, point) {
+      let classIndex = this.$store.state.totalAnalysis.curClassIndex
+      let origin = data.stupercent
+      let keys = origin.keys
+      let datas = classIndex === -1 ? (origin[point] || []) : origin[point].filter(i => i[1] === this.classList[classIndex + 1])
+      this.tableData = this.$tools.jsonTransform({
+        datas: datas,
+        keys: keys
+      })
+    },
+
+    doRenderWrong(data) {
+      let classIndex = this.$store.state.totalAnalysis.curClassIndex
+      let origin = data.wrong
+      let keys = origin.keys
+      let datas = origin.datas
+      console.log(datas)
+      datas.forEach((i, index) => {
+        i[0] = this.transArr[index]
+      })
+      let allWrongData = this.$tools.jsonTransform({ datas: datas, keys: keys })
+      this.numData = classIndex === -1 ? allWrongData : this.getClassWrongData(allWrongData, classIndex)
+
+    },
+
+    getClassWrongData(allWrongData) {
+      let classIndex = this.$store.state.totalAnalysis.curClassIndex
+      let curSubjectIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.name).indexOf(this.$store.state.totalAnalysis.currentSubjectJoint)
+      let curItem = this.getAnalysisJson.classes[classIndex].subjects[curSubjectIndex]
+      allWrongData.forEach((item, index) => {
+        item.persent = curItem.frate[index]
+        item.rhw = curItem.fphc[index]
+        item.rlw = curItem.fplc[index]
+        item.wrong = curItem.fpc[index]
+      })
+      return allWrongData
+    }
+
+  },
+  mounted() {
+    this.$refs.detailsTable.$el.childNodes[1].style.borderRight = '0'
+    this.$refs.numTable.$el.childNodes[1].style.borderRight = '0'
+    if (this.getLevelData) {
+      this.doRender(this.getLevelData, this.currentPoint, this.curClassIndex)
+      this.doRenderWrong(this.getLevelData, this.curClassIndex)
+      this.classList = [this.$t('totalAnalysis.allClasses')].concat([...new Set(this.getAnalysisJson.classes.map(item => item.className))]) // 获取班级列表
+    }
+  },
+  computed: {
+    // 获取最新散点图数据
+    getLevelData() {
+      let curSubjectIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.name).indexOf(this
+        .$store.state
+        .totalAnalysis.currentSubjectJoint)
+      let levelJson = this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].levelKey
+      let transArr = this.$GLOBAL.EXERCISE_LEVELS()
+      levelJson.pointList = levelJson.pointList.map((i, index) => transArr[index])
+      for (let key in levelJson.classpercent) {
+        if (!isNaN(key)) {
+          let newKey = transArr[+key - 1]
+          levelJson.classpercent[newKey] = levelJson.classpercent[key]
+          levelJson.stupercent[newKey] = levelJson.stupercent[key]
+        }
+      }
+      return levelJson
+    },
+    // 获取最新散点图数据
+    getKnowledgeData() {
+      let curSubjectIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.name).indexOf(this
+        .$store.state
+        .totalAnalysis.currentSubjectJoint)
+      return this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].pointKey
+    },
+    getAnalysisJson() {
+      return this.$store.state.totalAnalysis.analysisJsonJoint
+    },
+  },
+  watch: {
+    // getLevelData: {
+    // 	handler(val) {
+    // 		if (val) {
+    // 			this.doRender(this.getLevelData, this.getLevelData.pointList[0],this.curClassIndex)
+    // 			this.doRenderWrong(this.getLevelData,this.curClassIndex)
+    // 		}
+    // 	}
+    // },
+    classIndex(n, o) {
+      this.curClassIndex = n - 1
+      this.doRender(this.getLevelData, this.getLevelData.pointList[0], n - 1)
+      this.doRenderWrong(this.getLevelData, n - 1)
+    },
+    immediate: true
+  }
+}
+</script>
+
+<style src="./LevelAnalysis.css" scoped></style>
+<style>
+.component-title-point {
+  position: absolute;
+  font-size: 14px;
+  font-weight: 600;
+  right: 220px;
+  top: 60px;
+  color: #70b1e7;
+}
+</style>

+ 492 - 0
TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/ScatterAnalysis/htScatterAnalysis.vue

@@ -0,0 +1,492 @@
+<template>
+  <div class="scatter-container">
+    <Row class-name="base-table-row">
+      <Col span="12">
+      <div class="component-title">
+        <span>{{ $t('totalAnalysis.sca_title1') }}</span>
+        <!--                <Select v-model="currentClass" @on-change="onClassSelect">
+                    <Option v-for="(item,index) in classList" :value="index" :key="item">{{ item }}</Option>
+                </Select> -->
+      </div>
+      <BaseScatter :scatterData="tableData"></BaseScatter>
+      </Col>
+      <Col span="12">
+      <div class="scatter-statistics">
+        <div class="scatter-table-line">
+          <span>{{ $t('totalAnalysis.sca_text1') }}</span>
+          <span style="text-align: left;width: 250px;">{{ $t('totalAnalysis.sca_text2') }}</span>
+          <span>{{ $t('totalAnalysis.sca_text3') }}</span>
+        </div>
+        <Divider />
+        <div class="scatter-table-line">
+          <span>A</span>
+          <span style="text-align: left;width: 250px;">{{ $t('totalAnalysis.sca_text5') }}</span>
+          <span>{{ tableData.filter(item => item.scatter === 'A').length }}</span>
+        </div>
+        <Divider />
+        <div class="scatter-table-line">
+          <span>A'</span>
+          <span style="text-align: left;width: 250px;">{{ $t('totalAnalysis.sca_text6') }}</span>
+          <span>{{ tableData.filter(item => item.scatter === "A'").length }}</span>
+        </div>
+        <Divider />
+        <div class="scatter-table-line">
+          <span>B</span>
+          <span style="text-align: left;width: 250px;">{{ $t('totalAnalysis.sca_text7') }}</span>
+          <span>{{ tableData.filter(item => item.scatter === 'B').length }}</span>
+        </div>
+        <Divider />
+        <div class="scatter-table-line">
+          <span>B'</span>
+          <span style="text-align: left;width: 250px;">{{ $t('totalAnalysis.sca_text8') }}</span>
+          <span>{{ tableData.filter(item => item.scatter === "B'").length }}</span>
+        </div>
+        <Divider />
+        <div class="scatter-table-line">
+          <span>C</span>
+          <span style="text-align: left;width: 250px;">{{ $t('totalAnalysis.sca_text9') }}</span>
+          <span>{{ tableData.filter(item => item.scatter === 'C').length }}</span>
+        </div>
+        <Divider />
+        <div class="scatter-table-line">
+          <span>C'</span>
+          <span style="text-align: left;width: 250px;">{{ $t('totalAnalysis.sca_text10') }}</span>
+          <span>{{ tableData.filter(item => item.scatter === "C'").length }}</span>
+        </div>
+      </div>
+      </Col>
+    </Row>
+
+    <!-- 学生稳定度统计表 -->
+    <Row class-name="base-table-row">
+      <div>
+        <BaseTable @onStuClick="onStuClick" :columns="tableColumns" :tableDatas="tableData"
+          :tableName="$t('totalAnalysis.sca_title2')" tableRef="scatterTable" ref="scatterTable"></BaseTable>
+      </div>
+    </Row>
+
+    <!-- 学生报告 -->
+    <Modal v-model="stuReportStatus" class-name="custom-modal-top print-me" :title="$t('learnActivity.score.stuRpt')"
+      :width="1000" footer-hide>
+      <div slot="header" style="position:relative;padding:60px 0px 20px 0px;" id="printDom">
+        <img src="./cloudas_title.png" alt="" style="position:absolute;top:-45px;left:0;width:100%">
+        <span class="stu-info">
+          <span class="stu-label">
+            {{ $t('evaluation.index.paper') }}:
+          </span>
+          <span class="stu-value">{{ examInfo.name }}</span>
+        </span>
+        <span class="stu-info">
+          <span class="stu-label">
+            {{ $t('learnActivity.score.subjectLabel') }}
+          </span>
+          <span class="stu-value">{{ subjectName }}</span>
+        </span>
+        <span class="stu-info">
+          <span class="stu-label">
+            {{ $t('totalAnalysis.echarts_text11') }}:
+          </span>
+          <span class="stu-value">{{ $tools.formatTime(examInfo.startTime, 'yyyy-MM-dd') }}</span>
+        </span>
+        <span class="stu-info">
+          <span class="stu-label">
+            {{ $t('totalAnalysis.text7') }}:
+          </span>
+          <span class="stu-value">{{ examInfo.stuCount }}</span>
+        </span>
+        <br>
+        <br>
+        <span class="stu-info">
+          <span class="stu-label">
+            {{ $t('learnActivity.score.classLabel') }}
+          </span>
+          <span class="stu-value">{{ className }}</span>
+        </span>
+        <span class="stu-info">
+          <span class="stu-label">
+            {{ $t('learnActivity.score.stuLabel') }}
+          </span>
+          <span class="stu-value">{{ viewStuData.name }}</span>
+        </span>
+        <span class="stu-info">
+          <span class="stu-label">
+            {{ $t('totalAnalysis.base_id') }}:
+          </span>
+          <span class="stu-value">{{ viewStuData.id }}</span>
+        </span>
+        <span style="float:right;margin: -5px 40px" class="print-hidden">
+          <Button type="primary" @click="onPrint">{{ $t('learnActivity.score.print') }}</Button>
+        </span>
+      </div>
+      <StuReport :stuData="viewStuData" :classId="chooseClass" :examInfo="examInfo" :subject="chooseSubject">
+        <div style="display: flex;justify-content: space-between;margin-top: 3%;">
+          <Card style="width: 49%;">
+            <BaseSingleStuScatter :scatterData="tableData" :stuData="viewStuData"></BaseSingleStuScatter>
+          </Card>
+          <Card style="width: 49%;padding-top: 60px;padding-left: 30px;">
+            <p style="margin-bottom:20px;">{{ $t('totalAnalysis.sca_table_text6') }}<br><span
+                style="font-size: 20px;margin-top: 10px;display: inline-block;color: #ff5508;">{{ viewStuData.scatter
+                }}</span></p>
+            <p style="margin-bottom:20px;">{{ $t('totalAnalysis.sca_text2') }}<br><span
+                style="font-size: 20px;margin-top: 10px;display: inline-block;color: #ff5508;">{{
+                  getScatterTip(viewStuData.scatter) }}</span></p>
+            <p style="margin-bottom:20px;">{{ $t('totalAnalysis.sca_table_text4') }}<br><span
+                style="font-size: 20px;margin-top: 10px;display: inline-block;color: #ff5508;">{{ viewStuData.carefulList
+                }}</span></p>
+            <p style="margin-bottom:20px;">{{ $t('totalAnalysis.sca_table_text3') }}<br><span
+                style="font-size: 20px;margin-top: 10px;display: inline-block;color: #ff5508;">{{ viewStuData.hardList
+                }}</span></p>
+          </Card>
+        </div>
+
+      </StuReport>
+    </Modal>
+  </div>
+</template>
+
+<script>
+import JsPDF from 'jspdf'
+import domtoimage from '@/utils/dom_to_image';
+import BaseScatter from '@/components/student-analysis/total/htBaseScatter.vue'
+import BaseSingleStuScatter from '@/components/student-analysis/total/htBaseSingleStuScatter.vue'
+import BaseTable from '@/components/student-analysis/total/htBaseMyTable.vue'
+// import StuReport from "./StuReport.vue"
+import StuReport from "@/view/learnactivity/StuReport.vue"
+export default {
+  components: {
+    BaseScatter, BaseTable, StuReport, BaseSingleStuScatter
+  },
+  data() {
+    return {
+      chooseClass: '',
+      chooseSubject: '',
+      examInfo: null,
+      stuReportStatus: false,
+      subjectName: '',
+      className: '',
+      stuName: '',
+      viewStuData: {
+        name: ''
+      },
+      tableData: [],
+      originTableData: [],
+      currentClass: 0,
+      classList: [],
+      tableColumns: [
+        {
+          title: this.$t('totalAnalysis.base_name'),
+          key: 'name',
+          minWidth: 110,
+          renderType: 'renderStuName'
+        },
+        {
+          title: this.$t('totalAnalysis.base_class'),
+          key: 'className',
+          minWidth: 60
+        },
+        {
+          title: this.$t('totalAnalysis.base_id'),
+          key: 'no',
+          sortable: 'custom',
+          minWidth: 50,
+          renderType: function (h, params) {
+            return h('span', params.row.no || '-')
+          }
+        },
+        {
+          title: this.$t('totalAnalysis.sca_chart_text1'),
+          key: 'sRate',
+          sortable: 'custom',
+          minWidth: 50,
+          renderType: function (h, params) {
+            return h('span', (Number(params.row.sRate)).toFixed(2) + '%')
+          },
+        },
+        {
+          title: this.$t('totalAnalysis.sca_table_text6'),
+          key: 'scatter',
+          minWidth: 60,
+          filters: [
+            {
+              label: "A",
+              value: "A"
+            },
+            {
+              label: "A'",
+              value: "A'"
+            }, {
+              label: "B",
+              value: "B"
+            },
+            {
+              label: "B'",
+              value: "B'"
+            }, {
+              label: "C",
+              value: "C"
+            },
+            {
+              label: "C'",
+              value: "C'"
+            }
+          ],
+          filterMultiple: false,
+          filterRemote(value, row) {
+          }
+        },
+        {
+          title: this.$t('totalAnalysis.sca_table_text5'),
+          sortable: true,
+          key: 'x',
+          renderType: function (h, params) {
+            return h('span', Number(params.row.x).toFixed(2))
+          },
+          minWidth: 60
+        },
+        {
+          title: this.$t('totalAnalysis.sca_table_text1'),
+          key: 'trueNum',
+          sortable: 'custom',
+          minWidth: 60
+        },
+        {
+          title: this.$t('totalAnalysis.sca_table_text2'),
+          key: 'falseNum',
+          sortable: 'custom',
+          minWidth: 60
+        },
+        {
+          title: this.$t('totalAnalysis.sca_table_text3'),
+          minWidth: 120,
+          key: 'hardList',
+          renderType: 'renderHard'
+        },
+        {
+          title: this.$t('totalAnalysis.sca_table_text4'),
+          minWidth: 120,
+          key: 'carefulList',
+          renderType: 'renderCareful'
+        },
+
+      ]
+    }
+  },
+  created() {
+    this.$EventBus.$off('onExport')
+    this.$EventBus.$on("onExport", exportTables => {
+      console.log('收到')
+      console.log(exportTables)
+      if (exportTables[0] === 'scatterTable') {
+        this.$refs.scatterTable.exportData(3)
+      }
+    })
+    this.$parent.$parent.$parent.isShowQuestions = false
+    this.examInfo = JSON.parse(localStorage.getItem('curExam'))
+  },
+
+  methods: {
+    getScatterTip(scatter) {
+      let arr = ["A", "A'", "B", "B'", "C", "C'"]
+      return this.$t('totalAnalysis.sca_text' + (arr.indexOf(scatter) + 5))
+    },
+    onPrint() {
+      // 获取需要打印预览的DOM节点
+      let modalDom = document.getElementsByClassName('print-me')[0].getElementsByClassName('ivu-modal-content')[0]
+      Array.from(document.getElementsByClassName('print-hidden')).forEach(dom => { dom.style.opacity = 0 })
+      Array.from(document.getElementsByClassName('ivu-icon-ios-close')).forEach(dom => { dom.style.opacity = 0 })
+      Array.from(document.getElementsByClassName('toggle-info')).forEach(dom => { dom.style.opacity = 0 })
+      domtoimage.toJpeg(modalDom, {
+        bgcolor: '#fff'
+      }).then(pageData => {
+        let that = this
+        console.log(pageData)
+        let img = new Image();
+        img.src = pageData;
+        img.onload = function () {
+          let contentWidth = img.width
+          let contentHeight = img.height
+          // 创建 canvas 并在其上绘制图片
+          const canvas = document.createElement('canvas');
+          canvas.width = img.naturalWidth;
+          canvas.height = img.naturalHeight;
+          let pageHeight = contentWidth / 592.28 * 841.89
+          let leftHeight = contentHeight
+          let position = 0
+          const a4Height = 297
+          const a4Width = 210
+          let imgWidth = a4Width
+          let imgHeight = a4Width / contentWidth * contentHeight
+          const ctx = canvas.getContext('2d');
+          ctx.drawImage(img, 0, 0);
+          // 创建一个新的 PDF 文档
+          const PDF = new JsPDF({
+            orientation: 'p',
+            unit: 'mm',
+            format: 'a4',
+            putOnlyUsedFonts: true
+          })
+          let curPage = 1
+          PDF.addImage("./cloudas_title.png", "JPEG", 15, 40, 180, 180);
+          // 如果是一页的情况
+          if (leftHeight <= pageHeight) {
+            PDF.addImage(pageData, 'JPEG', 0, 0, imgWidth,
+              imgHeight)
+          } else {
+            // 如果超出则多页
+            while (leftHeight > 0) {
+              PDF.addImage(pageData, 'JPEG', 0, position,
+                imgWidth, imgHeight)
+              leftHeight -= pageHeight
+              position -= a4Height
+              if (leftHeight > 0) {
+                PDF.addPage()
+              }
+              curPage++
+            }
+          }
+          // 下载 PDF
+          PDF.save(that.examInfo.name + '-' + that.viewStuData.name + '-' + that.$t('studentWeb.exam.report.title') + '.pdf');
+          Array.from(document.getElementsByClassName('print-hidden')).forEach(dom => { dom.style.opacity = 1 })
+          Array.from(document.getElementsByClassName('ivu-icon-ios-close')).forEach(dom => { dom.style.opacity = 1 })
+          Array.from(document.getElementsByClassName('toggle-info')).forEach(dom => { dom.style.opacity = 1 })
+        }
+      })
+    },
+    onStuClick(stu) {
+      if (!this.examInfo.papers[0].item) {
+        let newPapers = []
+        this.examInfo.papers.forEach(async paper => {
+          let fullPaper = await this.$evTools.getFullPaper(paper)
+          fullPaper.blob = paper.blob
+          newPapers.push(fullPaper)
+        })
+        this.examInfo.papers = newPapers
+      }
+      console.log('点击的学生', stu)
+      this.stuName = stu.name
+      this.subjectName = localStorage.getItem('cur_s')
+      this.className = stu.className
+      this.chooseClass = stu.classId
+      this.chooseSubject = this.examInfo.subjects.find(s => s.name === this.subjectName).id
+      this.viewStuData = stu
+      this.stuReportStatus = true
+    },
+
+    onClassSelect(val) {
+      if (val === 0) {
+        this.tableData = this.originTableData
+      } else {
+        this.tableData = this.originTableData.filter(item => item.className === this.classList[val])
+      }
+      console.log('落点图', this.tableData.map(i => i.name))
+    },
+
+    // 调整图表所需数据结构格式
+    renderData(analysisJson) {
+      let curSubject = this.$store.state.totalAnalysis.currentSubjectJoint
+      let result = []
+      analysisJson.subjects.forEach((subject, subjectIndex) => {
+        if (subject.name === curSubject) {
+          result.push({
+            subject: subject.name,
+            students: this.getSubjectStudents(analysisJson, subject)
+          })
+        }
+      })
+      return result[0].students
+    },
+
+    /* 获取每个学生每个科目下的落点数据 */
+    getSubjectStudents(analysisJson, subject) {
+      let result = []
+      analysisJson.students.forEach(stu => {
+        let obj = {}
+
+        let  stuSubjectItem = stu.subjects.find(i => i.id === subject.id)
+        if(stuSubjectItem){
+           analysisJson.scatterKey.forEach((key, index) => {
+            stuSubjectItem.scatter[0] = stu.name
+             obj[key] = stuSubjectItem.scatter[index]
+             obj.no = stu.no
+             obj.id = stu.id
+             obj.classId = stu.classId
+             obj.sRate = Number(stuSubjectItem.sRate)
+           })
+           result.push(obj)
+        }
+      })
+      return result
+    },
+  },
+
+  mounted() {
+    this.$refs.scatterTable.$el.childNodes[1].style.borderRight = '0'
+    if (this.getAnalysisJson) {
+      this.tableData = this.renderData(this.getAnalysisJson)
+      this.originTableData = JSON.parse(JSON.stringify(this.tableData))
+      this.classList = [this.$t('totalAnalysis.allClasses')].concat([...new Set(this.getAnalysisJson.classes.map(item => item.className))]) // 获取班级列表
+    }
+
+    this.$EventBus.$off('onSubjectChange')
+    this.$EventBus.$on('onSubjectChange', val => {
+      console.error(val)
+      this.tableData = this.renderData(this.getAnalysisJson)
+      this.originTableData = JSON.parse(JSON.stringify(this.tableData))
+      this.currentClass = 0
+    })
+
+    this.onClassSelect(this.$store.state.totalAnalysis.curClassIndex + 1)
+
+    this.$EventBus.$off('changeClassName')
+    this.$EventBus.$on('changeClassName', val => {
+      console.log('落点选择的班级', val)
+      console.log(this.classList)
+      this.onClassSelect(this.classList.indexOf(val))
+    })
+  },
+
+  computed: {
+    // 获取最新散点图数据
+    getAnalysisJson() {
+      return this.$store.state.totalAnalysis.analysisJsonJoint
+    }
+  },
+  watch: {
+    getAnalysisJson: {
+      deep: true,
+      handler(val) {
+        if (val) {          
+          this.tableData = this.renderData(JSON.parse(JSON.stringify(val)))
+          this.originTableData = JSON.parse(JSON.stringify(this.tableData))
+          this.classList = [this.$t('totalAnalysis.allClasses')].concat([...new Set(val.classes.map(item => item.className))])
+        }
+      }
+
+    }
+  }
+}
+</script>
+
+<style src="./ScatterAnalysis.css" scoped></style>
+<style lang="less">
+.scatter-container .ivu-select {
+  margin-left: 10px;
+  margin-bottom: 5px;
+  width: 120px;
+}
+
+.custom-modal-top {
+  .stu-info {
+    margin-right: 30px;
+    font-size: 16px;
+  }
+
+  .stu-value {
+    font-weight: 600;
+  }
+
+  .stu-label {
+    color: #808695;
+  }
+}
+</style>

+ 410 - 0
TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/TestAnalysis/htQuestionList.vue

@@ -0,0 +1,410 @@
+<template>
+  <div class="question-list-container" ref="questionContainer">
+    <!-- 左侧题目列表清单 -->
+    <div class="ql-left-box">
+      <Loading :top="300" v-show="dataLoading" type="3"></Loading>
+      <ExerciseList :propsList="questionList" :flatIds="flatList.length ? flatList.map(i => i.id) : []"
+        :examScope="examScope" :analysisJson="paperAnalysisJson" :optionRate="paperOptionRateArr" isAnalysis
+        @pageScroll="doScroll" ref="exList" isShowAnalysis></ExerciseList>
+    </div>
+
+    <!-- 右侧题目列表题型概览 -->
+    <div class='ql-right-box' ref="rightBox" :style="{ width: isOpen ? '17%' : '18%' }">
+      <Button type="info" @click="handleBackTo"
+        style="width: 100%;height: 40px;margin-bottom: 15px;border-radius: 2px;">{{ $t('totalAnalysis.backUp') }}</Button>
+      <Button type="info" @click="isShowAnalysis = true"
+        style="width: 100%;height: 40px;margin-bottom: 15px;border-radius: 2px;background-color: #29a482;border: none;">{{
+          $t('evaluation.paperList.paperAnalysis') }}</Button>
+      <Button type="info" @click="onPrintPaper"
+        style="width: 100%;height: 40px;margin-bottom: 15px;border-radius: 2px;background-color: #419901;border: none;">{{
+          $t('evaluation.paperList.downloadPaperPDF') }}</Button>
+      <div style="background-color: #fff;">
+        <div class="ql-right-score">
+          <p style="margin-top: 20px;">{{ $t('totalAnalysis.paperSubject') }}:{{ $store.state.totalAnalysis.currentSubject
+          }}</p>
+          <p style="margin-top: 10px;">{{ $t('totalAnalysis.paperItemsCount') }}:{{ questionList.length }}</p>
+          <p style="margin-top: 10px;">{{ $t('totalAnalysis.ql_text1') }}
+            :<span>{{ sumArr(questionList.map(item => item.score)) }} {{ $t('totalAnalysis.ql_text8') }}</span>
+          </p>
+        </div>
+
+        <div class="ql-right-list">
+          <div>
+            <div class="ql-right-part" v-if="SingleList.length">
+              <span class="ql-right-part-title"><span class="ql-line"></span>{{ $t('totalAnalysis.ql_text2') }}({{
+                sumArr(SingleList.map(item =>
+                  item.score)) }}{{ $t('totalAnalysis.ql_text8') }})</span>
+              <div class="ql-right-items">
+                <span class="ql-right-item" v-for="(item, index) in SingleList" :key="index"
+                  @click="handleItemClick(item, $event)" :ref="'indexRef' + flatList.indexOf(item)"
+                  :data-order="flatList.indexOf(item)">{{ getIndexOrder(item) }}</span>
+              </div>
+            </div>
+            <div class="ql-right-part" v-if="MultipleList.length">
+              <span class="ql-right-part-title"><span class="ql-line"></span>{{ $t('totalAnalysis.ql_text3') }}({{
+                sumArr(MultipleList.map(item =>
+                  item.score)) }}{{ $t('totalAnalysis.ql_text8') }})</span>
+              <div class="ql-right-items">
+                <span class="ql-right-item" v-for="(item, index) in MultipleList" :key="index"
+                  @click="handleItemClick(item, $event)" :ref="'indexRef' + flatList.indexOf(item)"
+                  :data-order="flatList.indexOf(item)">{{ getIndexOrder(item) }}</span>
+              </div>
+            </div>
+            <div class="ql-right-part" v-if="JudgeList.length">
+              <span class="ql-right-part-title"><span class="ql-line"></span>{{ $t('totalAnalysis.ql_text4') }}({{
+                sumArr(JudgeList.map(item =>
+                  item.score)) }}{{ $t('totalAnalysis.ql_text8') }})</span>
+              <div class="ql-right-items">
+                <span class="ql-right-item" v-for="(item, index) in JudgeList" :key="index"
+                  @click="handleItemClick(item, $event)" :ref="'indexRef' + flatList.indexOf(item)"
+                  :data-order="flatList.indexOf(item)">{{ getIndexOrder(item) }}</span>
+              </div>
+            </div>
+            <div class="ql-right-part" v-if="CompleteList.length">
+              <span class="ql-right-part-title"><span class="ql-line"></span>{{ $t('totalAnalysis.ql_text5') }}({{
+                sumArr(CompleteList.map(item =>
+                  item.score)) }}{{ $t('totalAnalysis.ql_text8') }})</span>
+              <div class="ql-right-items">
+                <span class="ql-right-item" v-for="(item, index) in CompleteList" :key="index"
+                  @click="handleItemClick(item, $event)" :ref="'indexRef' + flatList.indexOf(item)"
+                  :data-order="flatList.indexOf(item)">{{ getIndexOrder(item) }}</span>
+              </div>
+            </div>
+            <div class="ql-right-part" v-if="SubjectiveList.length">
+              <span class="ql-right-part-title"><span class="ql-line"></span>{{ $t('totalAnalysis.ql_text6') }}({{
+                sumArr(SubjectiveList.map(item =>
+                  item.score)) }}{{ $t('totalAnalysis.ql_text8') }})</span>
+              <div class="ql-right-items">
+                <span class="ql-right-item" v-for="(item, index) in SubjectiveList" :key="index"
+                  @click="handleItemClick(item, $event)" :ref="'indexRef' + flatList.indexOf(item)"
+                  :data-order="flatList.indexOf(item)">{{ getIndexOrder(item) }}</span>
+              </div>
+            </div>
+            <div class="ql-right-part" v-if="ConnectorList.length">
+              <span class="ql-right-part-title"><span class="ql-line"></span>{{ $t('totalAnalysis.ql_text14') }}({{
+                sumArr(ConnectorList.map(item =>
+                  item.score)) }}{{ $t('totalAnalysis.ql_text8') }})</span>
+              <div class="ql-right-items">
+                <span class="ql-right-item" v-for="(item, index) in ConnectorList" :key="index"
+                  @click="handleItemClick(item, $event)" :ref="'indexRef' + flatList.indexOf(item)"
+                  :data-order="flatList.indexOf(item)">{{ getIndexOrder(item) }}</span>
+              </div>
+            </div>
+            <div class="ql-right-part" v-if="CorrectList.length">
+              <span class="ql-right-part-title"><span class="ql-line"></span>{{ $t('totalAnalysis.ql_text15') }}({{
+                sumArr(CorrectList.map(item =>
+                  item.score)) }}{{ $t('totalAnalysis.ql_text8') }})</span>
+              <div class="ql-right-items">
+                <span class="ql-right-item" v-for="(item, index) in CorrectList" :key="index"
+                  @click="handleItemClick(item, $event)" :ref="'indexRef' + flatList.indexOf(item)"
+                  :data-order="flatList.indexOf(item)">{{ getIndexOrder(item) }}</span>
+              </div>
+            </div>
+            <div class="ql-right-part" v-if="ComposeList.length">
+              <span class="ql-right-part-title"><span class="ql-line"></span>{{ $t('totalAnalysis.ql_text7') }}({{
+                sumArr(ComposeList.map(item =>
+                  item.score)) }}{{ $t('totalAnalysis.ql_text8') }})</span>
+              <div class="ql-right-items">
+                <!-- 如果是综合题 则需要把小题题序放出来 -->
+                <span v-for="(item, index) in ComposeList" :key="index" style="background-color: none;"
+                  class="ql-right-items">
+                  <span class="ql-right-item" v-for="(child, childIndex) in item.children" :key="childIndex"
+                    @click="handleItemClick(item, $event)" :ref="'indexRef' + (flatList.indexOf(child))"
+                    :data-order="flatList.indexOf(child)">{{ fillIndexOrder(flatList.indexOf(child)) }}
+                    ({{ getIndexOrder(item) }} - {{ childIndex + 1 }})</span>
+                </span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+      <Modal v-model="isShowAnalysis" width="1100" footer-hide class="export-modal analysis-modal">
+        <ExamPaperAnalysis :testPaper="paperInfo" v-if="isShowAnalysis"></ExamPaperAnalysis>
+      </Modal>
+    </div>
+  </div>
+</template>
+
+<script>
+
+import ExerciseList from '@/components/evaluation/ExerciseList.vue'
+import ExamPaperAnalysis from '@/view/learnactivity/ExamPaperAnalysis.vue'
+export default {
+  components: {
+    ExamPaperAnalysis,
+    ExerciseList
+  },
+  data() {
+    return {
+      paperInfo: null,
+      examScope: null,
+      dataLoading: false,
+      isOpen: true,
+      activeCollapseIndex: [],
+      isFixed: false,
+      isLoadingEcharts: false,
+      isShowAnswerExplain: false,
+      isShowAnalysis: false,
+      tableData: [],
+      optionsData: [],
+      collapseList: [],
+      questionList: [],
+      flatList: [],
+      SingleList: [],
+      MultipleList: [],
+      JudgeList: [],
+      CompleteList: [],
+      SubjectiveList: [],
+      ConnectorList: [],
+      CorrectList: [],
+      ComposeList: [],
+      diffColors: ['#32CF74', '#E8BE15', '#F19300', '#EB5E00', '#D30000'],
+      scrollTop: 0,
+      fromRoutePath: null,
+      paperAnalysisJson: [],
+      paperOptionRateArr: []
+    }
+  },
+  created() {
+    let that = this
+    let parentVm = this.$parent.$parent.$parent
+    parentVm.isShowQuestions = true
+
+    let curExam = JSON.parse(localStorage.getItem('curExam'))
+    let curSubjectIndex = curExam.subjects.map(i => i.name).indexOf(this.$store.state.totalAnalysis.currentSubject)
+    console.log(curSubjectIndex)
+    console.log('当前的评测数据', curExam)
+    this.examScope = curExam.scope
+    let curPaper = curExam.papers[curSubjectIndex]
+    this.initFullPaper(curPaper, curExam)
+  },
+
+  methods: {
+    onPrintPaper(){
+      this.$refs.exList.onPrintPaper()
+    },
+    async initFullPaper(examPaper, exam) {
+      examPaper.examId = exam.code.replace('Exam-', '')
+      let fullPaperJson = await this.$evTools.getFullPaper(examPaper, exam.scope)
+      this.questionList = fullPaperJson.item
+      this.SingleList = this.questionList.filter(item => item.type === 'single')
+      this.MultipleList = this.questionList.filter(item => item.type === 'multiple')
+      this.JudgeList = this.questionList.filter(item => item.type === 'judge')
+      this.CompleteList = this.questionList.filter(item => item.type === 'complete')
+      this.SubjectiveList = this.questionList.filter(item => item.type === 'subjective')
+      this.ConnectorList = this.questionList.filter(item => item.type === 'connector')
+      this.CorrectList = this.questionList.filter(item => item.type === 'correct')
+      this.ComposeList = this.questionList.filter(item => item.type === 'compose')
+      this.dataLoading = false
+      console.log('当前的评测试卷数据', fullPaperJson)
+      this.paperInfo = fullPaperJson
+
+      // 如果是试题页面过来带有题序 则获取指定题目并进行滚动
+      let qIndex = this.$route.query.QIndex
+      if (qIndex) {
+        let paperItems = fullPaperJson.item
+        let allItems = []
+        paperItems.forEach(i => {
+          allItems = allItems.concat(i.type === 'compose' ? i.children : [i])
+        })
+        this.flatList = allItems
+        console.log('拉平后的题目', allItems)
+        // 获取当前试题的分析数据
+        this.paperAnalysisJson = this.getExerciseList()
+        this.$nextTick(() => {
+          console.log(this.$refs['indexRef' + (qIndex - 1)][0])
+          setTimeout(() => {
+            this.$refs['indexRef' + (qIndex - 1)][0]
+              .click() // 根据路由携带的题序 来触发对应题序的点击事件 完成滚动
+          }, 1000)
+        })
+      } else {
+        this.$nextTick(() => {
+          this.$refs.indexRef0[0].click()
+        })
+      }
+    },
+
+    /* 页面滚动逻辑 */
+    doScroll(scrollDistance) {
+      this.$nextTick(() => {
+        let parentVm = this.$parent.$parent.$parent
+        parentVm.$refs['vs'].scrollTo({
+          y: scrollDistance,
+        },
+          500,
+          "easeInQuad"
+        );
+      })
+    },
+
+    // 指定容器滚动到指定位置
+    scrollToTop(element, to, duration) {
+      if (duration <= 0) return
+      const diff = to - element.scrollTop
+      const perTick = diff / duration * 10
+      this.timer = setTimeout(() => {
+        element.scrollTop += perTick
+        if (element.scrollTop === to) return
+        this.scrollToTop(element, to, duration - 10)
+      }, 10)
+    },
+
+    // 点击右边题序 获取到题目DOM 进行滚动操作
+    handleItemClick(item, e) {
+      this.$nextTick(() => {
+        let parentVm = this.$parent.$parent.$parent
+        let currentSpan = e.target || e
+        let allList = document.getElementsByClassName('ql-right-item')
+        let questionList = this.$refs.exList.$el.getElementsByClassName('cp-exercise-item')
+        let itemIndex = this.questionList.indexOf(item)
+        let questionDom = questionList[itemIndex]
+        questionDom.style.border = '2px solid #2db7f5'
+        setTimeout(() => {
+          questionDom.style.border = '2px solid transparent'
+        }, 2000)
+        this.doScroll(questionDom.offsetTop)
+        // 伪数组处理统一背景颜色
+        Array.prototype.slice.call(allList).forEach(item => {
+          item.style.background = '#2db7f5'
+        })
+        // 将当前选中项修改选中色
+        currentSpan.style.background = '#139c51'
+        this.$refs.exList.collapseList = [itemIndex]
+      })
+
+    },
+
+    // 点击返回
+    handleBackTo() {
+      let isClouDAS = this.$route.name === 'privExam'
+      if (isClouDAS) {
+        this.$EventBus.$emit('cloudas-close-question')
+      } else {
+        this.$parent.$parent.$parent.isShowQuestions = false
+        this.$router.back(-1)
+      }
+
+    },
+
+    // 返回题目区域总分 字符串换算
+    sumArr(arr) {
+      if (arr.length) {
+        return arr.reduce((a, b) => a + b)
+      } else {
+        return 0
+      }
+
+    },
+
+    /* 获取所有试题的对应分析数据 */
+    getExerciseList() {
+      let analysisJson = JSON.parse(JSON.stringify(this.getAnalysisJson))
+      let curSubjectIndex = analysisJson.subjects.map(i => i.name).indexOf(this.$store.state.totalAnalysis
+        .currentSubject)
+      let result = []
+      analysisJson.paper[curSubjectIndex].value.forEach((exercise, exerciseIndex) => {
+        let obj = {}
+        analysisJson.paperKey.forEach((key, index) => {
+          obj[key] = exercise[index]
+        })
+
+        analysisJson.classes.forEach(classItem => {
+          obj[classItem.className] = classItem.subjects[curSubjectIndex].item[exerciseIndex]
+        })
+        result.push(obj)
+      })
+
+      let curSubject = analysisJson.subjects[curSubjectIndex]
+      curSubject.record.forEach((i, itemIndex) => {
+        this.paperOptionRateArr.push({
+          record: curSubject.record.length ? curSubject.record[itemIndex] : {},
+          ph: curSubject.phc.length ? curSubject.phc[itemIndex] : {},
+          pl: curSubject.plc.length ? curSubject.plc[itemIndex] : {}
+        })
+      })
+
+      return result
+    },
+  },
+
+  mounted() {
+    this.$EventBus.$off('onCollapseChange')
+    this.$EventBus.$on('onCollapseChange', val => {
+      // 如果侧边栏展开的时候
+      this.isOpen = !val
+    })
+
+
+
+
+
+  },
+
+  computed: {
+    // 获取最新滚动数据
+    getScrollTop() {
+      let top = this.$store.state.totalAnalysis.scrollTop
+      return top
+    },
+    // 获取最新试题数据
+    getAnalysisJson() {
+      return this.$store.state.totalAnalysis.analysisJsonJoint
+    },
+    // 替换题序
+    getIndexOrder() {
+      return item => {
+        return this.questionList.indexOf(item) + 1 > 9 ? this.questionList.indexOf(item) + 1 : '0' + (this
+          .questionList.indexOf(item) + 1)
+      }
+    },
+    // 替换题序
+    fillIndexOrder(index) {
+      return index => {
+        return index + 1 > 9 ? index + 1 : '0' + (index + 1)
+      }
+    }
+  },
+  watch: {
+    getScrollTop(val) {
+      this.scrollTop = val
+      // let box = this.$refs.rightBox
+      // let btnBack = this.$refs.btnBack
+      // let t = 232 - this.scrollTop
+      // box.style.top = this.scrollTop >= 129 ? '100px' : (t + 'px')
+      // btnBack.style.top = this.scrollTop >= 129 ? (this.scrollTop - 152) + 'px' : '0px'
+    },
+  }
+}
+</script>
+
+<style src="./QuestionList.css" scoped></style>
+
+<style>
+.ql-item .ivu-collapse {
+  border: 0;
+}
+
+.ql-item .ivu-collapse>.ivu-collapse-item>.ivu-collapse-header {
+  color: #06a9bb;
+}
+
+.ql-item .ivu-collapse>.ivu-collapse-item>.ivu-collapse-header>.ivu-icon {
+  vertical-align: unset;
+}
+
+.ql-item .ivu-collapse>.ivu-collapse-item {
+  border-top: 0;
+}
+
+.analysis-modal .ivu-modal-body {
+  padding: 50px 46px !important;
+  height: 800px !important;
+  overflow: auto;
+}
+
+.analysis-modal .ivu-icon-ios-close {
+  font-size: 40px !important;
+  font-weight: bolder;
+}
+</style>

+ 434 - 0
TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/TestAnalysis/htTestAnalysis.vue

@@ -0,0 +1,434 @@
+<template>
+  <div class="scatter-container">
+    <Row class-name="base-table-row">
+      <Col span="12">
+      <span class="component-title">{{$t('totalAnalysis.ta_title1')}}</span>
+      <BaseTestScatter @handleIndexClick="handleIndexClick" :scatterData="scatterData" :currentIndex="currentExerciseIndex"></BaseTestScatter>
+      </Col>
+      <Col span="12">
+      <div class="scatter-statistics">
+        <div class="scatter-table-line">
+          <span>{{$t('totalAnalysis.ta_text1')}}</span>
+          <span>{{$t('totalAnalysis.ta_text2')}}</span>
+          <span>{{$t('totalAnalysis.ta_text3')}}</span>
+          <span>{{$t('totalAnalysis.ta_text4')}}</span>
+        </div>
+        <div class="scatter-table-line">
+          <span>A</span>
+          <span class="scatter-explain" :title="$t('totalAnalysis.ta_text6')">{{$t('totalAnalysis.ta_text6')}}</span>
+          <span>{{A1List.length}}</span>
+          <div class="index-box"><span v-for="(item,index) in A1List" :key="index" class="scatter-exercise-index" @click="handleIndexClick(item.id)">{{Number(item.id) > 9 ? item.id : '0' + item.id}}</span>
+          </div>
+        </div>
+        <Divider />
+        <div class="scatter-table-line">
+          <span>A'</span>
+          <span class="scatter-explain" :title="$t('totalAnalysis.ta_text7')">{{$t('totalAnalysis.ta_text7')}}</span>
+          <span>{{A2List.length}}</span>
+          <div class="index-box"><span v-for="(item,index) in A2List" :key="index" class="scatter-exercise-index" @click="handleIndexClick(item.id)">{{Number(item.id) > 9 ? item.id : '0' + item.id}}</span>
+          </div>
+        </div>
+        <Divider />
+        <div class="scatter-table-line">
+          <span>B</span>
+          <span class="scatter-explain" :title="$t('totalAnalysis.ta_text8')">{{$t('totalAnalysis.ta_text8')}}</span>
+          <span>{{B1List.length}}</span>
+          <div class="index-box"><span v-for="(item,index) in B1List" :key="index" class="scatter-exercise-index" @click="handleIndexClick(item.id)">{{Number(item.id) > 9 ? item.id : '0' + item.id}}</span>
+          </div>
+        </div>
+        <Divider />
+        <div class="scatter-table-line">
+          <span>B'</span>
+          <span class="scatter-explain" :title="$t('totalAnalysis.ta_text9')">{{$t('totalAnalysis.ta_text9')}}</span>
+          <span>{{B2List.length}}</span>
+          <div class="index-box"><span v-for="(item,index) in B2List" :key="index" class="scatter-exercise-index" @click="handleIndexClick(item.id)">{{Number(item.id) > 9 ? item.id : '0' + item.id}}</span>
+          </div>
+        </div>
+      </div>
+      </Col>
+    </Row>
+    <Divider />
+    <!-- 年级单题得分率表 -->
+    <Row class-name="base-table-row">
+      <span class="component-title" style="width: 100%">
+        <span>{{$t('totalAnalysis.ta_title2')}}</span>
+
+      </span>
+      <div class="index-wrap" style="display: unset">
+        <span v-for="(item,index) in exerciseIndexList" :key="index" :title="index" class="exercise-item-index" @click="onIndexClick(item)" :style="{background: (+item > 9 ? item : '0' + item) === currentExerciseIndex ? '#d482ab':'#c8e5e8',
+								color: (+item > 9 ? item : '0' + item) === currentExerciseIndex ? '#fff':'#757575' }">
+          {{+item > 9 ? item : '0' + item}}
+        </span>
+      </div>
+      <div style="width: 100%;">
+        <BaseLineBar :exerciseIndex="currentExerciseIndex" ref="lineBar"></BaseLineBar>
+      </div>
+    </Row>
+    <!-- 试题分析总表 -->
+    <Row class-name="base-table-row">
+      <BaseTable :columns="tableColumns" :tableDatas="tableData" :tableName="$t('totalAnalysis.ta_title3')" tableRef="exerciseAnalsisTable" ref="analysisTable" :tips="$t('totalAnalysis.ta_table_tip2')">
+      </BaseTable>
+    </Row>
+    <!-- 试题得分率表 -->
+    <Row class-name="base-table-row">
+      <BaseTable :columns="exerciseColumns" :tableDatas="tableData" :tableName="$t('totalAnalysis.ta_title4')" tableRef="exerciseScoreRateTable" ref="scoreRateTable" :tips="$t('totalAnalysis.ta_table_tip1')">
+      </BaseTable>
+    </Row>
+  </div>
+</template>
+
+<script>
+import BaseTestScatter from '@/components/student-analysis/total/BaseTestScatter.vue'
+import BaseTable from '@/components/student-analysis/total/BaseMyTable.vue'
+import BaseLineBar from '@/components/student-analysis/total/htBaseLineBar.vue'
+export default {
+  components: {
+    BaseTestScatter,
+    BaseTable,
+    BaseLineBar
+  },
+  data() {
+    return {
+      tableData: [],
+      scatterData: [],
+      exerciseIndexList: [],
+      currentExerciseIndex: 0,
+      A1List: [],
+      A2List: [],
+      B1List: [],
+      B2List: [],
+      exersicesType: this.$GLOBAL.EXERCISE_TYPES(),
+      tableColumns: [{
+        title: this.$t('totalAnalysis.ta_table_text1'),
+        key: 'id',
+        width: 100,
+        renderType: 'renderEventIndex'
+      },
+      {
+        title: this.$t('totalAnalysis.ta_table_text2'),
+        key: 'type',
+        minWidth: 100,
+        renderType: (h, params) => {
+          return h('span', this.exersicesType[params.row.type] || this.$t('courseManage.noData1'))
+        },
+      },
+      {
+        title: this.$t('totalAnalysis.ta_table_text3'),
+        key: 'knowledge',
+        minWidth: 150,
+        renderType: function (h, params) {
+          return h('span', params.row.knowledge || this.$t('courseManage.noData1'))
+        },
+      },
+      {
+        title: this.$t('totalAnalysis.sca_table_text6'),
+        key: 'areaName',
+        minWidth: 100,
+        filters: [
+          {
+            label: "A",
+            value: "A"
+          },
+          {
+            label: "A'",
+            value: "A'"
+          }, {
+            label: "B",
+            value: "B"
+          },
+          {
+            label: "B'",
+            value: "B'"
+          }
+        ],
+        filterMultiple: false,
+        filterRemote(value, row) {
+        }
+      },
+      {
+        title: this.$t('totalAnalysis.ta_table_text4'),
+        key: 'score',
+        sortable: true,
+        minWidth: 80
+      },
+      {
+        title: this.$t('totalAnalysis.ta_table_text5'),
+        key: 'diff',
+        sortable: true,
+        minWidth: 100,
+        renderType: function (h, params) {
+          return h('span', Number(params.row.diff).toFixed(2))
+        },
+      },
+      {
+        title: this.$t('totalAnalysis.ta_table_text6'),
+        key: 'identify',
+        renderType: function (h, params) {
+          return h('span', Number(params.row.identify).toFixed(2))
+        },
+        minWidth: 120
+      },
+      {
+        title: 'R1',
+        key: 'R1',
+        sortable: true,
+        renderType: function (h, params) {
+          return h('span', (Number(params.row.R1) * 100).toFixed(0) + '%')
+        },
+        minWidth: 100
+      },
+      {
+        title: 'R2',
+        key: 'R2',
+        sortable: true,
+        renderType: function (h, params) {
+          return h('span', (Number(params.row.R2) * 100).toFixed(0) + '%')
+        },
+        minWidth: 100
+      },
+      {
+        title: 'R3',
+        sortable: true,
+        key: 'R3',
+        renderType: function (h, params) {
+          return h('span', (Number(params.row.R3) * 100).toFixed(0) + '%')
+        },
+        minWidth: 100
+      },
+      {
+        title: 'R4',
+        sortable: true,
+        key: 'R4',
+        renderType: function (h, params) {
+          return h('span', (Number(params.row.R4) * 100).toFixed(0) + '%')
+        },
+        minWidth: 100
+      },
+      {
+        title: 'R5',
+        sortable: true,
+        key: 'R5',
+        renderType: function (h, params) {
+          return h('span', (Number(params.row.R5) * 100).toFixed(0) + '%')
+        },
+        minWidth: 100
+      },
+      {
+        title: 'R6',
+        sortable: true,
+        key: 'R6',
+        renderType: function (h, params) {
+          return h('span', (Number(params.row.R6) * 100).toFixed(0) + '%')
+        },
+        minWidth: 100
+      }
+      ],
+      exerciseColumns: [{
+        title: this.$t('totalAnalysis.ta_table_text1'),
+        key: 'id',
+        fixed: 'left',
+        width: 100,
+        renderType: 'renderEventIndex'
+      },
+      {
+        title: this.$t('totalAnalysis.ta_table_text10'),
+        key: 'PH',
+        sortable: true,
+        renderType: function (h, params) {
+          return h('span', (Number(params.row.PH) * 100).toFixed(0) + '%')
+        },
+        width: 150
+      },
+      {
+        title: this.$t('totalAnalysis.ta_table_text11'),
+        key: 'PL',
+        sortable: true,
+        renderType: function (h, params) {
+          return h('span', (Number(params.row.PL) * 100).toFixed(0) + '%')
+        },
+        width: 150
+      },
+      {
+        title: this.$t('totalAnalysis.ta_table_text8'),
+        key: 'examScoreRate',
+        sortable: true,
+        renderType: function (h, params) {
+          return h('span', (Number(params.row.examScoreRate)).toFixed(0) + '%')
+        },
+        width: 150
+      },
+        // {
+        //     title: this.$t('totalAnalysis.ta_table_text9'),
+        //     key: 'areaScoreRate',
+        //     sortable: true,
+        //     renderType: function(h, params) {
+        //         return h('span', (Number(params.row.areaScoreRate)).toFixed(0) + '%')
+        //     },
+        //     width: 150
+        // }
+      ]
+    }
+  },
+  created() {
+    this.$parent.$parent.$parent.isShowQuestions = false
+
+  },
+
+  methods: {
+    renderClassColumns(classData) {
+      let classCloumns = []
+      // 渲染进线表格的班级数据
+      classData.classes.forEach(item => {
+        let classColumn = {
+          title: item.className,
+          sortable: 'custom',
+          key: item.className,
+          renderType: (h, params) => {
+            return h('span', (Number(params.row[item.className])).toFixed(0) + '%')
+          },
+          minWidth: 150
+        }
+        classCloumns.push(classColumn)
+      })
+
+      this.exerciseColumns.splice(4, this.exerciseColumns.length - 4, ...classCloumns)
+    },
+
+    // 点击落点图某个点事件
+    handleItemClick(item) {
+      let index = item.data[item.data.length - 1]
+      this.currentExerciseIndex = index > 9 ? '' + index : '0' + index
+
+      // 题目编号联动
+      this.$nextTick(() => {
+        let indexDomList = Array.prototype.slice.call(document.getElementsByClassName(
+          'scatter-exercise-index'))
+        let hignlightIndexDom = indexDomList.filter(item => +item.innerText === +this
+          .currentExerciseIndex)
+        indexDomList.forEach(item => {
+          item.style.background = 'transparent'
+          item.style.color = '#303030'
+        })
+        if (hignlightIndexDom.length) {
+          hignlightIndexDom[0].style.background = '#018b99'
+          hignlightIndexDom[0].style.color = '#FFF'
+          hignlightIndexDom[0].style.borderRadius = '50%'
+        }
+      })
+    },
+
+    onIndexClick(item) {
+      this.handleItemClick({
+        data: [+item]
+      })
+    },
+
+    handleIndexClick(index) {
+      let isClouDAS = this.$route.name === 'privExam'
+      if (isClouDAS) {
+        this.$EventBus.$emit('cloudas-question-click', index)
+      } else {
+        this.$router.push({
+          path: '/total/questionList',
+          query: {
+            QIndex: index
+          }
+        })
+      }
+    },
+
+
+    // 调整图表所需数据结构格式
+    renderData(data) {
+      let analysisJson = JSON.parse(JSON.stringify(this.getAnalysisJson))
+      let curSubjectIndex = analysisJson.subjects.map(i => i.name).indexOf(this.$store.state.totalAnalysis
+        .currentSubjectJoint)
+      let result = []
+      analysisJson.paper[curSubjectIndex].value.forEach((exercise, exerciseIndex) => {
+        let obj = {}
+        analysisJson.paperKey.forEach((key, index) => {
+          obj[key] = exercise[index]
+        })
+
+        analysisJson.classes.forEach(classItem => {
+          obj[classItem.className] = classItem.subjects[curSubjectIndex].item[exerciseIndex]
+        })
+        result.push(obj)
+      })
+      console.log(result)
+      return result
+    },
+
+    doRender(analysisJson) {
+      let exerciseList = this.renderData(analysisJson)
+      this.renderClassColumns(analysisJson)
+      this.exerciseIndexList = exerciseList.map(item => item.id)
+      this.A1List = exerciseList.filter(item => item.areaName === 'A')
+      this.A2List = exerciseList.filter(item => item.areaName === "A'")
+      this.B1List = exerciseList.filter(item => item.areaName === 'B')
+      this.B2List = exerciseList.filter(item => item.areaName === "B'")
+      this.tableData = JSON.parse(JSON.stringify(exerciseList))
+      this.getScatterData()
+      console.log('最新试题数据', this.tableData)
+    },
+
+    getScatterData() {
+      let analysisJson = JSON.parse(JSON.stringify(this.getAnalysisJson))
+      let curSubjectIndex = analysisJson.subjects.map(i => i.name).indexOf(this.$store.state.totalAnalysis
+        .currentSubjectJoint)
+      let result = []
+      analysisJson.paper[curSubjectIndex].value.forEach((exercise, exerciseIndex) => {
+        let obj = {}
+        analysisJson.paperKey.forEach((key, index) => {
+          obj[key] = exercise[index]
+        })
+        result.push(obj)
+      })
+      let arr = result
+      let newArr = []
+      arr.forEach(item => {
+        let arr2 = []
+        arr2.push((+item.X).toFixed(2))
+        arr2.push((item.Y * 100).toFixed(2))
+        arr2.push(item.type)
+        arr2.push(item.id)
+        newArr.push(arr2)
+      })
+      this.scatterData = newArr
+    },
+
+    getLineBarData() {
+
+    }
+  },
+
+  mounted() {
+    this.$refs.analysisTable.$el.childNodes[1].style.borderRight = '0'
+    this.handleItemClick({
+      data: [1]
+    })
+    this.getAnalysisJson && this.doRender(this.getAnalysisJson)
+
+    this.$EventBus.$off('onSubjectChange')
+    this.$EventBus.$on('onSubjectChange', val => {
+      this.doRender(this.getAnalysisJson)
+      // this.$nextTick(() => {
+      // 	this.$refs.lineBar.renderData(this.getAnalysisJson)
+      // })
+    })
+  },
+  computed: {
+    // 获取最新试题分析模块数据
+    getAnalysisJson() {
+      return this.$store.state.totalAnalysis.analysisJsonJoint
+    }
+  }
+}
+</script>
+
+<style src="./TestAnalysis.css" scoped></style>
+<style>
+.scatter-container .ivu-poptip-content {
+  max-width: 520px;
+}
+</style>

+ 12 - 9
TEAMModelOS/Controllers/System/BlobController.cs

@@ -951,11 +951,12 @@ namespace TEAMModelOS.Controllers
                 if (exists)
                 {
                     ttl=0;
-                    var ttlVals = await _azureRedis.GetRedisClient(8).KeyExpireTimeAsync($"Blob:ScanResult:{scope}:{containerName}");
+                    var ttlVals = await _azureRedis.GetRedisClient(8).KeyTimeToLiveAsync($"Blob:ScanResult:{scope}:{containerName}");
                     if (ttlVals.HasValue)
                     {
-                        ttl = DateTimeHelper.ToUnixTimestamp(ttlVals.Value);
-                        ttl= ttl- DateTimeOffset.Now.ToUnixTimeMilliseconds();
+                        ttl =(long) ttlVals.Value.TotalMilliseconds;
+                        //ttl = DateTimeHelper.ToUnixTimestamp(ttlVals.Value);
+                        //ttl= ttl- DateTimeOffset.Now.ToUnixTimeMilliseconds();
                     }
                     var result = await _azureRedis.GetRedisClient(8).StringGetAsync($"Blob:ScanResult:{scope}:{containerName}");
                     if (result.HasValue)
@@ -1057,11 +1058,13 @@ namespace TEAMModelOS.Controllers
             bool exists = await _azureRedis.GetRedisClient(8).KeyExistsAsync($"Blob:ScanResult:{scope}:{containerName}");
             if (exists)
             {
-                var ttlVal=  await _azureRedis.GetRedisClient(8).KeyExpireTimeAsync($"Blob:ScanResult:{scope}:{containerName}");
+              //  var ttlVal=  await _azureRedis.GetRedisClient(8).KeyExpireTimeAsync($"Blob:ScanResult:{scope}:{containerName}");
+                var ttlVal = await _azureRedis.GetRedisClient(8).KeyTimeToLiveAsync($"Blob:ScanResult:{scope}:{containerName}");
                 if (ttlVal.HasValue) 
                 {
-                    ttl = DateTimeHelper.ToUnixTimestamp(ttlVal.Value);
-                    ttl= ttl- DateTimeOffset.Now.ToUnixTimeMilliseconds();
+                   // var ttk = ttlVal.Value.TotalMilliseconds;
+                    ttl = (long )ttlVal.Value.TotalMilliseconds;
+                   // ttl= ttl- DateTimeOffset.Now.ToUnixTimeMilliseconds();
                 }
                 var result = await _azureRedis.GetRedisClient(8).StringGetAsync($"Blob:ScanResult:{scope}:{containerName}");
                 if (result.HasValue)
@@ -1449,11 +1452,11 @@ namespace TEAMModelOS.Controllers
 
              _azureRedis.GetRedisClient(8).StringSet($"Blob:ScanResult:{scope}:{containerName}", unLinks.ToJsonString(), expiry: new TimeSpan(0, 3, 0));
  
-            var ttlVals = await _azureRedis.GetRedisClient(8).KeyExpireTimeAsync($"Blob:ScanResult:{scope}:{containerName}");
+            var ttlVals = await _azureRedis.GetRedisClient(8).KeyTimeToLiveAsync($"Blob:ScanResult:{scope}:{containerName}");
             if (ttlVals.HasValue)
             {
-                ttl = DateTimeHelper.ToUnixTimestamp(ttlVals.Value);
-                ttl= ttl- DateTimeOffset.Now.ToUnixTimeMilliseconds();
+                ttl = (long)ttlVals.Value.TotalMilliseconds;
+               // ttl= ttl- DateTimeOffset.Now.ToUnixTimeMilliseconds();
             }
             return Ok(new { status = 1, totalCount, totalSize, summary, unLinks, ttl });
         }

+ 4 - 4
TEAMModelOS/TEAMModelOS.csproj

@@ -79,11 +79,11 @@
 		<SpaRoot>ClientApp\</SpaRoot>
 		<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
 		<UserSecretsId>078b5d89-7d90-4f6a-88fc-7d96025990a8</UserSecretsId>
-		<Version>5.2411.6</Version>
-		<AssemblyVersion>5.2411.6.1</AssemblyVersion>
-		<FileVersion>5.2411.6.1</FileVersion>
+		<Version>5.2411.13</Version>
+		<AssemblyVersion>5.2411.13.1</AssemblyVersion>
+		<FileVersion>5.2411.13.1</FileVersion>
 		<Description>TEAMModelOS(IES5)</Description>
-		<PackageReleaseNotes>IES版本说明版本切换标记5.2411.6.1</PackageReleaseNotes>
+		<PackageReleaseNotes>IES版本说明版本切换标记5.2411.13.1</PackageReleaseNotes>
 		<PackageId>TEAMModelOS</PackageId>
 		<Authors>teammodel</Authors>
 		<Company>醍摩豆(成都)信息技术有限公司</Company>

+ 1 - 1
TEAMModelOS/appsettings.Development.json

@@ -18,7 +18,7 @@
     "IdTokenSalt": "8263692E2213497BB55E74792B7900B4",
     "HttpTrigger": "https://teammodelosfunction-test.chinacloudsites.cn/api/",
     //"HttpTrigger": "http://localhost:7071/api/"
-    "Version": "5.2411.6.1"
+    "Version": "5.2411.13.1"
   },
   "Azure": {
     // 测试站数据库

+ 1 - 1
TEAMModelOS/appsettings.json

@@ -18,7 +18,7 @@
     "Exp": 86400,
     "IdTokenSalt": "8263692E2213497BB55E74792B7900B4",
     "HttpTrigger": "https://teammodelosfunction.chinacloudsites.cn/api/",
-    "Version": "5.2411.6.1"
+    "Version": "5.2411.13.1"
   },
   "Azure": {
     "Storage": {