Ver código fonte

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

zhouj1203@hotmail.com 2 anos atrás
pai
commit
5f29efdc93
24 arquivos alterados com 1052 adições e 360 exclusões
  1. 1 1
      TEAMModelBI/ClientApp/src/view/areaServe/areamanage.vue
  2. 16 6
      TEAMModelBI/Controllers/BIProductAnalysis/ProductAnalysisController.cs
  3. 1 0
      TEAMModelBI/Controllers/BISchool/SchoolController.cs
  4. 1 4
      TEAMModelOS.SDK/DI/AzureCosmos/AzureCosmosFactory.cs
  5. 1 9
      TEAMModelOS.SDK/DI/AzureStorage/AzureStorageFactory.cs
  6. 146 0
      TEAMModelOS.SDK/Extension/HttpContextExtensions.cs
  7. 4 1
      TEAMModelOS/ClientApp/public/lang/en-US.js
  8. 4 1
      TEAMModelOS/ClientApp/public/lang/zh-CN.js
  9. 4 1
      TEAMModelOS/ClientApp/public/lang/zh-TW.js
  10. 3 0
      TEAMModelOS/ClientApp/src/api/areaArt.js
  11. 131 107
      TEAMModelOS/ClientApp/src/common/BaseLayout.vue
  12. 2 1
      TEAMModelOS/ClientApp/src/components/student-analysis/total/BaseSingleStuScatter.vue
  13. 8 0
      TEAMModelOS/ClientApp/src/components/student-web/EventView/EventContentTypeTemplate/LessonTestReportCharts/BaseScatter.vue
  14. 288 129
      TEAMModelOS/ClientApp/src/view/art/AreaArt.vue
  15. 137 37
      TEAMModelOS/ClientApp/src/view/evaluation/bank/TestPaperList.vue
  16. 48 33
      TEAMModelOS/ClientApp/src/view/evaluation/index/CreatePaper.vue
  17. 1 1
      TEAMModelOS/ClientApp/src/view/login/Index.vue
  18. 18 1
      TEAMModelOS/Controllers/Both/PaperController.cs
  19. 32 0
      TEAMModelOS/Filter/AspNetCoreBuilderServiceCollectionExtensions.cs
  20. 82 0
      TEAMModelOS/Filter/BlobLoggerProvider.cs
  21. 82 0
      TEAMModelOS/Filter/RequestAuditFilter.cs
  22. 9 0
      TEAMModelOS/Program.cs
  23. 5 0
      TEAMModelOS/Startup.cs
  24. 28 28
      TEAMModelOS/appsettings.Development.json

+ 1 - 1
TEAMModelBI/ClientApp/src/view/areaServe/areamanage.vue

@@ -827,7 +827,7 @@ export default {
         schoolCode: [addSchoolitem.value.id],
         standard: currentlySelect.value.standard,
         areaId: currentlySelect.value.id,
-        //code: [addSchoolitem.value.code],
+        code: [addSchoolitem.value.code],
         // schoolCode: [schoolcode],
       }
       console.log(data,'准备提交的内容')

+ 16 - 6
TEAMModelBI/Controllers/BIProductAnalysis/ProductAnalysisController.cs

@@ -70,8 +70,6 @@ namespace TEAMModelBI.Controllers.ProductAnalysis
                 if(string.IsNullOrWhiteSpace(prod)) return BadRequest();
                 if (!jsonElement.TryGetProperty("schoolIds", out JsonElement schoolIdsJobj)) return BadRequest();//學校ID(array)
                 List<string> schoolIds = schoolIdsJobj.ToObject<List<string>>();
-                schoolIds.Add("noschoolid");
-                schoolIds.Add("allschool");
                 string dateUnit = (jsonElement.TryGetProperty("dateUnit", out JsonElement dateUnitJobj)) ? (!string.IsNullOrWhiteSpace(Convert.ToString(dateUnitJobj))) ? Convert.ToString(dateUnitJobj).ToLower() : "day" : "day";//時間統計單位 ※以每年(Year)、每月(Month)、每日(Day) 為統計單位 預設值:每日
                 //起始終止日期換算
                 List<string> dateFromList = dateFromJobj.ToString().Split('-').ToList();
@@ -90,8 +88,13 @@ namespace TEAMModelBI.Controllers.ProductAnalysis
                 //CosmosDB資料取得
                 ////取得學校基本資訊 => 記入Dictionary
                 Dictionary<string, Dictionary<string, string>> schDic = new();
-                string schIdListStr = JsonSerializer.Serialize(schoolIds);
-                string SqlSch = $"SELECT c.id, c.name, c.code, c.region, c.province, c.city, c.dist FROM c WHERE ARRAY_CONTAINS({schIdListStr}, c.id, true) AND (c.code = 'Base' OR c.code = 'VirtualBase')";
+                string SqlSch = $"SELECT c.id, c.name, c.code, c.region, c.province, c.city, c.dist FROM c WHERE (c.code = 'Base' OR c.code = 'VirtualBase')";
+                //ARRAY_CONTAINS({schIdListStr}, c.id, true) AND
+                if(schoolIds.Count > 0)
+                {
+                    string schIdListStr = JsonSerializer.Serialize(schoolIds);
+                    SqlSch += $" AND ARRAY_CONTAINS({schIdListStr}, c.id, true)";
+                }
                 await foreach (var item in cosmosClient.GetContainer("TEAMModelOS", "School").GetItemQueryStreamIterator(queryText: SqlSch, requestOptions: null))
                 {
                     var json = await JsonDocument.ParseAsync(item.ContentStream);
@@ -102,7 +105,7 @@ namespace TEAMModelBI.Controllers.ProductAnalysis
                         string region = Convert.ToString(obj.GetProperty("region"));
                         string province = Convert.ToString(obj.GetProperty("province"));
                         string city = Convert.ToString(obj.GetProperty("city"));
-                        string dist = Convert.ToString(obj.GetProperty("dist"));
+                        string dist = (obj.TryGetProperty("dist", out JsonElement distJ)) ? Convert.ToString(distJ) : string.Empty;
                         string type = Convert.ToString(obj.GetProperty("code"));
                         if (!schDic.ContainsKey(schId))
                         {
@@ -114,7 +117,14 @@ namespace TEAMModelBI.Controllers.ProductAnalysis
 
                 ////取得產品分析資訊
                 List<ProdAnalysisApiResult> result = new List<ProdAnalysisApiResult>();
-                string Sql = $"SELECT * FROM c WHERE c.toolType = '{prod}' AND c.dateUnit = '{dateUnit}' AND ARRAY_CONTAINS({schIdListStr}, c.schoolId, true) AND c.dateTime >= {dateTimeFromSec} AND c.dateTime <= {dateTimeToSec}";
+                string Sql = $"SELECT * FROM c WHERE c.toolType = '{prod}' AND c.dateUnit = '{dateUnit}' AND c.dateTime >= {dateTimeFromSec} AND c.dateTime <= {dateTimeToSec}";
+                if(schoolIds.Count > 0)
+                {
+                    schoolIds.Add("noschoolid");
+                    schoolIds.Add("allschool");
+                    string schIdListStr = JsonSerializer.Serialize(schoolIds);
+                    Sql += $" AND ARRAY_CONTAINS({schIdListStr}, c.schoolId, true)";
+                }
                 await foreach (var item in cosmosClient.GetContainer("TEAMModelOS", "School").GetItemQueryStreamIterator(queryText: Sql, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"ProdAnalysis") }))
                 {
                     var json = await JsonDocument.ParseAsync(item.ContentStream);

+ 1 - 0
TEAMModelBI/Controllers/BISchool/SchoolController.cs

@@ -901,6 +901,7 @@ namespace TEAMModelBI.Controllers.BISchool
                 {
                     if (rmvFromSc) //將該老師從學校的老師名單中移除
                     {
+                        //將該老師從學校的老師名單中移除
                         await cosmosClient.GetContainer("TEAMModelOS", "School").DeleteItemAsync<SchoolTeacher>($"{tmdId}", new PartitionKey($"Teacher-{scId}"));
                     }
                     else

+ 1 - 4
TEAMModelOS.SDK/DI/AzureCosmos/AzureCosmosFactory.cs

@@ -10,20 +10,18 @@ namespace TEAMModelOS.SDK.DI
     {
         private readonly IServiceProvider _services;
         private readonly IOptionsMonitor<AzureCosmosFactoryOptions> _optionsMonitor;
-        private readonly ILogger _logger;
         //private Option _option;
         private ConcurrentDictionary<string, CosmosClient> CosmosClients { get; } = new ConcurrentDictionary<string, CosmosClient>();
         
         //   private CosmosDatabase database { get; set; }
         
-        public AzureCosmosFactory(IServiceProvider services, IOptionsMonitor<AzureCosmosFactoryOptions> optionsMonitor, ILogger<AzureCosmosFactory> logger)
+        public AzureCosmosFactory(IServiceProvider services, IOptionsMonitor<AzureCosmosFactoryOptions> optionsMonitor )
         {
             if (services == null) throw new ArgumentNullException(nameof(services));
             if (optionsMonitor == null) throw new ArgumentNullException(nameof(optionsMonitor));
 
             _services = services;
             _optionsMonitor = optionsMonitor;
-            _logger = logger;  
         }
 
         /// <summary>
@@ -43,7 +41,6 @@ namespace TEAMModelOS.SDK.DI
             }
             catch (Exception e)
             {
-                _logger?.LogWarning(e, e.Message);
                 return null;
             }
         }

+ 1 - 9
TEAMModelOS.SDK/DI/AzureStorage/AzureStorageFactory.cs

@@ -23,15 +23,13 @@ namespace TEAMModelOS.SDK.DI
     {
         private readonly IServiceProvider _services;
         private readonly IOptionsMonitor<AzureStorageFactoryOptions> _optionsMonitor;
-        private readonly ILogger _logger;
-        public AzureStorageFactory(IServiceProvider services, IOptionsMonitor<AzureStorageFactoryOptions> optionsMonitor, ILogger<AzureStorageFactory> logger)
+        public AzureStorageFactory(IServiceProvider services, IOptionsMonitor<AzureStorageFactoryOptions> optionsMonitor)
         {
             if (services == null) throw new ArgumentNullException(nameof(services));
             if (optionsMonitor == null) throw new ArgumentNullException(nameof(optionsMonitor));
 
             _services = services;
             _optionsMonitor = optionsMonitor;
-            _logger = logger;
         }
 
         public BlobServiceClient GetBlobServiceClient(string name = "Default")
@@ -43,7 +41,6 @@ namespace TEAMModelOS.SDK.DI
             }
             catch (OptionsValidationException e)
             {
-                _logger?.LogWarning(e, e.Message);
                 return null;
             }
 
@@ -58,7 +55,6 @@ namespace TEAMModelOS.SDK.DI
             }
             catch (OptionsValidationException e)
             {
-                _logger?.LogWarning(e, e.Message);
                 return null;
             }
         }
@@ -73,7 +69,6 @@ namespace TEAMModelOS.SDK.DI
             }
             catch (OptionsValidationException e)
             {
-                _logger?.LogWarning(e, e.Message);
                 return null;
             }
         }
@@ -321,7 +316,6 @@ namespace TEAMModelOS.SDK.DI
             }
             catch (OptionsValidationException e)
             {
-                _logger?.LogWarning(e, e.Message);
                 return null;
             }
         }
@@ -342,7 +336,6 @@ namespace TEAMModelOS.SDK.DI
             }
             catch (OptionsValidationException e)
             {
-                _logger?.LogWarning(e, e.Message);
                 return null;
             }
         }
@@ -363,7 +356,6 @@ namespace TEAMModelOS.SDK.DI
             }
             catch (OptionsValidationException e)
             {
-                _logger?.LogWarning(e, e.Message);
                 return null;
             }
         }

+ 146 - 0
TEAMModelOS.SDK/Extension/HttpContextExtensions.cs

@@ -1,7 +1,12 @@
 using System;
 using System.Collections.Generic;
+using System.IO;
+using System.Linq;
 using System.Text;
+using System.Threading.Tasks;
+using System.Threading;
 using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Controllers;
 using Microsoft.Extensions.Primitives;
 
 namespace TEAMModelOS.SDK.Extension
@@ -15,7 +20,148 @@ namespace TEAMModelOS.SDK.Extension
         {
             return httpContext.Request.Headers["Authorization"].ToString();
         }
+        /// <summary>
+        /// 获取 Action 特性
+        /// </summary>
+        /// <typeparam name="TAttribute"></typeparam>
+        /// <param name="httpContext"></param>
+        /// <returns></returns>
+        public static TAttribute GetMetadata<TAttribute>(this HttpContext httpContext)
+            where TAttribute : class
+        {
+            return httpContext.GetEndpoint()?.Metadata?.GetMetadata<TAttribute>();
+        }
+        /// <summary>
+        /// 获取 控制器/Action 描述器
+        /// </summary>
+        /// <param name="httpContext"></param>
+        /// <returns></returns>
+        public static ControllerActionDescriptor GetControllerActionDescriptor(this HttpContext httpContext)
+        {
+            return httpContext.GetEndpoint()?.Metadata?.FirstOrDefault(u => u is ControllerActionDescriptor) as ControllerActionDescriptor;
+        }
+        /// <summary>
+        /// 获取本机 IPv4地址
+        /// </summary>
+        /// <param name="context"></param>
+        /// <returns></returns>
+        public static string GetLocalIpAddressToIPv4(this HttpContext context)
+        {
+            return context.Connection.LocalIpAddress?.MapToIPv4()?.ToString();
+        }
+
+        /// <summary>
+        /// 获取本机 IPv6地址
+        /// </summary>
+        /// <param name="context"></param>
+        /// <returns></returns>
+        public static string GetLocalIpAddressToIPv6(this HttpContext context)
+        {
+            return context.Connection.LocalIpAddress?.MapToIPv6()?.ToString();
+        }
+
+        /// <summary>
+        /// 获取远程 IPv4地址
+        /// </summary>
+        /// <param name="context"></param>
+        /// <returns></returns>
+        public static string GetRemoteIpAddressToIPv4(this HttpContext context)
+        {
+            return context.Connection.RemoteIpAddress?.MapToIPv4()?.ToString();
+        }
+
+        /// <summary>
+        /// 获取远程 IPv6地址
+        /// </summary>
+        /// <param name="context"></param>
+        /// <returns></returns>
+        public static string GetRemoteIpAddressToIPv6(this HttpContext context)
+        {
+            return context.Connection.RemoteIpAddress?.MapToIPv6()?.ToString();
+        }
+
+        /// <summary>
+        /// 获取完整请求地址
+        /// </summary>
+        /// <param name="request"></param>
+        /// <returns></returns>
+        public static string GetRequestUrlAddress(this HttpRequest request)
+        {
+            return new StringBuilder()
+                    .Append(request.Scheme)
+                    .Append("://")
+                    .Append(request.Host)
+                    .Append(request.PathBase)
+                    .Append(request.Path)
+                    .Append(request.QueryString)
+                    .ToString();
+        }
+
+        /// <summary>
+        /// 获取来源地址
+        /// </summary>
+        /// <param name="request"></param>
+        /// <param name="refererHeaderKey"></param>
+        /// <returns></returns>
+        public static string GetRefererUrlAddress(this HttpRequest request, string refererHeaderKey = "Referer")
+        {
+            return request.Headers[refererHeaderKey].ToString();
+        }
 
+        /// <summary>
+        /// 读取 Body 内容
+        /// </summary>
+        /// <param name="httpContext"></param>
+        /// <remarks>需先在 Startup 的 Configure 中注册 app.EnableBuffering()</remarks>
+        /// <returns></returns>
+        public static async Task<string> ReadBodyContentAsync(this HttpContext httpContext)
+        {
+            if (httpContext == null) return default;
+            return await httpContext.Request.ReadBodyContentAsync();
+        }
+
+        /// <summary>
+        /// 读取 Body 内容
+        /// </summary>
+        /// <param name="request"></param>
+        /// <remarks>需先在 Startup 的 Configure 中注册 app.EnableBuffering()</remarks>
+        /// <returns></returns>
+        public static async Task<string> ReadBodyContentAsync(this HttpRequest request)
+        {
+            request.Body.Seek(0, SeekOrigin.Begin);
+
+            using var reader = new StreamReader(request.Body, Encoding.UTF8, true, 1024, true);
+            var body = await reader.ReadToEndAsync();
+
+            // 回到顶部,解决此类问题 https://gitee.com/dotnetchina/Furion/issues/I6NX9E
+            request.Body.Seek(0, SeekOrigin.Begin);
+            return body;
+        }
+
+      
+        /// <summary>
+        /// 判断是否是 WebSocket 请求
+        /// </summary>
+        /// <param name="context"></param>
+        /// <returns></returns>
+        public static bool IsWebSocketRequest(this HttpContext context)
+        {
+            return context.WebSockets.IsWebSocketRequest || context.Request.Path == "/ws";
+        }
+        /// <summary>
+        /// 设置响应头 Tokens
+        /// </summary>
+        /// <param name="httpContext"></param>
+        /// <param name="accessToken"></param>
+        /// <param name="refreshToken"></param>
+        public static void SetTokensOfResponseHeaders(this HttpContext httpContext, string accessToken, string refreshToken = null)
+        {
+            httpContext.Response.Headers["access-token"] = accessToken;
+            if (!string.IsNullOrWhiteSpace(refreshToken))
+            {
+                httpContext.Response.Headers["x-access-token"] = refreshToken;
+            }
+        }
         /// <summary>
         /// 取得JWT驗證金鑰,Authorization Bearer
         /// </summary>

+ 4 - 1
TEAMModelOS/ClientApp/public/lang/en-US.js

@@ -1687,7 +1687,9 @@ const LANG_EN_US = {
             reCreateSheet: 'Regenerate',
             choosePaper: 'Select exam file',
             choosed: 'Deselect',
-            searchPaper: 'Enter exam file name to search...'
+            searchPaper: 'Enter exam file name to search...',
+            copyTip1: 'Use ',
+            copyTip2: '  to create a copy'
         },
         importFile: {
             uploadSuc: 'Exam file uploaded and parsed successfully!',
@@ -4975,6 +4977,7 @@ const LANG_EN_US = {
                 student: 'people',
                 score: 'Score',
                 keyPointPerformance: 'Key concept Performance Distribution',
+                pointPlot: 'Learning point map',
                 me: 'Personal',
                 allClass: 'All Class',
                 participantClass: 'Whole Class',

+ 4 - 1
TEAMModelOS/ClientApp/public/lang/zh-CN.js

@@ -1686,7 +1686,9 @@ const LANG_ZH_CN = {
             reCreateSheet: '重新生成',
             choosePaper: '选择试卷',
             choosed: '取消选择',
-            searchPaper: '输入试卷名称进行搜索...'
+            searchPaper: '输入试卷名称进行搜索...',
+            copyTip1: '确认以',
+            copyTip2: '创建副本'
         },
         importFile: {
             uploadSuc: '文件上传解析成功!',
@@ -4974,6 +4976,7 @@ const LANG_ZH_CN = {
                 student: '人',
                 score: '分',
                 keyPointPerformance: '知识点得分率雷达图',
+                pointPlot: '学习落点图',
                 me: '个人',
                 allClass: '所有班级',
                 participantClass: '全班',

+ 4 - 1
TEAMModelOS/ClientApp/public/lang/zh-TW.js

@@ -1688,7 +1688,9 @@ const LANG_ZH_TW = {
             reCreateSheet: '重新生成',
             choosePaper: '選擇試卷',
             choosed: '取消選擇',
-            searchPaper: '輸入試卷名稱進行搜尋…'
+            searchPaper: '輸入試卷名稱進行搜尋…',
+            copyTip1: '確認以',
+            copyTip2: '複製建立試卷'
         },
         importFile: {
             uploadSuc: '試卷檔案上傳解析成功!',
@@ -4975,6 +4977,7 @@ const LANG_ZH_TW = {
                 student: '人',
                 score: '分',
                 keyPointPerformance: '知識點表現分佈圖',
+                pointPlot: '學習落點圖',
                 me: '個人',
                 allClass: '所有班級',
                 participantClass: '全班',

+ 3 - 0
TEAMModelOS/ClientApp/src/api/areaArt.js

@@ -57,4 +57,7 @@ export default {
     findAreaArtList: function (data) {
         return post('/school/area/find-area-art', data)
     },
+    findKnoPercents: function (data) {
+        return post('/school/area/get-all-knowledge', data)
+    },
 }

+ 131 - 107
TEAMModelOS/ClientApp/src/common/BaseLayout.vue

@@ -3,125 +3,150 @@
     <!-- 头部菜单栏 -->
     <Header class="header">
       <div class="logo-wrap">
-        <img style="margin-left: 20px;" src="../assets/login/ies5_logo_2.svg" :class="isCollapsed ? 'collapsed-logo-width unit-logo':'collapsed-logo-width  unit-logo'" v-show="isShowLogo && !isShowAreaSelect" />
-        <BaseSelectArea @noArea="isShowAreaSelect = false" v-show="isShowAreaSelect"></BaseSelectArea>
-      </div>
-      <div class="school-wrap">
-        <BaseSelectSchool @onSchoolChange="onSchoolChange"></BaseSelectSchool>
-      </div>
-      <slot name="header-content"></slot>
-    </Header>
-    <!-- 侧边菜单栏 -->
-    <Sider class="biz-menu" @mouseover.native="mouseOver" @mouseleave.native="mouseLeave" ref="side1" hide-trigger collapsible width="190" :collapsed-width="78" v-model="isCollapsed">
-      <div class="system-level-wrap" v-show="$store.state.userInfo.hasSchool && !isGlobalSite">
-        <p :class="[!versionsPast ?'system-level-past':'','system-level']">{{systemLevel}}</p>
-      </div>
-      <Menu style="margin-top:0px" width="auto" :class="['menu-item', isCollapsed ? 'collapsed-menu' : '']" accordion :active-name="activeName" :open-names="openNames">
-        <vuescroll :ops="ops">
-          <!-- 学校菜单 -->
-          <MenuGroup :title="$t('system.menu.school')">
-            <!-- 暂未提供判断是否购买服务的字段 -->
-            <p class="school-menu-tips" v-show="schoolStatusInfo">
-              {{schoolStatusInfo}}
-            </p>
-            <div v-if="$store.state.userInfo.hasSchool">
-              <div v-for="(item,index) in schoolMenu" :key="index">
-                <Submenu :name="item.subName" v-if="$access.ability(item.role,item.permission).validateAll && item.child.length && item.isShow" v-show="(index == 0 && $store.state.userInfo.hasSchool) || index > 0">
+          <img style="margin-left: 20px;" src="../assets/login/ies5_logo_2.svg"
+            :class="isCollapsed ? 'collapsed-logo-width unit-logo' : 'collapsed-logo-width  unit-logo'"
+            v-show="isShowLogo && !isShowAreaSelect" />
+          <BaseSelectArea @noArea="isShowAreaSelect = false" v-show="isShowAreaSelect"></BaseSelectArea>
+        </div>
+        <div class="school-wrap">
+          <BaseSelectSchool @onSchoolChange="onSchoolChange"></BaseSelectSchool>
+        </div>
+        <slot name="header-content"></slot>
+      </Header>
+      <!-- 侧边菜单栏 -->
+      <Sider class="biz-menu" @mouseover.native="mouseOver" @mouseleave.native="mouseLeave" ref="side1" hide-trigger
+        collapsible width="190" :collapsed-width="78" v-model="isCollapsed">
+        <div class="system-level-wrap" v-show="$store.state.userInfo.hasSchool && !isGlobalSite">
+          <p :class="[!versionsPast ? 'system-level-past' : '', 'system-level']">{{ systemLevel }}</p>
+        </div>
+        <Menu style="margin-top:0px" width="auto" :class="['menu-item', isCollapsed ? 'collapsed-menu' : '']" accordion
+          :active-name="activeName" :open-names="openNames">
+          <vuescroll :ops="ops">
+            <!-- 学校菜单 -->
+            <MenuGroup :title="$t('system.menu.school')">
+              <!-- 暂未提供判断是否购买服务的字段 -->
+              <p class="school-menu-tips" v-show="schoolStatusInfo">
+                {{ schoolStatusInfo }}
+              </p>
+              <div v-if="$store.state.userInfo.hasSchool">
+                <div v-for="(item, index) in schoolMenu" :key="index">
+                  <Submenu :name="item.subName"
+                    v-if="$access.ability(item.role, item.permission).validateAll && item.child.length && item.isShow"
+                    v-show="(index == 0 && $store.state.userInfo.hasSchool) || index > 0">
+                    <template slot="title">
+                      <!-- <Tooltip :content="item.name" placement="right" transfer v-show="isCollapsed"> -->
+                      <Icon v-show="isCollapsed" :custom="item.icon" style="width:55px;text-align:left;"
+                        :class="isCollapsed ? 'collapse-icon-size' : ''" size="16" />
+                      <!-- </Tooltip> -->
+                      <Icon v-show="!isCollapsed" :custom="item.icon" :class="isCollapsed ? 'collapse-icon-size' : ''"
+                        size="16" />
+                      <span>{{ item.name }}</span>
+                    </template>
+                    <MenuItem :name="menuItem.menuName" @click.native.capture="menuClick(menuItem)" :to="menuItem.router"
+                      v-for="(menuItem, i) in item.child" :key="i"
+                      v-show="$access.ability(menuItem.role, menuItem.permission).validateAll && menuItem.isShow">
+                    <!-- <Tooltip :content="menuItem.name" placement="right" transfer v-show="isCollapsed"> -->
+                    <Icon v-show="isCollapsed" class="sub-menu-icon" :custom="menuItem.icon" size="16" />
+                    <!-- </Tooltip> -->
+                    <Icon v-show="!isCollapsed" class="sub-menu-icon" :custom="menuItem.icon" size="16" />
+                    <span>
+                      {{ menuItem.name }}
+                      <Tooltip v-if="menuItem.info" transfer-class-name="menu-tooltips" :content="menuItem.info" transfer
+                        theme="light" max-width="180">
+                        <Icon type="ios-information-circle-outline" color="#1cc0f3" />
+                      </Tooltip>
+                      <span style="margin-left: 2px;font-size: 12px;vertical-align: text-top;color:#ff7c00">
+                        {{ menuItem.tag }}
+                      </span>
+                    </span>
+                    </MenuItem>
+                  </Submenu>
+                  <MenuItem :name="item.menuName"
+                    v-else-if="$access.ability(item.role, item.permission).validateAll && item.isShow" :to="item.router">
+                  <!-- <Tooltip :content="item.name" placement="right" transfer v-show="isCollapsed"> -->
+                  <Icon v-show="isCollapsed" :custom="item.icon" style="width:55px;text-align:left;"
+                    :class="isCollapsed ? 'collapse-icon-size' : ''" size="16" />
+                  <!-- </Tooltip> -->
+                  <Icon v-show="!isCollapsed" :custom="item.icon" :class="isCollapsed ? 'collapse-icon-size' : ''"
+                    size="16" />
+                  <span>
+                    {{ item.name }}
+                    <Tooltip v-if="item.info" :content="item.info" theme="light" transfer max-width="180">
+                      <Icon type="ios-information-circle-outline" color="#1cc0f3" />
+                    </Tooltip>
+                    <span style="color: rgb(255, 124, 0);margin-left: 2px;font-size: 12px;vertical-align: text-top;">
+                      {{ item.tag }}
+                    </span>
+                  </span>
+                  </MenuItem>
+                </div>
+              </div>
+            </MenuGroup>
+            <!-- 个人菜单 -->
+            <MenuGroup :title="$t('system.menu.private')">
+              <div v-for="(item, index) in teacherMenu" :key="index">
+                <Submenu :name="item.subName"
+                  v-if="$access.ability(item.role, item.permission).validateAll && item.child.length && item.isShow"
+                  v-show="(index == 0 && $store.state.userInfo.hasSchool) || index > 0">
                   <template slot="title">
                     <!-- <Tooltip :content="item.name" placement="right" transfer v-show="isCollapsed"> -->
-                    <Icon v-show="isCollapsed" :custom="item.icon" style="width:55px;text-align:left;" :class="isCollapsed ? 'collapse-icon-size':''" size="16" />
+                    <Icon v-show="isCollapsed" :custom="item.icon" style="width:55px;text-align:left;"
+                      :class="isCollapsed ? 'collapse-icon-size' : ''" size="16" />
                     <!-- </Tooltip> -->
-                    <Icon v-show="!isCollapsed" :custom="item.icon" :class="isCollapsed ? 'collapse-icon-size':''" size="16" />
-                    <span>{{item.name}}</span>
+                    <Icon v-show="!isCollapsed" :custom="item.icon" :class="isCollapsed ? 'collapse-icon-size' : ''"
+                      size="16" />
+                    <span>{{ item.name }}</span>
                   </template>
-                  <MenuItem :name="menuItem.menuName" @click.native.capture="menuClick(menuItem)" :to="menuItem.router" v-for="(menuItem,i) in item.child" :key="i" v-show="$access.ability(menuItem.role,menuItem.permission).validateAll && menuItem.isShow">
+                  <MenuItem :name="menuItem.menuName" :to="menuItem.router" v-for="(menuItem, i) in item.child" :key="i"
+                    v-show="$access.ability(menuItem.role, menuItem.permission).validateAll && menuItem.isShow">
                   <!-- <Tooltip :content="menuItem.name" placement="right" transfer v-show="isCollapsed"> -->
-                  <Icon v-show="isCollapsed" class="sub-menu-icon" :custom="menuItem.icon" size="16" />
+                  <Icon v-show="isCollapsed" class="sub-menu-icon" :custom="menuItem.icon" size="18" />
                   <!-- </Tooltip> -->
                   <Icon v-show="!isCollapsed" class="sub-menu-icon" :custom="menuItem.icon" size="16" />
                   <span>
-                    {{menuItem.name}}
-                    <Tooltip v-if="menuItem.info" transfer-class-name="menu-tooltips" :content="menuItem.info" transfer theme="light" max-width="180">
+                    {{ menuItem.name }}
+                    <Tooltip transfer-class-name="menu-tooltips" v-if="menuItem.info" :content="menuItem.info" transfer
+                      theme="light" max-width="200">
                       <Icon type="ios-information-circle-outline" color="#1cc0f3" />
                     </Tooltip>
                     <span style="margin-left: 2px;font-size: 12px;vertical-align: text-top;color:#ff7c00">
-                      {{menuItem.tag}}
+                      {{ menuItem.tag }}
                     </span>
                   </span>
                   </MenuItem>
                 </Submenu>
-                <MenuItem :name="item.menuName" v-else-if="$access.ability(item.role,item.permission).validateAll && item.isShow" :to="item.router">
-                <!-- <Tooltip :content="item.name" placement="right" transfer v-show="isCollapsed"> -->
-                <Icon v-show="isCollapsed" :custom="item.icon" style="width:55px;text-align:left;" :class="isCollapsed ? 'collapse-icon-size':''" size="16" />
-                <!-- </Tooltip> -->
-                <Icon v-show="!isCollapsed" :custom="item.icon" :class="isCollapsed ? 'collapse-icon-size':''" size="16" />
+                <MenuItem @click.native.capture="menuClick(item)" :name="item.menuName"
+                  v-else-if="$access.ability(item.role, item.permission).validateAll && item.isShow" :to="item.router">
+                <Icon v-show="isCollapsed" :custom="item.icon" style="width:55px;text-align:left;"
+                  :class="isCollapsed ? 'collapse-icon-size' : ''" size="16" />
+                <Icon v-show="!isCollapsed" :custom="item.icon" :class="isCollapsed ? 'collapse-icon-size' : ''" size="16" />
                 <span>
-                  {{item.name}}
-                  <Tooltip v-if="item.info" :content="item.info" theme="light" transfer max-width="180">
+                  {{ item.name }}
+                  <Tooltip transfer-class-name="menu-tooltips" v-if="item.info" :content="item.info" theme="light" transfer
+                    max-width="200">
                     <Icon type="ios-information-circle-outline" color="#1cc0f3" />
                   </Tooltip>
-                  <span style="color: rgb(255, 124, 0);margin-left: 2px;font-size: 12px;vertical-align: text-top;">
-                    {{item.tag}}
+                  <span style="color: aqua;margin-left: 2px;font-size: 12px;margin-top:3px">
+                    {{ item.tag }}
                   </span>
                 </span>
                 </MenuItem>
               </div>
-            </div>
-          </MenuGroup>
-          <!-- 个人菜单 -->
-          <MenuGroup :title="$t('system.menu.private')">
-            <div v-for="(item,index) in teacherMenu" :key="index">
-              <Submenu :name="item.subName" v-if="$access.ability(item.role,item.permission).validateAll && item.child.length && item.isShow" v-show="(index == 0 && $store.state.userInfo.hasSchool) || index > 0">
-                <template slot="title">
-                  <!-- <Tooltip :content="item.name" placement="right" transfer v-show="isCollapsed"> -->
-                  <Icon v-show="isCollapsed" :custom="item.icon" style="width:55px;text-align:left;" :class="isCollapsed ? 'collapse-icon-size':''" size="16" />
-                  <!-- </Tooltip> -->
-                  <Icon v-show="!isCollapsed" :custom="item.icon" :class="isCollapsed ? 'collapse-icon-size':''" size="16" />
-                  <span>{{item.name}}</span>
-                </template>
-                <MenuItem :name="menuItem.menuName" :to="menuItem.router" v-for="(menuItem,i) in item.child" :key="i" v-show="$access.ability(menuItem.role,menuItem.permission).validateAll && menuItem.isShow">
-                <!-- <Tooltip :content="menuItem.name" placement="right" transfer v-show="isCollapsed"> -->
-                <Icon v-show="isCollapsed" class="sub-menu-icon" :custom="menuItem.icon" size="18" />
-                <!-- </Tooltip> -->
-                <Icon v-show="!isCollapsed" class="sub-menu-icon" :custom="menuItem.icon" size="16" />
-                <span>
-                  {{menuItem.name}}
-                  <Tooltip transfer-class-name="menu-tooltips" v-if="menuItem.info" :content="menuItem.info" transfer theme="light" max-width="200">
-                    <Icon type="ios-information-circle-outline" color="#1cc0f3" />
-                  </Tooltip>
-                  <span style="margin-left: 2px;font-size: 12px;vertical-align: text-top;color:#ff7c00">
-                    {{menuItem.tag}}
-                  </span>
-                </span>
-                </MenuItem>
-              </Submenu>
-              <MenuItem @click.native.capture="menuClick(item)" :name="item.menuName" v-else-if="$access.ability(item.role,item.permission).validateAll && item.isShow" :to="item.router">
-              <Icon v-show="isCollapsed" :custom="item.icon" style="width:55px;text-align:left;" :class="isCollapsed ? 'collapse-icon-size':''" size="16" />
-              <Icon v-show="!isCollapsed" :custom="item.icon" :class="isCollapsed ? 'collapse-icon-size':''" size="16" />
-              <span>
-                {{item.name}}
-                <Tooltip transfer-class-name="menu-tooltips" v-if="item.info" :content="item.info" theme="light" transfer max-width="200">
-                  <Icon type="ios-information-circle-outline" color="#1cc0f3" />
-                </Tooltip>
-                <span style="color: aqua;margin-left: 2px;font-size: 12px;margin-top:3px">
-                  {{item.tag}}
-                </span>
-              </span>
-              </MenuItem>
-            </div>
-          </MenuGroup>
-        </vuescroll>
-      </Menu>
-      <p class="copyright-info">
-        <Icon :title="isLock ? $t('system.menu.unlockTips') : $t('system.menu.lockTips')" :custom="isLock ? 'iconfont icon-lock' : 'iconfont icon-unlock'" @click="isLock = !isLock" class="lock-menu-btn" />
-      </p>
-    </Sider>
-    <Loading v-if="isLoading"></Loading>
-    <!-- Body内容部分 -->
-    <Layout :class="!isLock ?'collapsed-padding content-wrap':'content-wrap'">
-      <slot name="content"></slot>
-    </Layout>
-  </div>
+            </MenuGroup>
+          </vuescroll>
+        </Menu>
+        <p class="copyright-info">
+          <Icon :title="isLock ? $t('system.menu.unlockTips') : $t('system.menu.lockTips')"
+            :custom="isLock ? 'iconfont icon-lock' : 'iconfont icon-unlock'" @click="isLock = !isLock"
+            class="lock-menu-btn" />
+        </p>
+      </Sider>
+      <Loading v-if="isLoading"></Loading>
+      <!-- Body内容部分 -->
+      <Layout :class="!isLock ? 'collapsed-padding content-wrap' : 'content-wrap'">
+        <slot name="content"></slot>
+      </Layout>
+    </div>
 </template>
 <script>
 import { mapGetters } from 'vuex'
@@ -341,6 +366,9 @@ export default {
     isGlobalSite() {
       return this.$store.state.config.srvAdr !== 'China'
     },
+    isQingYangArea() {
+      return sessionStorage.getItem('areaId') && sessionStorage.getItem('areaId') === '7a51072f-b329-4e74-99e0-ba0407ba8926'
+    },
     /* 是否开启课堂记录管理开关 */
     isOpenLessonRecord() {
       return this.$store.state.user.schoolProfile.school_base?.openLessonRecord
@@ -752,7 +780,7 @@ export default {
               role: 'admin',
               permission: 'art-read|art-upd',
               menuName: 'mgtArtExam',
-              isShow: true
+              isShow: !this.isQingYangArea
             },
             {
               icon: 'iconfont icon-assess',
@@ -1152,10 +1180,12 @@ body {
   // font-family:"Microsoft YaHei",'"Hm"','Microsoft JhengHei UI','Microsoft JhengHei';
   // font-family: "Hm", "Microsoft JhengHei UI", "Microsoft JhengHei";
 }
+
 .menu-tooltips .ivu-tooltip-inner {
   word-break: break-word;
   text-align: left;
 }
+
 .biz-menu .ivu-menu-vertical .ivu-menu-item-group-title {
   font-size: 12px;
   line-height: 30px;
@@ -1211,9 +1241,7 @@ body {
   width: 0px;
 }
 
-.biz-menu
-  .ivu-menu-light.ivu-menu-vertical
-  .ivu-menu-item-active:not(.ivu-menu-submenu) {
+.biz-menu .ivu-menu-light.ivu-menu-vertical .ivu-menu-item-active:not(.ivu-menu-submenu) {
   background: #101117;
   color: white;
   z-index: 2;
@@ -1317,15 +1345,11 @@ body {
   align-items: center;
 }
 
-.biz-menu
-  .ivu-menu-light.ivu-menu-vertical
-  .ivu-menu-item-active:not(.ivu-menu-submenu) {
+.biz-menu .ivu-menu-light.ivu-menu-vertical .ivu-menu-item-active:not(.ivu-menu-submenu) {
   color: #1cc0f3;
 }
 
-.biz-menu
-  .ivu-menu-light.ivu-menu-vertical
-  .ivu-menu-item-active:not(.ivu-menu-submenu):after {
+.biz-menu .ivu-menu-light.ivu-menu-vertical .ivu-menu-item-active:not(.ivu-menu-submenu):after {
   // background: transparent;
   background: #1cc0f3;
   width: 1px;

+ 2 - 1
TEAMModelOS/ClientApp/src/components/student-analysis/total/BaseSingleStuScatter.vue

@@ -47,7 +47,8 @@ export default {
       // 指定图表的配置项和数据
       var option = {
         title: {
-          text: this.$t('studentWeb.exam.chart.recognizePerformance'),
+          text: this.$t('studentWeb.exam.chart.pointPlot'),
+          left: 'left',
           textStyle: {
             fontSize: "14",
           }

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

@@ -46,6 +46,14 @@ export default {
       let _this = this
       // 指定图表的配置项和数据
       var option = {
+        title: {
+            text: this.$t('studentWeb.exam.chart.pointPlot'),
+            left: 'left',
+            textStyle: {
+                // fontWeight: "normal",
+                fontSize: "14",
+            }
+        },
         tooltip: {
           trigger: 'item',
           showDelay: 0,

+ 288 - 129
TEAMModelOS/ClientApp/src/view/art/AreaArt.vue

@@ -3,131 +3,144 @@
     <vuescroll ref="art-dasboard">
       <Loading v-show="isLoading"></Loading>
       <back-to-top @on-to-top="backToTop"></back-to-top>
-      <div class="export-box"  @click="exportArtTable">
-        <span class="icon iconfont icon-download" style="margin-right:5px;margin-top: 5px;" :title="`下载艺术评测数据表`"></span>
-        <span>下载数据总表</span>
-      </div>
-      <div class="tab-box" style="padding:0px 20px 5px 20px;">
-        <span class="pane" v-for="item in periodList" style="line-height:30px;padding:2px;" @click="tabClick(item.value)" :class="{ active: periodId === item.value }">
-          {{item.label}}
-        </span>
-        <!-- 艺术活动选择 -->
-        <Select v-model="curAreaArtIndex" @on-change="onAcChange" style="margin:15px 0">
-          <Option v-for="(item,index) in areaArtList" :value="index" :key="index">
-            {{ item.name }}
-          </Option>
-        </Select>
-      </div>
-      <TipsInfo v-if="emptyData" msg="暂无数据"></TipsInfo>
-      <template v-else>
-        <!-- 头部统计 -->
-        <div class="top-block-wrap">
-          <div class="content-con-item border-style" v-for="(item,index) in topData" :key="index">
-            <div class="left-area" :style="{background: item.color, width: '36%'}">
-              <Icon :type="item.icon" class="icon" style="font-size: 40px; color: rgb(255, 255, 255);"></Icon>
-            </div>
-            <div class="right-area" style="width: 64%;">
-              <div>
-                <div class="count-to-wrapper">
-                  <p class="content-outer">
-                    <CountTo :decimals="item.type === 'rate' ? 1 : 0" class="count-to-count-text count-style" :endVal="item.number" :duration="600"></CountTo>
-                    <span style="font-size: 28px;" v-if="item.type === 'rate'">%</span>
-                  </p>
-                </div>
-                <p>{{item.text}}</p>
-              </div>
-            </div>
-          </div>
-        </div>
-        <!-- 艺术素质评测概览 -->
-        <div class="online-train-wrap">
-          <h4 class="block-title">艺术素质评测概览</h4>
-          <div class="chart-data-wrap">
-            <Overall :overall="overallData"></Overall>
-          </div>
-        </div>
-        <!-- 知识点得分统计 -->
-        <div class="online-train-wrap">
-          <h4 class="block-title">知识点得分率(音乐)</h4>
-          <div class="chart-line-wrap">
-            <div class="kng-level-wrap chart-data-wrap">
-              <KngLevel :kngData="musicKn"></KngLevel>
-            </div>
-            <div class="kng-point-wrap border-style chart-data-wrap">
-              <KngPoint :kngData="musicKn"></KngPoint>
-            </div>
-          </div>
-        </div>
-        <div class="online-train-wrap">
-          <h4 class="block-title">知识点得分率(美术)</h4>
-          <div class="chart-line-wrap">
-            <div class="kng-level-wrap chart-data-wrap">
-              <KngLevel :kngData="drawKn"></KngLevel>
-            </div>
-            <div class="kng-point-wrap border-style chart-data-wrap">
-              <KngPoint :kngData="drawKn"></KngPoint>
-            </div>
-          </div>
-        </div>
-        <!-- 评测学校情况对比 -->
-        <div class="online-train-wrap">
-          <h4 class="block-title">学校评测情况(音乐)</h4>
-          <div class="chart-data-wrap">
-            <SchoolComp :schools="musicExam"></SchoolComp>
-          </div>
-        </div>
-        <div class="online-train-wrap">
-          <h4 class="block-title">学校评测情况(美术)</h4>
-          <div class="chart-data-wrap">
-            <SchoolComp :schools="drawExam"></SchoolComp>
-          </div>
-        </div>
-        <!-- 艺术特长获奖情况 -->
-        <div class="online-train-wrap">
-          <h4 class="block-title">艺术特长获奖情况</h4>
-          <div class="chart-line-wrap">
-            <div class="award-chart-wrap chart-data-wrap">
-              <!-- <Award :awardData="musicAward" titleText="音乐赛事艺术获奖情况"></Award> -->
-              <EmptyData textContent="暂无音乐获奖情况数据"></EmptyData>
-            </div>
-            <div class="award-chart-wrap chart-data-wrap">
-              <!-- <Award :awardData="drawAward" titleText="美术赛事艺术获奖情况"></Award> -->
-              <EmptyData textContent="暂无美术获奖情况数据"></EmptyData>
-            </div>
-          </div>
-        </div>
-        <!-- 学校列表 -->
-        <div class="online-train-wrap">
-          <div style="height:30px">
-            <h4 class="block-title">学校列表</h4>
-            <Input v-special-char search placeholder="搜索学校" class="school-search" v-model="keyword" />
-          </div>
-          <div class="school-data-wrap">
-            <div class="school-data-item border-style" v-for="(item,index) in schoolListShow" :key="index">
-              <img class="school-img" :src="item.picture || defImg">
-              <div style="margin-left:10px;height:fit-content;">
-                <p class="school-name" :title="item.name">{{item.name}}<span class="to-school-detail" @click="toSchoolDetail(item)">
-                    详情 >
-                  </span></p>
-                <div style="display:flex;margin-top:10px">
-                  <p class="school-value">
-                    <span class="value">{{ parseInt(item.musicPass * 100) || 0}} <span style="font-size:18px">%</span> </span>
-                    <span>音乐及格率</span>
-                  </p>
-                  <p class="school-value">
-                    <span class="value">{{ parseInt(item.drawPass * 100) || 0}} <span style="font-size:18px">%</span> </span>
-                    <span>美术及格率</span>
-                  </p>
-                </div>
+                      <div class="export-box" @click="exportArtTable">
+                        <span class="icon iconfont icon-download" style="margin-right:5px;margin-top: 5px;" :title="`下载艺术评测数据表`"></span>
+                        <span>下载数据总表</span>
+                      </div>
+                      <div class="tab-box" style="padding:0px 20px 5px 20px;">
+                        <span class="pane" v-for="item in periodList" style="line-height:30px;padding:2px;" @click="tabClick(item.value)"
+                          :class="{ active: periodId === item.value }">
+                          {{ item.label }}
+                        </span>
+                        <!-- 艺术活动选择 -->
+                        <Select v-model="curAreaArtIndex" @on-change="onAcChange" style="margin:15px 0">
+                          <Option v-for="(item, index) in areaArtList" :value="index" :key="index">
+                            {{ item.name }}
+                          </Option>
+                        </Select>
+                      </div>
+                      <TipsInfo v-if="emptyData" msg="暂无数据"></TipsInfo>
+                      <template v-else>
+                        <!-- 头部统计 -->
+                        <div class="top-block-wrap">
+                          <div class="content-con-item border-style" v-for="(item, index) in topData" :key="index">
+                            <div class="left-area" :style="{ background: item.color, width: '36%' }">
+                              <Icon :type="item.icon" class="icon" style="font-size: 40px; color: rgb(255, 255, 255);"></Icon>
+                            </div>
+                            <div class="right-area" style="width: 64%;">
+                              <div>
+                                <div class="count-to-wrapper">
+                                  <p class="content-outer">
+                                    <CountTo :decimals="item.type === 'rate' ? 1 : 0" class="count-to-count-text count-style"
+                                      :endVal="item.number" :duration="600"></CountTo>
+                                    <span style="font-size: 28px;" v-if="item.type === 'rate'">%</span>
+                                  </p>
+                                </div>
+                                <p>{{ item.text }}</p>
+                              </div>
+                            </div>
+                          </div>
+                        </div>
+                        <!-- 艺术素质评测概览 -->
+                        <div class="online-train-wrap">
+                          <h4 class="block-title">艺术素质评测概览</h4>
+                          <div class="chart-data-wrap">
+                            <Overall :overall="overallData"></Overall>
+                          </div>
+                        </div>
+                        <!-- 知识点得分统计 -->
+                        <div class="online-train-wrap">
+                          <h4 class="block-title">知识点得分率(音乐)</h4>
+                          <p class="export-btn" @click="exportKnoData('subject_music')">
+                            <span class="icon iconfont icon-download" :title="`下载知识点得分率表格`"></span>
+                            导出数据
+                          </p>
+                          <div class="chart-line-wrap">
+                            <div class="kng-level-wrap chart-data-wrap">
+                              <KngLevel :kngData="musicKn"></KngLevel>
+                            </div>
+                            <div class="kng-point-wrap border-style chart-data-wrap">
+                              <KngPoint :kngData="musicKn"></KngPoint>
+                            </div>
+                          </div>
+                        </div>
+                        <div class="online-train-wrap">
+                          <h4 class="block-title">知识点得分率(美术)</h4>
+                          <p class="export-btn" @click="exportKnoData('subject_painting')">
+                            <span class="icon iconfont icon-download" :title="`下载知识点得分率表格`"></span>
+                            导出数据
+                          </p>
+                          <div class="chart-line-wrap">
+                            <div class="kng-level-wrap chart-data-wrap">
+                              <KngLevel :kngData="drawKn"></KngLevel>
+                            </div>
+                            <div class="kng-point-wrap border-style chart-data-wrap">
+                              <KngPoint :kngData="drawKn"></KngPoint>
+                            </div>
+                          </div>
+                        </div>
+                        <!-- 评测学校情况对比 -->
+                        <div class="online-train-wrap">
+                          <h4 class="block-title">学校评测情况(音乐)</h4>
+                          <div class="chart-data-wrap">
+                            <SchoolComp :schools="musicExam"></SchoolComp>
+                          </div>
+                        </div>
+                        <div class="online-train-wrap">
+                          <h4 class="block-title">学校评测情况(美术)</h4>
+                          <div class="chart-data-wrap">
+                            <SchoolComp :schools="drawExam"></SchoolComp>
+                          </div>
+                        </div>
+                        <!-- 艺术特长获奖情况 -->
+                        <div class="online-train-wrap">
+                          <h4 class="block-title">艺术特长获奖情况</h4>
+                          <div class="chart-line-wrap">
+                            <div class="award-chart-wrap chart-data-wrap">
+                              <!-- <Award :awardData="musicAward" titleText="音乐赛事艺术获奖情况"></Award> -->
+                              <EmptyData textContent="暂无音乐获奖情况数据"></EmptyData>
+                            </div>
+                            <div class="award-chart-wrap chart-data-wrap">
+                              <!-- <Award :awardData="drawAward" titleText="美术赛事艺术获奖情况"></Award> -->
+                              <EmptyData textContent="暂无美术获奖情况数据"></EmptyData>
+                            </div>
+                          </div>
+                        </div>
+                        <!-- 学校列表 -->
+                        <div class="online-train-wrap">
+                          <div style="height:30px">
+                            <h4 class="block-title">学校列表</h4>
+                            <Input v-special-char search placeholder="搜索学校" class="school-search" v-model="keyword" />
+                          </div>
+                          <div class="school-data-wrap">
+                            <div class="school-data-item border-style" v-for="(item, index) in schoolListShow" :key="index">
+                              <img class="school-img" :src="item.picture || defImg">
+                              <div style="margin-left:10px;height:fit-content;">
+                                <p class="school-name" :title="item.name">{{ item.name }}<span class="to-school-detail"
+                                    @click="toSchoolDetail(item)">
+                                    详情 >
+                                  </span></p>
+                                <div style="display:flex;margin-top:10px">
+                                  <p class="school-value">
+                                    <span class="value">{{ parseInt(item.musicPass * 100) || 0 }} <span style="font-size:18px">%</span>
+                                    </span>
+                                    <span>音乐及格率</span>
+                                  </p>
+                                  <p class="school-value">
+                                    <span class="value">{{ parseInt(item.drawPass * 100) || 0 }} <span style="font-size:18px">%</span>
+                                    </span>
+                                    <span>美术及格率</span>
+                                  </p>
+                                </div>
 
-              </div>
-            </div>
-            <EmptyData textContent="暂无学校数据" v-show="!schoolListShow.length"></EmptyData>
-          </div>
-        </div>
-      </template>
-    </vuescroll>
-  </div>
+                              </div>
+                            </div>
+                            <EmptyData textContent="暂无学校数据" v-show="!schoolListShow.length"></EmptyData>
+                          </div>
+                        </div>
+                      </template>
+                    </vuescroll>
+                  </div>
 </template>
 <script>
 import excel from '@/utils/excel.js'
@@ -146,6 +159,7 @@ export default {
   },
   data() {
     return {
+      areaKnoJson: null,
       emptyData: false,
       isLoading: false,
       keyword: '',
@@ -177,7 +191,7 @@ export default {
     topData() {
       let { scCount, classCount, stuCount, subjectCount } = this.curPeriodData.areaSchool
       let { classJoinCount, stuJoinCount } = this.curPeriodData
-      let totalStandard = (+(this.curPeriodData.knData.map(i => i.stand).reduce((a, b) => a + b, 0)) / 2).toFixed(2)
+      let totalStandard = parseInt(+(this.curPeriodData.knData.map(i => i.stand).reduce((a, b) => a + b, 0)) / 2)
       let joinCount = this.curPeriodData.periodAll ? this.curPeriodData.periodAll.schoolScore?.length : 0
       let topData = [
         {
@@ -318,6 +332,113 @@ export default {
     this.getAreaArtList()
   },
   methods: {
+    exportKnoData(subjectId) {
+      console.error(subjectId, this.areaKnoJson)
+      let sheets = []
+      // 区级概况
+      let area_knoHeaders = ['知识点名称', '知识块名称', '知识块配分', '知识块维度', '知识点得分率']
+      let area_knoKeys = ['pointName', 'blockName', 'blockScore', 'dim', 'pointScoreRate']
+      let subjectBlocks = this.areaKnoJson.blocks.find(i => i.subjectId === subjectId).dim
+      let area_knoData = this.areaKnoJson.areaSubjectPersent.find(i => i.subjectId === subjectId).psersent.map(item => {
+        let blockInfo = subjectBlocks.find(i => i.name === item.block[0])
+        return {
+          pointName: item.know,
+          blockName: item.block.length ? item.block[0] : '-',
+          blockScore: blockInfo ? blockInfo.score : 0,
+          dim: blockInfo ? blockInfo.dim[0] : '-',
+          pointScoreRate: parseInt(item.score * 100) + '%'
+        }
+      })
+      const areaSheet = {
+        title: area_knoHeaders,
+        key: area_knoKeys,
+        data: area_knoData,
+        filename: '区级概况',
+        autoWidth: true
+      }
+      sheets.push(areaSheet)
+      // 校级概况
+      let sch_knoHeaders = ['学校名称', '知识点名称', '知识块名称', '知识块配分', '知识块维度', '知识点得分率']
+      let sch_knoKeys = ['schoolName', 'pointName', 'blockName', 'blockScore', 'dim', 'pointScoreRate']
+      let sch_knoData = []
+      this.areaKnoJson.schoolSubjectPersent.forEach(school => {
+        school.subject.find(i => i.subjectId === subjectId).psersent.forEach(item => {
+          let blockInfo = subjectBlocks.find(i => i.name === item.block[0])
+          sch_knoData.push({
+            schoolName: school.schoolName,
+            pointName: item.know,
+            blockName: item.block.length ? item.block[0] : '-',
+            blockScore: blockInfo ? blockInfo.score : 0,
+            dim: blockInfo ? blockInfo.dim[0] : '-',
+            pointScoreRate: parseInt(item.score * 100) + '%'
+          })
+        })
+      })
+      const schSheet = {
+        title: sch_knoHeaders,
+        key: sch_knoKeys,
+        data: sch_knoData,
+        filename: '校级概况',
+        autoWidth: true
+      }
+      sheets.push(schSheet)
+      // 年级概况
+      let grade_knoHeaders = ['年级名称', '知识点名称', '知识块名称', '知识块配分', '知识块维度', '知识点得分率']
+      let grade_knoKeys = ['gradeName', 'pointName', 'blockName', 'blockScore', 'dim', 'pointScoreRate']
+      let grade_knoData = []
+      this.areaKnoJson.gradeSubjectPersent.find(i => i.subjectId === subjectId).psersent.forEach(grade => {
+        grade.knowledge.forEach(item => {
+          let blockInfo = subjectBlocks.find(i => i.name === item.block[0])
+          grade_knoData.push({
+            gradeName: grade.gradeName,
+            pointName: item.know,
+            blockName: item.block.length ? item.block[0] : '-',
+            blockScore: blockInfo ? blockInfo.score : 0,
+            dim: blockInfo ? blockInfo.dim[0] : '-',
+            pointScoreRate: parseInt(item.score * 100) + '%'
+          })
+        })
+      })
+      const gradeSheet = {
+        title: grade_knoHeaders,
+        key: grade_knoKeys,
+        data: grade_knoData,
+        filename: '年级概况',
+        autoWidth: true
+      }
+      sheets.push(gradeSheet)
+      // 班级概况
+      let class_knoHeaders = ['学校名称', '年级名称', '班级名称', '知识点名称', '知识块名称', '知识块配分', '知识块维度', '知识点得分率']
+      let class_knoKeys = ['schoolName', 'gradeName', 'className', 'pointName', 'blockName', 'blockScore', 'dim', 'pointScoreRate']
+      let class_knoData = []
+      this.areaKnoJson.classPersent.filter(i => i.subjectId === subjectId).forEach(classItem => {
+        classItem.know.forEach(item => {
+          let blockInfo = subjectBlocks.find(i => i.name === item.block[0])
+          class_knoData.push({
+            schoolName: classItem.schoolName,
+            gradeName: item.gradeName,
+            className: item.className,
+            pointName: item.knowledgeName,
+            blockName: item.block.length ? item.block[0] : '-',
+            blockScore: blockInfo ? blockInfo.score : 0,
+            dim: blockInfo ? blockInfo.dim[0] : '-',
+            pointScoreRate: parseInt(item.score * 100) + '%'
+          })
+        })
+      })
+      const classSheet = {
+        title: class_knoHeaders,
+        key: class_knoKeys,
+        data: class_knoData,
+        filename: '班级概况',
+        autoWidth: true
+      }
+      sheets.push(classSheet)
+      // 合并导出
+      let curPeriodName = this.periodList.find(i => i.value === this.periodId).label
+      let subjectName = subjectId === 'subject_music' ? '音乐' : '美术'
+      excel.export_array_to_sheet(sheets, `${sessionStorage.getItem('areaName')} - 知识点得分率报告(${curPeriodName}-${subjectName})`)
+    },
     exportArtTable() {
       let sheets = []
       // 区级概况
@@ -338,7 +459,7 @@ export default {
           m_min: mScore.min,
           m_average: mScore.average,
           m_excellent: parseInt(mScore.excellent * 100) + '%',
-          m_pass:  parseInt(mScore.pass * 100) + '%',
+          m_pass: parseInt(mScore.pass * 100) + '%',
           m_stan: this.curPeriodData.knData.find(i => i.key === 'subject_music').stand,
           p_max: pScore.max,
           p_min: pScore.min,
@@ -358,7 +479,7 @@ export default {
       sheets.push(areaSheet)
       // 各学校数据
       let schoolHeaders = ['学校名称', '音乐最高分', '音乐平均分', '音乐优秀率', '音乐合格率', '美术最高分', '美术平均分', '美术优秀率', '美术合格率']
-      let schoolKeys = ['name', 'm_max','m_average','m_excellent','m_pass', 'p_max',  'p_average', 'p_excellent', 'p_pass']
+      let schoolKeys = ['name', 'm_max', 'm_average', 'm_excellent', 'm_pass', 'p_max', 'p_average', 'p_excellent', 'p_pass']
       let schoolDatas = []
       this.allData.periodAll.schoolScore.forEach(school => {
         let sch_music_score = school.scores.find(i => i.subjectId === 'subject_music')
@@ -438,7 +559,7 @@ export default {
       console.log(this.periodList);
       console.log(this.periodId);
       let curPeriodName = this.periodList.find(i => i.value === this.periodId).label
-      excel.export_array_to_sheet(sheets, `${sessionStorage.getItem('areaName')} - 艺术评测报告(${ curPeriodName })`)
+      excel.export_array_to_sheet(sheets, `${sessionStorage.getItem('areaName')} - 艺术评测报告(${curPeriodName})`)
     },
     getAreaArtList() {
       this.$api.areaArt.findAreaArtList({
@@ -451,6 +572,13 @@ export default {
           this.onAcChange()
         }
       })
+      // 获取区级当前学段汇总知识点得分率数据
+      this.$api.areaArt.findKnoPercents({
+        id: sessionStorage.getItem('areaId'),
+        periodType: this.periodId,
+      }).then(res => {
+        this.areaKnoJson = res
+      })
     },
     onAcChange() {
       this.getAreaArtAnalysis()
@@ -597,12 +725,14 @@ export default {
   border-radius: 20px;
   // margin-right: 20px;
 }
+
 .school-img {
   width: 90px;
   height: 90px;
   margin-right: 20px;
   border-radius: 10px;
 }
+
 .school-data-item {
   position: relative;
   display: flex;
@@ -615,10 +745,12 @@ export default {
   min-width: 360px;
   border-radius: 5px;
   transition: all 0.2s ease 0s;
+
   &:hover {
     box-shadow: 0 26px 40px -24px #aaa;
     transform: translateY(-4px);
   }
+
   .school-name {
     // width: 158px;
     // text-overflow: ellipsis;
@@ -629,6 +761,7 @@ export default {
     // font-weight: 600;
     font-size: 16px;
   }
+
   .school-value {
     margin-top: 5px;
     font-size: 12px;
@@ -647,6 +780,7 @@ export default {
       font-size: 24px;
     }
   }
+
   .to-school-detail {
     margin-left: 8px;
     color: #2d8cf0;
@@ -654,6 +788,7 @@ export default {
     cursor: pointer;
   }
 }
+
 .school-data-wrap {
   margin-bottom: 20px;
   display: flex;
@@ -662,25 +797,31 @@ export default {
   margin-top: 20px;
   justify-content: start;
 }
+
 .award-chart-wrap {
   width: 49%;
 }
+
 .kng-point-wrap {
   width: ~"calc(100% - 420px)";
 }
+
 .chart-line-wrap {
   display: flex;
   justify-content: space-between;
 }
+
 .kng-level-wrap {
   width: fit-content;
   margin-right: 30px;
 }
+
 .chart-data-wrap {
   background: #ffffff;
   margin-top: 10px;
   padding: 40px 20px;
 }
+
 .block-title {
   border-left: 4px solid #1cc0f3;
   padding-left: 10px;
@@ -690,18 +831,30 @@ export default {
   color: #414749;
   margin-bottom: 20px;
 }
+
 .online-train-wrap {
+  position: relative;
   border-radius: 5px;
   width: 100%;
   // background: white;
   padding: 0px 20px;
   // box-shadow: 0px 4px 4px 1px #f0f0f0;
   margin-top: 35px;
+
+  .export-btn {
+    position: absolute;
+    right: 20px;
+    top: 10px;
+    cursor: pointer;
+  }
 }
+
+
 .border-style {
   box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.1);
   border-radius: 4px;
 }
+
 .content-con-item {
   width: 24%;
   height: 110px;
@@ -709,27 +862,32 @@ export default {
   border-radius: 2px;
   margin-bottom: 20px;
   overflow: hidden;
+
   .left-area {
     float: left;
     height: 100%;
     display: table;
     text-align: center;
+
     .icon {
       display: table-cell;
       vertical-align: middle;
     }
   }
+
   .right-area {
     background: white;
     float: left;
     height: 100%;
     display: table;
     text-align: center;
+
     .count-style {
       font-size: 50px;
     }
   }
 }
+
 .top-block-wrap {
   width: 100%;
   display: flex;
@@ -738,13 +896,14 @@ export default {
   margin-top: 10px;
   padding: 0px 20px 0px 20px;
 }
+
 .area-data-container {
   position: relative;
   width: 100%;
   height: 100%;
   background: #ededed;
 
-  .export-box{
+  .export-box {
     position: absolute;
     right: 20px;
     top: 20px;

+ 137 - 37
TEAMModelOS/ClientApp/src/view/evaluation/bank/TestPaperList.vue

@@ -1,9 +1,11 @@
 <template>
   <div class="pl-container" ref="plContainer">
     <!-- 条件筛选部分 -->
-    <BaseFilter ref="baseFilter" @onChange="onFilterChange" :filterCounts="filterCounts" :isFilterPaper="isFilterPaper" v-show="!isPreview && !isComponent"></BaseFilter>
+    <BaseFilter ref="baseFilter" @onChange="onFilterChange" :filterCounts="filterCounts" :isFilterPaper="isFilterPaper"
+      v-show="!isPreview && !isComponent"></BaseFilter>
 
-    <div class="bank-action-bar light-iview-select light-iview-input" v-show="!isPreview" :style="{ margin:isComponent ? '0 0 10px 0' : '10px 0' }">
+    <div class="bank-action-bar light-iview-select light-iview-input" v-show="!isPreview"
+      :style="{ margin: isComponent ? '0 0 10px 0' : '10px 0' }">
       <!-- 试卷列表的搜索功能 -->
       <div class="action-tools">
         <div class="action-tool">
@@ -13,29 +15,33 @@
           </Select>
         </div>
         <div class="action-tool">
-          <Input v-special-char suffix="ios-search" v-model="searchVal" clearable :placeholder="$t('evaluation.paperList.searchPaper')" style="width: 300px" @on-click="onCloseSearch" @on-change="onSearchChange" />
+          <Input v-special-char suffix="ios-search" v-model="searchVal" clearable
+            :placeholder="$t('evaluation.paperList.searchPaper')" style="width: 300px" @on-click="onCloseSearch"
+            @on-change="onSearchChange" />
         </div>
       </div>
       <div class="action-tools">
-        <span>{{ $t('evaluation.exerciseList.totalTip1') }}<span style="font-size: 18px;color: #ff0206;margin: 0 10px;font-weight: bold;">{{ totalNum }}</span>{{ $t('unit.text3') }}</span>
+        <span>{{ $t('evaluation.exerciseList.totalTip1') }}<span
+            style="font-size: 18px;color: #ff0206;margin: 0 10px;font-weight: bold;">{{ totalNum }}</span>{{
+              $t('unit.text3') }}</span>
       </div>
     </div>
 
     <!-- 空数据展示 -->
     <div v-if="paperList.length === 0" class="no-data-text">
       <img src="@/assets/icon/no_data.svg" width="120" />
-      <span style="margin-top:15px;color:#808080">{{$t('evaluation.noData')}}</span>
+      <span style="margin-top:15px;color:#808080">{{ $t('evaluation.noData') }}</span>
     </div>
 
     <!-- 试卷列表页面 -->
     <div class="pl-content-wrap" v-else>
       <Loading :top="100" v-show="dataLoading" type="1" hideMask></Loading>
       <div style="width: 100%;" v-if="!isPreview">
-        <div class="paper-item" v-for="(paper,index) in paperList" :key="index" @click="onPreviewPaper(paper)">
+        <div class="paper-item" v-for="(paper, index) in paperList" :key="index" @click="onPreviewPaper(paper)">
           <div class="paper-item-name">
             <span class="paper-item-tag" v-if="isSchool">{{ paper.subjectName }}</span>
             <span style="margin-left: 8px;">{{ paper.name }}</span>
-            <span style="margin-left: 8px;" v-for="(tag,tagIndex) in paper.tags">
+            <span style="margin-left: 8px;" v-for="(tag, tagIndex) in paper.tags">
               <Tag color="geekblue" style="vertical-align: sub;">{{ tag }}</Tag>
             </span>
             <span style="margin-left: 8px;" v-if="paper.mode">
@@ -43,12 +49,17 @@
             </span>
           </div>
           <div class="paper-item-info">
-            <span class="info-item" v-if="isSchool">{{$t('evaluation.paperList.usePeriod')}}:<span class="info-bold">{{ getPeriodName(paper.periodId) }}</span></span>
-            <span class="info-item" v-if="isSchool">{{$t('evaluation.paperList.useGrade')}}:<span class="info-bold" v-for="(grade,gIndex) in paper.gradeIds" :key="gIndex">{{ getGradeName(paper.periodId,grade) }}
+            <span class="info-item" v-if="isSchool">{{ $t('evaluation.paperList.usePeriod') }}:<span class="info-bold">{{
+              getPeriodName(paper.periodId) }}</span></span>
+            <span class="info-item" v-if="isSchool">{{ $t('evaluation.paperList.useGrade') }}:<span class="info-bold"
+                v-for="(grade, gIndex) in paper.gradeIds" :key="gIndex">{{ getGradeName(paper.periodId, grade) }}
                 <span v-show="gIndex !== paper.gradeIds.length - 1"> / </span></span></span>
-            <span class="info-item">{{$t('evaluation.paperList.itemCount')}}:<span class="info-bold">{{ paper.scoring ? paper.scoring.length : 0 }}</span></span>
-            <span class="info-item">{{$t('evaluation.paperList.sortType')}}:<span class="info-bold">{{ paper.itemSort === 1 ? $t('evaluation.paperList.sortByOrder') : $t('evaluation.paperList.sortByType') }}</span></span>
-            <span class="info-item">{{$t('evaluation.updateTime')}}:<span class="info-bold">{{ $tools.formatTime(paper.createTime)  || 0 }} </span></span>
+            <span class="info-item">{{ $t('evaluation.paperList.itemCount') }}:<span class="info-bold">{{ paper.scoring ?
+              paper.scoring.length : 0 }}</span></span>
+            <span class="info-item">{{ $t('evaluation.paperList.sortType') }}:<span class="info-bold">{{ paper.itemSort ===
+              1 ? $t('evaluation.paperList.sortByOrder') : $t('evaluation.paperList.sortByType') }}</span></span>
+            <span class="info-item">{{ $t('evaluation.updateTime') }}:<span class="info-bold">{{
+              $tools.formatTime(paper.createTime) || 0 }} </span></span>
             <!-- <span class="info-item">
 							<span>标签:</span>
 							<span class="info-bold" v-for="(tag,tagIndex) in paper.tags"><Tag color="blue">{{ tag }}</Tag></span>
@@ -59,46 +70,54 @@
               <Icon type="md-download" />
               <span>{{ $t('syllabus.download') }}</span>
             </span>
-            <!-- <span class="paper-item-tools-edit" @click.stop="renderAnswerSheet(paper)">
-							<Icon type="md-eye" />
-							<span>预览答题卡</span>
-						</span> -->
-            <span class="paper-item-tools-edit" @click.stop="goToPaper(paper)" v-if="($access.can('admin.*||exercise-upd') || !isSchool)">
+            <span class="paper-item-tools-edit" @click.stop="doCopyPaper(paper)">
+              <Icon type="md-copy" />
+              <span>创建副本</span>
+            </span>
+            <span class="paper-item-tools-edit" @click.stop="goToPaper(paper)"
+              v-if="($access.can('admin.*||exercise-upd') || !isSchool)">
               <Icon type="md-create" />
-              <span>{{$t('evaluation.editItem')}}</span>
+              <span>{{ $t('evaluation.editItem') }}</span>
             </span>
-            <span class="paper-item-tools-delete" @click.stop="onDeletePaper(paper)" v-if="($access.can('admin.*||exercise-upd') || !isSchool)">
+            <span class="paper-item-tools-delete" @click.stop="onDeletePaper(paper)"
+              v-if="($access.can('admin.*||exercise-upd') || !isSchool)">
               <Icon type="md-trash" />
-              <span>{{$t('evaluation.deleteItem')}}</span>
+              <span>{{ $t('evaluation.deleteItem') }}</span>
             </span>
           </div>
           <div v-if="chooseModel" class="paper-item-select">
             <div v-if="!singleChoose">
-              <Button :type="checkedPaperList.map(i => i.id).includes(paper.id) ? 'warning' : 'info'" @click.stop="checkedPaperList.map(i => i.id).includes(paper.id) ? onCancelCheck(paper) : onCheckPaper(paper)">
-                {{ checkedPaperList.map(i => i.id).includes(paper.id) ? $t('evaluation.paperList.choosed') : $t('evaluation.paperList.choosePaper')}}
+              <Button :type="checkedPaperList.map(i => i.id).includes(paper.id) ? 'warning' : 'info'"
+                @click.stop="checkedPaperList.map(i => i.id).includes(paper.id) ? onCancelCheck(paper) : onCheckPaper(paper)">
+                {{ checkedPaperList.map(i => i.id).includes(paper.id) ? $t('evaluation.paperList.choosed') :
+                  $t('evaluation.paperList.choosePaper') }}
               </Button>
             </div>
             <div v-else>
-              <Button :type="selectPaperId === paper.id ? 'warning' : 'info'" @click.stop="selectPaperId === paper.id ? onCheckPaper(paper) : onCheckPaper(paper)">{{ selectPaperId === paper.id ? $t('evaluation.paperList.choosed') : $t('evaluation.paperList.choosePaper') }}</Button>
+              <Button :type="selectPaperId === paper.id ? 'warning' : 'info'"
+                @click.stop="selectPaperId === paper.id ? onCheckPaper(paper) : onCheckPaper(paper)">{{ selectPaperId ===
+                  paper.id ? $t('evaluation.paperList.choosed') : $t('evaluation.paperList.choosePaper') }}</Button>
             </div>
           </div>
         </div>
 
         <!-- 底部分页区域 -->
-        <Page :total="totalNum" show-sizer show-total :page-size="pageSize" :current="pageNum" @on-page-size-change="pageSizeChange" @on-change="pageChange" :page-size-opts="[10,20,30,40]" />
+        <Page :total="totalNum" show-sizer show-total :page-size="pageSize" :current="pageNum"
+          @on-page-size-change="pageSizeChange" @on-change="pageChange" :page-size-opts="[10, 20, 30, 40]" />
       </div>
 
       <!-- 预览试卷部分 -->
       <div class="pl-review-wrap animated fadeIn" v-if="isPreview">
         <div class="pl-review-wrap-left" :style="{ width: isComponent ? '100%' : '75%' }">
-          <TestPaper :paper="evaluationInfo" isExamPaper hidePie :isSharePreview="isComponent" @exitPreview="exitPreview"></TestPaper>
+          <TestPaper :paper="evaluationInfo" isExamPaper hidePie :isSharePreview="isComponent" @exitPreview="exitPreview">
+          </TestPaper>
         </div>
 
         <div class="pl-review-wrap-right" v-if="!isComponent">
-          <h2>{{$t('evaluation.paperList.paperAnalysis')}}</h2>
+          <h2>{{ $t('evaluation.paperList.paperAnalysis') }}</h2>
           <p style="margin-bottom: 20px;margin-top: 10px;">
-            ({{$t('evaluation.paperList.totalScore')}}:{{ evaluationInfo.score }}
-            {{$t('evaluation.paperList.score')}})
+            ({{ $t('evaluation.paperList.totalScore') }}:{{ evaluationInfo.score }}
+            {{ $t('evaluation.paperList.score') }})
           </p>
           <BaseTypePie :echartsData="evaluationInfo"></BaseTypePie>
           <BaseObjectivePie :echartsData="evaluationInfo"></BaseObjectivePie>
@@ -130,6 +149,7 @@
   </div>
 </template>
 <script>
+import blobTool from '@/utils/blobTool.js'
 import Loading from '@/common/Loading.vue'
 import BaseFilter from '../components/BaseFilter'
 import BaseImport from '../components/BaseImport'
@@ -157,6 +177,7 @@ export default {
   },
   data() {
     return {
+      containerClient: null,
       selectPaperId: '',
       curPaper: null,
       selectTags: [],
@@ -164,7 +185,6 @@ export default {
       searchVal: '',
       checkedPaperList: [],
       isShowSheet: false,
-      containerClient: null,
       schoolCode: '',
       totalNum: 0,
       pageSize: 10,
@@ -449,6 +469,91 @@ export default {
       }
     },
 
+    /* 创建副本 */
+    doCopyPaper(paper) {
+      console.log(paper);
+      this.$Modal.confirm({
+        title: this.$t('evaluation.newExercise.modalTip'),
+        content: `${this.$t('evaluation.paperList.copyTip1')}【${paper.name}】${this.$t('evaluation.paperList.copyTip2')}?`,
+        onOk: async () => {
+          this.dataLoading = true
+          try {
+            let isSchool = this.$route.name === "schoolBank"
+            // 获取初始化Blob需要的数据
+            let sasData = isSchool ? await this.$tools.getSchoolSas() : await this.$tools.getPrivateSas()
+            //初始化Blob
+            this.containerClient = new blobTool(sasData.url, sasData.name, sasData.sas, this.isSchool ? 'school' : 'private')
+            // 获取新试卷名称
+            let newPaperName = await this.getCopyPaperName(paper.name)
+            // 获取新试卷新ID
+            let newPaperId = this.$tools.guid()
+            // 第一步 - 生成新试卷基础信息
+            let newPaperItem = this._.cloneDeep(paper)
+            newPaperItem.id = newPaperId
+            newPaperItem.name = newPaperName
+            newPaperItem.sheet = null
+            newPaperItem.sheetNo = null
+            newPaperItem.blob = paper.blob.replaceAll(paper.name, newPaperName)
+            newPaperItem.code = paper.code.replaceAll('Paper-', '')
+            console.log(newPaperItem)
+            // 第二步 - 复制BLOB试卷Folder
+            this.containerClient.copyFolder('paper/' + newPaperName + '/', 'paper/' + paper.name, this.containerClient, false).then(async res => {
+              console.error(res)
+              // 第三步 - 要修改index.json基础ID信息
+              let fullPath = this.$evTools.getBlobHost() + '/' + paper.code.replaceAll('Paper-', '') + paper.blob + '/index.json' + sasData.sas
+              let jsonInfo = await this.$tools.getFile(fullPath)
+              let indexJsonData = JSON.parse(jsonInfo)
+              indexJsonData.id = newPaperId
+              indexJsonData.name = newPaperName
+              let indexJsonFile = new File([JSON.stringify(indexJsonData)], "index.json");
+              this.containerClient.upload(indexJsonFile, {
+                path: 'paper/' + newPaperName,
+                checkSize: false
+              }).then(result => {
+                this.$api.learnActivity.SaveExamPaper({
+                  paper: newPaperItem,
+                  option: 'insert'
+                }).then(insertRes => {
+                  console.error(insertRes)
+                  if (!insertRes.error) {
+                    this.$Message.success(this.$t('syllabus.doSuc'))
+                    this.doFilter()
+                  } else {
+                    this.$Message.success(this.$t('syllabus.doFail'))
+                  }
+                  this.dataLoading = false
+                })
+              })
+            })
+          } catch (error) {
+            this.$Message.success(this.$t('syllabus.doFail'))
+            this.dataLoading = false
+          }
+        }
+      })
+    },
+
+    /* 获取可用的新试卷名称 */
+    async getCopyPaperName(originPaperName) {
+      return new Promise(async (r, j) => {
+        let copyNum = 1
+        let newPaperName = originPaperName + '(' + copyNum + ')'
+        let checkName = async name => {
+          let isExist = await this.containerClient.exists('paper/' + name + '/index.json')
+          if (isExist) {
+            copyNum++
+            newPaperName = originPaperName + '(' + copyNum + ')'
+            checkName(newPaperName)
+          } else {
+            r(newPaperName)
+          }
+        }
+        checkName(newPaperName)
+      })
+
+
+    },
+
     /* 下载试卷 */
     async goToDownload(paper) {
       let fullPaperJson = this.fullPaperJson.id === paper.id ? this.fullPaperJson : await this.$evTools
@@ -606,8 +711,7 @@ export default {
   }
 }
 </script>
-<style src="./TestPaperList.less" lang="less" scoped>
-</style>
+<style src="./TestPaperList.less" lang="less" scoped></style>
 
 <style>
 .preview-modal .ivu-modal-body {
@@ -630,10 +734,7 @@ export default {
   display: none !important;
 }
 
-.random-pick-modal
-  .question-condition-wrap
-  .question-condition-item
-  .condition-label,
+.random-pick-modal .question-condition-wrap .question-condition-item .condition-label,
 .random-pick-modal .question-condition-wrap .question-condition-item {
   color: #000;
   font-size: 14px;
@@ -680,5 +781,4 @@ export default {
 
 .random-pick-modal .ivu-tag .ivu-icon-ios-close {
   color: #515a6e !important;
-}
-</style>
+}</style>

+ 48 - 33
TEAMModelOS/ClientApp/src/view/evaluation/index/CreatePaper.vue

@@ -2,60 +2,71 @@
   <div class="create-evaluation-container custom-iview-split">
     <div class="create-header">
       <p class="create-header-title">
-        {{ isEditPaper ? $t('evaluation.paperList.editPaper') : evaluationInfo.createType === 'auto' ? $t('evaluation.index.autoCreate') : evaluationInfo.createType === 'import' ? (isXkwMode ? $t('evaluation.xkwMode') : $t('evaluation.importItems'))  : $t('evaluation.index.manualCreate') }}
-        <Checkbox v-model="isSaveToBank" v-if="evaluationInfo.createType === 'import' && !isXkwMode" style="margin-left: 20px;" @on-change="onModeChange('item')"> {{ $t('evaluation.syncItems') }}
+        {{ isEditPaper ? $t('evaluation.paperList.editPaper') : evaluationInfo.createType === 'auto' ?
+          $t('evaluation.index.autoCreate') : evaluationInfo.createType === 'import' ? (isXkwMode ? $t('evaluation.xkwMode')
+            : $t('evaluation.importItems')) : $t('evaluation.index.manualCreate') }}
+        <Checkbox v-model="isSaveToBank" v-if="evaluationInfo.createType === 'import' && !isXkwMode"
+          style="margin-left: 20px;" @on-change="onModeChange('item')"> {{ $t('evaluation.syncItems') }}
         </Checkbox>
-        <Checkbox v-model="isGeneratePaper" v-if="evaluationInfo.createType === 'import'" style="margin-left: 20px;" @on-change="onModeChange('paper')"> {{ $t('evaluation.composePaper') }}
+        <Checkbox v-model="isGeneratePaper" v-if="evaluationInfo.createType === 'import'" style="margin-left: 20px;"
+          @on-change="onModeChange('paper')"> {{ $t('evaluation.composePaper') }}
         </Checkbox>
       </p>
       <div style="float: right;" class="common-save-btn">
-        <Button class="btn-save" type="text" icon="md-arrow-round-back" @click="goBack">{{$t('evaluation.paperList.backToBank')}}</Button>
-        <Button class="btn-save" type="text" icon="md-folder" :loading="isLoading" @click="isGeneratePaper ? saveTestPaper() : justSaveItems()">{{ isLoading ? $t('evaluation.paperList.isSaving') : isGeneratePaper ? $t('evaluation.paperList.savePaper') : $t('evaluation.newExercise.save')}}</Button>
+        <Button class="btn-save" type="text" icon="md-arrow-round-back"
+          @click="goBack">{{ $t('evaluation.paperList.backToBank') }}</Button>
+        <Button class="btn-save" type="text" icon="md-folder" :loading="isLoading"
+          @click="isGeneratePaper ? saveTestPaper() : justSaveItems()">{{ isLoading ? $t('evaluation.paperList.isSaving')
+            : isGeneratePaper ? $t('evaluation.paperList.savePaper') : $t('evaluation.newExercise.save') }}</Button>
       </div>
     </div>
     <div class="create-body">
       <Split v-model="split1">
         <div slot="left" class="demo-split-pane animated fadeIn" v-show="(isGeneratePaper && !isSchool) || isSchool">
           <div class="evaluation-attr-wrap">
-            <p class="wrap-label">{{$t('evaluation.paperList.baseInfo')}}</p>
+            <p class="wrap-label">{{ $t('evaluation.paperList.baseInfo') }}</p>
             <div style="width:100%; height:calc(100% - 45px);padding-top:30px;">
-              <Form ref="evaluationInfo" :model="evaluationInfo" label-position="top" class="evaluation-attr-form light-iview-input light-iview-select light-iview-input-number" label-colon>
+              <Form ref="evaluationInfo" :model="evaluationInfo" label-position="top"
+                class="evaluation-attr-form light-iview-input light-iview-select light-iview-input-number" label-colon>
                 <FormItem :label="$t('evaluation.paperList.paperType')" prop="type" style="display: none;">
                   <Select v-model="evaluationInfo.type" disabled>
-                    <Option value="private">{{$t('evaluation.paperList.praviteBank')}}</Option>
-                    <Option value="school">{{$t('evaluation.paperList.schoolBank')}}</Option>
+                    <Option value="private">{{ $t('evaluation.paperList.praviteBank') }}</Option>
+                    <Option value="school">{{ $t('evaluation.paperList.schoolBank') }}</Option>
                   </Select>
                 </FormItem>
                 <FormItem :label="$t('evaluation.paperList.paperName')" prop="name" v-show="isGeneratePaper">
                   <Input v-special-char v-model="evaluationInfo.name"></Input>
                 </FormItem>
                 <FormItem :label="$t('evaluation.paperTag')" v-show="isGeneratePaper">
-                  <Select v-model="evaluationInfo.tags" filterable multiple allow-create :placeholder="$t('evaluation.paperTagPlace')" @on-create="createTag">
+                  <Select v-model="evaluationInfo.tags" filterable multiple allow-create
+                    :placeholder="$t('evaluation.paperTagPlace')" @on-create="createTag">
                     <Option v-for="item in tags" :value="item" :key="item">{{ item }}</Option>
                   </Select>
                 </FormItem>
                 <FormItem :label="$t('evaluation.newExercise.choosePeriod')" prop="paperPeriod" v-if="isSchool">
                   <Select v-model="evaluationInfo.paperPeriod" @on-change="onPeriodChange">
-                    <Option v-for="(period,index) in schoolInfo.period" :value="index" :key="index">
+                    <Option v-for="(period, index) in schoolInfo.period" :value="index" :key="index">
                       {{ period.name }}
                     </Option>
                   </Select>
                 </FormItem>
                 <FormItem :label="$t('evaluation.newExercise.chooseGrade')" prop="publish" v-if="isSchool">
-                  <Select v-model="evaluationInfo.paperGrade" multiple :placeholder="$t('evaluation.newExercise.gradePlaceholder')">
-                    <Option v-for="(grade,index) in gradeList" :value="index" :key="grade">
+                  <Select v-model="evaluationInfo.paperGrade" multiple
+                    :placeholder="$t('evaluation.newExercise.gradePlaceholder')">
+                    <Option v-for="(grade, index) in gradeList" :value="index" :key="grade">
                       {{ grade }}
                     </Option>
                   </Select>
                 </FormItem>
                 <FormItem :label="$t('evaluation.newExercise.chooseSubject')" v-if="isSchool">
                   <Select v-model="evaluationInfo.paperSubject">
-                    <Option v-for="(subject,index) in subjectList" :value="index" :key="index">
+                    <Option v-for="(subject, index) in subjectList" :value="index" :key="index">
                       {{ subject.name }}
                     </Option>
                   </Select>
                 </FormItem>
-                <FormItem :label="$t('evaluation.paperList.paperScore')" prop="name" class="evaluation-attr-wrap-inputNumber" v-show="isGeneratePaper">
+                <FormItem :label="$t('evaluation.paperList.paperScore')" prop="name"
+                  class="evaluation-attr-wrap-inputNumber" v-show="isGeneratePaper">
                   <InputNumber :max="1000" :min="1" v-model="evaluationInfo.score"></InputNumber>
                 </FormItem>
               </Form>
@@ -66,27 +77,38 @@
           <div class="evaluation-question-wrap">
             <div class="evaluation-question-main">
               <p class="save-tips">
-                <span>*{{ viewModel === 'list' ? $t('evaluation.paperList.saveRule1') : $t('evaluation.paperList.saveRule2') }}</span>
-                <Checkbox v-model="isSavePoints" v-if="evaluationInfo.createType === 'import' && isSchool" style="margin-left: 20px;">{{ $t('evaluation.syncPoints') }}</Checkbox>
+                <span>*{{ viewModel === 'list' ? $t('evaluation.paperList.saveRule1') :
+                  $t('evaluation.paperList.saveRule2') }}</span>
+                <Checkbox v-model="isSavePoints" v-if="evaluationInfo.createType === 'import' && isSchool"
+                  style="margin-left: 20px;">{{ $t('evaluation.syncPoints') }}</Checkbox>
               </p>
               <Tabs v-model="activeTab" type="card" class="question-main-tabs" name="createTest">
                 <!-- 自动组卷 -->
-                <TabPane :label="$t('evaluation.paperList.tab1')" name="auto" v-if="evaluationInfo.createType == 'auto'" :index="1" tab="createTest">
-                  <AutoCreate :subject="propSubject" :period="propPeriod" @goToPreview="goToPreview" @autoQuestions="getAutoQuestions"></AutoCreate>
+                <TabPane :label="$t('evaluation.paperList.tab1')" name="auto" v-if="evaluationInfo.createType == 'auto'"
+                  :index="1" tab="createTest">
+                  <AutoCreate :subject="propSubject" :period="propPeriod" @goToPreview="goToPreview"
+                    @autoQuestions="getAutoQuestions"></AutoCreate>
                 </TabPane>
                 <!-- 手动挑题 -->
-                <TabPane :label="$t('evaluation.paperList.tab2')" name="manual" v-if="evaluationInfo.createType == 'manual'" :index="2" tab="createTest">
-                  <ManualCreate :questionList="questionList" :subjectCode="propSubject" :periodCode="propPeriod" :gradeCode="propGrades" :selQue="evaluationInfo.item" @goToPreview="goToPreview" @selectedQuestion="getSelectedQuestion">
+                <TabPane :label="$t('evaluation.paperList.tab2')" name="manual"
+                  v-if="evaluationInfo.createType == 'manual'" :index="2" tab="createTest">
+                  <ManualCreate :questionList="questionList" :subjectCode="propSubject" :periodCode="propPeriod"
+                    :gradeCode="propGrades" :selQue="evaluationInfo.item" @goToPreview="goToPreview"
+                    @selectedQuestion="getSelectedQuestion">
                   </ManualCreate>
                 </TabPane>
                 <!-- 导题组卷 -->
-                <TabPane :label="evaluationInfo.item.length ? $t('evaluation.paperList.tab3') : $t('evaluation.paperList.tab4')" name="import" v-if="evaluationInfo.createType == 'import' && !isXkwMode" :index="3" tab="createTest">
+                <TabPane
+                  :label="evaluationInfo.item.length ? $t('evaluation.paperList.tab3') : $t('evaluation.paperList.tab4')"
+                  name="import" v-if="evaluationInfo.createType == 'import' && !isXkwMode" :index="3" tab="createTest">
                   <BaseImport @importFinish="onImportFinish"></BaseImport>
                 </TabPane>
                 <!-- 预览试卷 -->
                 <TabPane :label="$t('evaluation.paperList.tab5')" name="preview" :index="4" tab="createTest">
                   <vuescroll ref="paperRef" @handle-scroll="handleScroll">
-                    <TestPaper v-if="!examAnalysisStatus" :subjectCode="propSubject" :periodCode="propPeriod" :gradeCode="propGrades" :paper="evaluationInfo" :class="examAnalysisStatus ? '':'animated fadeIn'" ref="testPaper" @onViewModelChange="onViewModelChange" :isPreviewItems="!isGeneratePaper">
+                    <TestPaper v-if="!examAnalysisStatus" :subjectCode="propSubject" :periodCode="propPeriod"
+                      :gradeCode="propGrades" :paper="evaluationInfo" :class="examAnalysisStatus ? '' : 'animated fadeIn'"
+                      ref="testPaper" @onViewModelChange="onViewModelChange" :isPreviewItems="!isGeneratePaper">
                     </TestPaper>
                   </vuescroll>
                 </TabPane>
@@ -1731,20 +1753,14 @@ export default {
   color: #fff;
 }
 
-.evaluation-question-main
-  .ivu-tabs.ivu-tabs-card
-  > .ivu-tabs-bar
-  .ivu-tabs-tab {
+.evaluation-question-main .ivu-tabs.ivu-tabs-card>.ivu-tabs-bar .ivu-tabs-tab {
   /* border: none; */
   /* background-color: #303030; */
   /* color: white; */
   margin-right: 2px;
 }
 
-.evaluation-question-main
-  .ivu-tabs.ivu-tabs-card
-  > .ivu-tabs-bar
-  .ivu-tabs-tab-active {
+.evaluation-question-main .ivu-tabs.ivu-tabs-card>.ivu-tabs-bar .ivu-tabs-tab-active {
   /* background-color: #404040; */
   /* color: white; */
   font-weight: 600;
@@ -1767,5 +1783,4 @@ export default {
 
 .question-main-tabs .ivu-badge-count-primary {
   background-color: #05c19c;
-}
-</style>
+}</style>

+ 1 - 1
TEAMModelOS/ClientApp/src/view/login/Index.vue

@@ -242,7 +242,7 @@
     border-right: 1px solid #616161;
   }
   .login-box-cont {
-    width: 80%;
+    width: 84%;
     float: left;
     display: flex;
     flex-direction: row;

+ 18 - 1
TEAMModelOS/Controllers/Both/PaperController.cs

@@ -276,7 +276,7 @@ namespace TEAMModelOS.Controllers
                paper = await client.GetContainer(Constant.TEAMModelOS, tbname).UpsertItemAsync(paper, new PartitionKey($"{paper.code}"));
 
             }
-            else
+            else  
             {
                 if (paper.scope.Equals("private"))
                 {
@@ -287,6 +287,23 @@ namespace TEAMModelOS.Controllers
                     paper = await client.GetContainer(Constant.TEAMModelOS, "School").ReplaceItemAsync(paper, paper.id, new PartitionKey($"{paper.code}"));
                 }
             }
+            //else if (option.ToString().Equals("copy"))
+            //{
+            //    if (!request.TryGetProperty("newId", out JsonElement _newId)) return BadRequest();
+
+            //    paper.id = _newId.GetString();
+            //    var now = DateTimeOffset.UtcNow;
+            //    paper.name = $"{paper.name}_{now.ToString("mmss")}";
+            //    paper.blob
+            //    if (paper.scope.Equals("private"))
+            //    {
+            //        paper = await client.GetContainer(Constant.TEAMModelOS, "Teacher").ReplaceItemAsync(paper, paper.id, new PartitionKey($"{paper.code}"));
+            //    }
+            //    else
+            //    {
+            //        paper = await client.GetContainer(Constant.TEAMModelOS, "School").ReplaceItemAsync(paper, paper.id, new PartitionKey($"{paper.code}"));
+            //    }
+            //}
             return Ok(new { paper });
         }
 

+ 32 - 0
TEAMModelOS/Filter/AspNetCoreBuilderServiceCollectionExtensions.cs

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

+ 82 - 0
TEAMModelOS/Filter/BlobLoggerProvider.cs

@@ -0,0 +1,82 @@
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using System.IO;
+using System.Text;
+using System;
+using TEAMModelOS.SDK.DI;
+using Azure;
+using Azure.Storage.Blobs.Specialized;
+using System.Threading.Tasks;
+
+namespace TEAMModelOS.Filter
+{
+    public class BlobLoggerProvider: ILoggerProvider
+    {
+        private readonly AzureStorageFactory _storageFactory;
+
+        public BlobLoggerProvider(AzureStorageFactory storageFactory)
+        {
+            _storageFactory=storageFactory;
+            var container = _storageFactory.GetBlobContainerClient("0-service-log");
+            container.CreateIfNotExists(PublicAccessType.None); //嘗試創建School容器,如存在則不做任何事,保障容器一定存在
+        }
+
+        public ILogger CreateLogger(string categoryName)
+        {
+            return new BlobLogger(categoryName, _storageFactory.GetBlobContainerClient("0-service-log"));
+        }
+
+        public void Dispose()
+        {
+            // Dispose any resources used by the provider.
+        }
+    }
+
+    public class BlobLogger : ILogger
+    {
+        private readonly string _categoryName;
+        private readonly BlobContainerClient _containerClient;
+
+        public BlobLogger(string categoryName, BlobContainerClient containerClient)
+        {
+            _categoryName = categoryName;
+            _containerClient = containerClient;
+        }
+
+        public IDisposable BeginScope<TState>(TState state)
+        {
+            return null;
+        }
+
+        public bool IsEnabled(LogLevel logLevel)
+        {
+            return true;
+        }
+
+        public async void Log<TState> ( LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
+        {
+            if (formatter == null)
+            {
+                throw new ArgumentNullException(nameof(formatter));
+            }
+            if (_categoryName.Equals("TEAMModelOS.Filter.RequestAuditFilter", StringComparison.OrdinalIgnoreCase)) {
+                var message = formatter(state, exception);
+                var appendBlob = _containerClient.GetAppendBlobClient($"{_categoryName}/{DateTimeOffset.UtcNow:yyyy-MM-dd}.log");
+                // var blobClient = _containerClient.GetBlobClient($"{_categoryName}/{DateTimeOffset.UtcNow:yyyy-MM-dd}.log");
+                if (!appendBlob.Exists())
+                {
+                    await appendBlob.CreateAsync();
+                    using var stream = new MemoryStream(Encoding.UTF8.GetBytes(message));
+                    await appendBlob.AppendBlockAsync(stream);
+                }
+                else
+                {
+                    using var stream = new MemoryStream(Encoding.UTF8.GetBytes($"\n,{message}"));
+                    await appendBlob.AppendBlockAsync(stream);
+                }
+            }
+        }
+    }
+}

+ 82 - 0
TEAMModelOS/Filter/RequestAuditFilter.cs

@@ -0,0 +1,82 @@
+using Microsoft.AspNetCore.Mvc.Controllers;
+using Microsoft.AspNetCore.Mvc.Filters;
+using System.Security.Claims;
+using System;
+using System.Threading.Tasks;
+using TEAMModelOS.SDK.Extension;
+using Microsoft.Extensions.Logging;
+using TEAMModelOS.SDK;
+using DocumentFormat.OpenXml.Office2010.Excel;
+using DocumentFormat.OpenXml.Wordprocessing;
+using System.IdentityModel.Tokens.Jwt;
+using System.Linq;
+using Azure.Core;
+using DocumentFormat.OpenXml.Office2016.Excel;
+
+namespace TEAMModelOS.Filter
+{
+    public class RequestAuditFilter : IAsyncActionFilter
+    {
+        private readonly ILogger _logger;
+      
+        public RequestAuditFilter(ILoggerFactory loggerFactory)
+        {
+            _logger = loggerFactory.CreateLogger<RequestAuditFilter>();
+        }
+        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+        {
+            //============== 这里是执行方法之前获取数据 ====================
+
+            // 获取控制器、路由信息
+            var actionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
+
+            // 获取请求的方法
+            var method = actionDescriptor.MethodInfo;
+
+            // 获取 HttpContext 和 HttpRequest 对象
+            var httpContext = context.HttpContext;
+            var httpRequest = httpContext.Request;
+
+            // 获取客户端 Ipv4 地址
+            var remoteIPv4 = httpContext.GetRemoteIpAddressToIPv4();
+
+            // 获取请求的 Url 地址
+           // var requestUrl = httpRequest.GetRequestUrlAddress();
+
+            
+            // 获取来源 Url 地址
+           //var refererUrl = httpRequest.GetRefererUrlAddress();
+
+            // 获取请求参数(写入日志,需序列化成字符串后存储)
+            var parameters = context.ActionArguments;
+
+            // 获取操作人(必须授权访问才有值)"userId" 为你存储的 claims type,jwt 授权对应的是 payload 中存储的键名
+            var userId = httpContext.User?.FindFirstValue("userId");
+            var authtoken = context.HttpContext.GetXAuth("AuthToken");
+            string id = string.Empty, name = string.Empty, picture = string.Empty, school = string.Empty;
+            if (!string.IsNullOrWhiteSpace(authtoken)) {
+                var jwt = new JwtSecurityTokenHandler().ReadJwtToken(authtoken);
+                id = jwt.Payload.Sub;
+                school = jwt.Payload.Azp;
+                name = jwt.Claims.FirstOrDefault(claim => claim.Type.Equals("name"))?.Value;
+            }
+            // 请求时间
+            var requestedTime = DateTimeOffset.Now.ToUnixTimeMilliseconds();
+
+            //============== 这里是执行方法之后获取数据 ====================
+            var actionContext = await next();
+
+            // 获取返回的结果
+            var returnResult = actionContext.Result;
+
+            // 判断是否请求成功,没有异常就是请求成功
+            var isRequestSucceed = actionContext.Exception == null;
+
+            // 获取调用堆栈信息,提供更加简单明了的调用和异常堆栈
+           // var stackTrace = EnhancedStackTrace.Current();
+           // string region = await _searcher.SearchIpAsync(remoteIPv4);
+           //同一个账号,同一IP,同一接口,UA标识(UA标识随意切换则表示可能会存在DDOS),时间段
+            _logger.LogInformation(new{ ua=httpContext.GetUserAgent(), ip=remoteIPv4,time=requestedTime,path =$"{httpRequest.PathBase}{httpRequest.Path}",host= $"{httpRequest.Host}", param=parameters,id ,name ,school,succeed =isRequestSucceed }.ToJsonString());
+        }
+    }
+}

+ 9 - 0
TEAMModelOS/Program.cs

@@ -1,6 +1,7 @@
 
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
 using System;
 using TEAMModelOS.SDK;
 
@@ -16,6 +17,14 @@ namespace TEAMModelOS
 
         public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
+            //.ConfigureLogging(logging => {
+            //    logging.ClearProviders();
+            //    logging.AddConsole();
+            //    logging.AddDebug();
+            //    logging.AddEventLog();
+            //    logging.AddAzureWebAppDiagnostics();
+            //    logging.AddProvider(new BlobLoggerProvider());
+            //})
                .ConfigureWebHostDefaults(webBuilder =>
                {
 #if DEBUG 

+ 5 - 0
TEAMModelOS/Startup.cs

@@ -25,6 +25,7 @@ using Microsoft.AspNetCore.StaticFiles;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Primitives;
 using Microsoft.IdentityModel.Tokens;
 using TEAMModelOS.Controllers;
@@ -171,6 +172,9 @@ namespace TEAMModelOS
             services.AddSingleton(typeof(IConverter), new SynchronizedConverter(new PdfTools()));
             services.AddXkwAPIHttpService(Configuration);
             //services.AddHostedService<>
+            //services.AddSingleton<ILoggerProvider, BlobLoggerProvider>();
+           // services.AddMvcFilter<RequestAuditFilter>();
+            
         }
 
         // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@@ -210,6 +214,7 @@ namespace TEAMModelOS
             //调用放在之后、UseRouting 和 UseCors,但在 UseEndpoints之前
             app.UseAuthentication();
             app.UseAuthorization();
+            
             app.UseEndpoints(endpoints =>
             {
                 endpoints.MapControllers();

+ 28 - 28
TEAMModelOS/appsettings.Development.json

@@ -22,43 +22,43 @@
   },
   "Azure": {
     // 测试站数据库
-    "Storage": {
-      "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=teammodeltest;AccountKey=O2W2vadCqexDxWO+px+QK7y1sHwsYj8f/WwKLdOdG5RwHgW/Dupz9dDUb4c1gi6ojzQaRpFUeAAmOu4N9E+37A==;EndpointSuffix=core.chinacloudapi.cn"
-    },
-    "Cosmos": {
-      "ConnectionString": "AccountEndpoint=https://cdhabookdep-free.documents.azure.cn:443/;AccountKey=JTUVk92Gjsx17L0xqxn0X4wX2thDPMKiw4daeTyV1HzPb6JmBeHdtFY1MF1jdctW1ofgzqkDMFOtcqS46by31A==;"
-    },
-    "Redis": {
-      "ConnectionString": "52.130.252.100:6379,password=habook,ssl=false,abortConnect=False,writeBuffer=10240"
-    },
-    "ServiceBus": {
-      "ConnectionString": "Endpoint=sb://teammodelos.servicebus.chinacloudapi.cn/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=Sy4h4EQ8zP+7w/lOLi1X3tGord/7ShFHimHs1vC50Dc=",
-      "ActiveTask": "dep-active-task",
-      "ItemCondQueue": "dep-itemcond",
-      "GenPdfQueue": "dep-genpdf"
-    },
-    "SignalR": {
-      "ConnectionString": "Endpoint=https://channel.service.signalr.net;AccessKey=KrblW06tuA4a/GyqRPDU0ynFFmAWxbAvyJihHclSXbQ=;Version=1.0;"
-    },
-    // 正式站数据库
     //"Storage": {
-    //  "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=teammodelos;AccountKey=Dl04mfZ9hE9cdPVO1UtqTUQYN/kz/dD/p1nGvSq4tUu/4WhiKcNRVdY9tbe8620nPXo/RaXxs+1F9sVrWRo0bg==;EndpointSuffix=core.chinacloudapi.cn"
+    //  "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=teammodeltest;AccountKey=O2W2vadCqexDxWO+px+QK7y1sHwsYj8f/WwKLdOdG5RwHgW/Dupz9dDUb4c1gi6ojzQaRpFUeAAmOu4N9E+37A==;EndpointSuffix=core.chinacloudapi.cn"
     //},
     //"Cosmos": {
-    //  "ConnectionString": "AccountEndpoint=https://teammodelos.documents.azure.cn:443/;AccountKey=clF73GwPECfP1lKZTCvs8gLMMyCZig1HODFbhDUsarsAURO7TcOjVz6ZFfPqr1HzYrfjCXpMuVD5TlEG5bFGGg==;"
+    //  "ConnectionString": "AccountEndpoint=https://cdhabookdep-free.documents.azure.cn:443/;AccountKey=JTUVk92Gjsx17L0xqxn0X4wX2thDPMKiw4daeTyV1HzPb6JmBeHdtFY1MF1jdctW1ofgzqkDMFOtcqS46by31A==;"
     //},
     //"Redis": {
-    //  "ConnectionString": "CoreRedisCN.redis.cache.chinacloudapi.cn:6380,password=LyJWP1ORJdv+poXWofAF97lhCEQPg1wXWqvtzXGXQuE=,ssl=True,abortConnect=False"
+    //  "ConnectionString": "52.130.252.100:6379,password=habook,ssl=false,abortConnect=False,writeBuffer=10240"
     //},
     //"ServiceBus": {
-    //  "ConnectionString": "Endpoint=sb://coreiotservicebuscnpro.servicebus.chinacloudapi.cn/;SharedAccessKeyName=TEAMModelOS;SharedAccessKey=llRPBMDJG9w1Nnifj+pGhV0g4H2REcq0PjvX2qqpcOg=",
-    //  "ActiveTask": "active-task",
-    //  "ItemCondQueue": "itemcond",
-    //  "GenPdfQueue": "genpdf"
+    //  "ConnectionString": "Endpoint=sb://teammodelos.servicebus.chinacloudapi.cn/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=Sy4h4EQ8zP+7w/lOLi1X3tGord/7ShFHimHs1vC50Dc=",
+    //  "ActiveTask": "dep-active-task",
+    //  "ItemCondQueue": "dep-itemcond",
+    //  "GenPdfQueue": "dep-genpdf"
     //},
     //"SignalR": {
-    //  "ConnectionString": "Endpoint=https://channel.signalr.azure.cn;AccessKey=AtcB7JYFNUbUXb1rGxa3PVksQ2X5YSv3JOHZR9J88tw=;Version=1.0;"
-    //}
+    //  "ConnectionString": "Endpoint=https://channel.service.signalr.net;AccessKey=KrblW06tuA4a/GyqRPDU0ynFFmAWxbAvyJihHclSXbQ=;Version=1.0;"
+    //},
+    // 正式站数据库
+    "Storage": {
+      "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=teammodelos;AccountKey=Dl04mfZ9hE9cdPVO1UtqTUQYN/kz/dD/p1nGvSq4tUu/4WhiKcNRVdY9tbe8620nPXo/RaXxs+1F9sVrWRo0bg==;EndpointSuffix=core.chinacloudapi.cn"
+    },
+    "Cosmos": {
+      "ConnectionString": "AccountEndpoint=https://teammodelos.documents.azure.cn:443/;AccountKey=clF73GwPECfP1lKZTCvs8gLMMyCZig1HODFbhDUsarsAURO7TcOjVz6ZFfPqr1HzYrfjCXpMuVD5TlEG5bFGGg==;"
+    },
+    "Redis": {
+      "ConnectionString": "CoreRedisCN.redis.cache.chinacloudapi.cn:6380,password=LyJWP1ORJdv+poXWofAF97lhCEQPg1wXWqvtzXGXQuE=,ssl=True,abortConnect=False"
+    },
+    "ServiceBus": {
+      "ConnectionString": "Endpoint=sb://coreiotservicebuscnpro.servicebus.chinacloudapi.cn/;SharedAccessKeyName=TEAMModelOS;SharedAccessKey=llRPBMDJG9w1Nnifj+pGhV0g4H2REcq0PjvX2qqpcOg=",
+      "ActiveTask": "active-task",
+      "ItemCondQueue": "itemcond",
+      "GenPdfQueue": "genpdf"
+    },
+    "SignalR": {
+      "ConnectionString": "Endpoint=https://channel.signalr.azure.cn;AccessKey=AtcB7JYFNUbUXb1rGxa3PVksQ2X5YSv3JOHZR9J88tw=;Version=1.0;"
+    }
   },
   "HaBookAuth": {
     "CoreId": {