Parcourir la source

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

zhouj1203@hotmail.com il y a 9 mois
Parent
commit
353b1cbb53
39 fichiers modifiés avec 4791 ajouts et 1087 suppressions
  1. 64 0
      TEAMModelOS.Extension/HTEX.Complex/Controllers/Mqtt/MqttWeatherForecastController.cs
  2. 4 98
      TEAMModelOS.Extension/HTEX.Complex/Controllers/OfficialController.cs
  3. 1 1
      TEAMModelOS.Extension/HTEX.Complex/Controllers/ScreenController.cs
  4. 2 0
      TEAMModelOS.Extension/HTEX.Complex/HTEX.Complex.csproj
  5. 82 11
      TEAMModelOS.Extension/HTEX.Complex/Program.cs
  6. 136 0
      TEAMModelOS.Extension/HTEX.Complex/Services/MQTT/MQTTEvents.cs
  7. 28 0
      TEAMModelOS.Extension/HTEX.Complex/Services/MQTT/MqttService.cs
  8. 3 7
      TEAMModelOS.Extension/HTEX.Complex/Services/SignalRScreenServerHub.cs
  9. 94 0
      TEAMModelOS.Extension/HTEX.Complex/Services/TaskService.cs
  10. 2 7
      TEAMModelOS.Extension/HTEX.Complex/appsettings.Development.json
  11. 2 7
      TEAMModelOS.Extension/HTEX.Complex/appsettings.json
  12. 45 1
      TEAMModelOS.Function/IESServiceBusTrigger.cs
  13. 1 0
      TEAMModelOS.SDK/Models/Cosmos/Student/StudentArtResult.cs
  14. 2 2
      TEAMModelOS.SDK/Models/Service/ArtService.cs
  15. 113 86
      TEAMModelOS.SDK/Models/Service/GenPDFService.cs
  16. 7 0
      TEAMModelOS.SDK/Models/Service/LessonService.cs
  17. 3 1
      TEAMModelOS/ClientApp/public/lang/en-US.js
  18. 3 1
      TEAMModelOS/ClientApp/public/lang/zh-CN.js
  19. 3 1
      TEAMModelOS/ClientApp/public/lang/zh-TW.js
  20. 8 0
      TEAMModelOS/ClientApp/src/api/htcommunity.js
  21. 8 3
      TEAMModelOS/ClientApp/src/components/student-web/EventView/EventList.vue
  22. 1 1
      TEAMModelOS/ClientApp/src/components/student-web/HomeView/HomeView.vue
  23. 9 0
      TEAMModelOS/ClientApp/src/router/routes.js
  24. 1 0
      TEAMModelOS/ClientApp/src/store/module/config.js
  25. 1 1
      TEAMModelOS/ClientApp/src/utils/blobTool.js
  26. 1 1
      TEAMModelOS/ClientApp/src/view/Home.vue
  27. 13 0
      TEAMModelOS/ClientApp/src/view/evaluation/index/TestPaper.vue
  28. 782 0
      TEAMModelOS/ClientApp/src/view/evaluation/index/htTestPaper.vue
  29. 7 0
      TEAMModelOS/ClientApp/src/view/htcommunity/htMgtExam.less
  30. 862 846
      TEAMModelOS/ClientApp/src/view/htcommunity/htMgtExam.vue
  31. 2 2
      TEAMModelOS/ClientApp/src/view/htcommunity/htMgtHome.vue
  32. 1 0
      TEAMModelOS/ClientApp/src/view/learnactivity/CreatePrivEva.vue
  33. 23 7
      TEAMModelOS/ClientApp/src/view/learnactivity/ManualCreateNew.vue
  34. 5 1
      TEAMModelOS/ClientApp/src/view/learnactivity/ManualPaper.vue
  35. 704 0
      TEAMModelOS/ClientApp/src/view/learnactivity/htCreateEva.vue
  36. 145 0
      TEAMModelOS/ClientApp/src/view/learnactivity/tabs/htAnswerTable.less
  37. 1490 0
      TEAMModelOS/ClientApp/src/view/learnactivity/tabs/htAnswerTable.vue
  38. 106 0
      TEAMModelOS/ClientApp/src/view/learnactivity/tabs/htExamPaper.vue
  39. 27 2
      TEAMModelOS/Controllers/System/GenPDFController.cs

+ 64 - 0
TEAMModelOS.Extension/HTEX.Complex/Controllers/Mqtt/MqttWeatherForecastController.cs

@@ -0,0 +1,64 @@
+using MQTTnet.AspNetCore.Routing;
+using MQTTnet.AspNetCore.Routing.Attributes;
+using System.Text.Json;
+using TEAMModelOS.SDK.Extension;
+namespace HTEX.Complex.Controllers.Mqtt
+{
+    /// <summary>
+    /// 参考示例 https://www.nuget.org/packages/MQTTnet.AspNetCore.Routing
+    /// </summary>
+    [MqttController]
+    [MqttRoute("[controller]")] // Optional route prefix
+    public class MqttWeatherForecastController : MqttBaseController // Inherit from MqttBaseController for convenience functions
+    {
+        private readonly ILogger<MqttWeatherForecastController> _logger;
+
+        // Controllers have full support for dependency injection just like AspNetCore controllers
+        public MqttWeatherForecastController(ILogger<MqttWeatherForecastController> logger)
+        {
+            _logger = logger;
+        }
+
+        // Supports template routing with typed constraints just like AspNetCore
+        // Action routes compose together with the route prefix on the controller level
+        [MqttRoute("{zipCode:int}/temperature")]
+        public Task WeatherReport(int zipCode)
+        {
+            // We have access to the MqttContext
+            if (zipCode != 90210) { MqttContext.CloseConnection = true; }
+
+            // We have access to the raw message
+            var temperature = BitConverter.ToDouble(Message.Payload);
+
+            _logger.LogInformation($"It's {temperature} degrees in Hollywood");
+
+            // Example validation
+            if (temperature <= 0 || temperature >= 130)
+            {
+                return BadMessage();
+            }
+
+            return Ok();
+        }
+
+        // Supports binding JSON message payload to parameters with [FromPayload] attribute,
+        // Similar to ASP.NET Core [FromBody]
+        [MqttRoute("{deviceName}/telemetry")]
+        public async Task NewTelemetry(string deviceName, [FromPayload] JsonElement telemetry)
+        {
+            // here telemetry is JSON-deserialized from message payload to type Telemetry
+            telemetry.ToJsonString();
+            bool success =true;
+            if (success)
+            {
+                await Ok();
+                return;
+            }
+            else
+            {
+                await BadMessage();
+                return;
+            }
+        }
+    }
+}

+ 4 - 98
TEAMModelOS.Extension/HTEX.Complex/Controllers/OfficialController.cs

@@ -13,53 +13,25 @@ namespace HTEX.Complex.Controllers
     {
 
         private readonly DingDing _dingDing;
-        // private readonly SnowflakeId _snowflakeId;
-        // private readonly ServerSentEventsService _sse;
-        private readonly AzureCosmosFactory _azureCosmos3Factory;
         private readonly System.Net.Http.IHttpClientFactory _httpClientFactory;
-        //private readonly Models.Option _option;
-        // private readonly MailFactory _mailFactory;
         private readonly AzureRedisFactory _azureRedis;
         private readonly AzureStorageFactory _azureStorage;
-        private readonly IWebHostEnvironment _environment;
         private readonly IConfiguration _configuration;
-        // private readonly CoreAPIHttpService _coreAPIHttpService;
         private readonly IPSearcher _searcher;
-        public OfficialController(IWebHostEnvironment environment, IConfiguration configuration,
-            AzureStorageFactory azureStorage, AzureRedisFactory azureRedis, System.Net.Http.IHttpClientFactory httpClientFactory, AzureCosmosFactory azureCosmos3Factory,
-            // IOptionsSnapshot<Option> option,  
+        public OfficialController(IConfiguration configuration,
+            AzureStorageFactory azureStorage, AzureRedisFactory azureRedis, System.Net.Http.IHttpClientFactory httpClientFactory,
             DingDing dingDing, IPSearcher searcher)
         {
             _dingDing = dingDing;
-            _azureCosmos3Factory = azureCosmos3Factory;
             _httpClientFactory = httpClientFactory;
             _azureRedis = azureRedis;
             _azureStorage = azureStorage;
-            _environment = environment;
             _configuration = configuration;
             _searcher = searcher;
         }
-
-        [ProducesDefaultResponseType]
-        [RequestSizeLimit(102_400_000_00)] //最大10000m左右
-        [HttpPost("video-ls")]
-        public async Task<IActionResult> VideoUpload()
-        {
-            List<string> students = new List<string>();
-            //var result= await _azureCosmos3Factory.GetCosmosClient().GetContainer("winteachos", Constant.Teacher)
-            //    .GetList<Teacher>( "select value c from c", "Base");
-
-            await foreach (var item in _azureCosmos3Factory.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Student)
-                .GetItemQueryIteratorSql<string>(queryText: "select value c.id from c", requestOptions: new QueryRequestOptions { MaxItemCount=5, PartitionKey = new PartitionKey("Base-hbcn") }))
-            {
-                students.Add(item);
-            }
-            return Ok(students);
-        }
-
         [ProducesDefaultResponseType]
         [HttpPost("video-list")]
-        public async Task<IActionResult> VideoList(JsonElement json)
+        public IActionResult VideoList(JsonElement json)
         {
             List<OfficialVideo> videos = new List<OfficialVideo>();
             var table = _azureStorage.TableServiceClient().GetTableClient("ShortUrl");
@@ -67,13 +39,10 @@ namespace HTEX.Complex.Controllers
             if (json.TryGetProperty("rowKey", out JsonElement _rowKey) && !string.IsNullOrWhiteSpace($"{_rowKey}"))
             {
                 videos =    table.Query<OfficialVideo>($"{Constant.PartitionKey} {Constant.Equal} 'OfficialVideo' and {Constant.RowKey} {Constant.Equal} '{_rowKey}'").ToList();
-
-
             }
             else
             {
                 videos =    table.Query<OfficialVideo>(filter: $"{Constant.PartitionKey} eq  'OfficialVideo'").ToList();
-
             }
             return Ok(new { videos });
         }
@@ -153,68 +122,5 @@ namespace HTEX.Complex.Controllers
         /// </summary>
         public string? url { get; set; }
     }
-    public class Student : CosmosEntity
-    {
-        public string mail { get; set; }
-        public string mobile { get; set; }
-        public string country { get; set; }
-        public string name { get; set; }
-        public string picture { get; set; }
-        public string schoolId { get; set; }
-        public string pw { get; set; }
-        public string salt { get; set; }
-        public int year { get; set; }
-        //座位号
-        public string no { get; set; }   //座位号
-        public string irs { get; set; }
-        //绑定班级Id
-        public string classId { get; set; }
-        //分组信息
-        public string groupId { get; set; }
-        public string groupName { get; set; }
-        public string periodId { get; set; }
-        /// <summary>
-        /// 性别 M( male,男) F (female 女)  N(secret 保密) 
-        /// </summary>
-        public string gender { get; set; }
-
-        //补充留级信息
-        //0在校,1毕业 
-        public int graduate { get; set; } = 0;
-
-        /// <summary>
-        /// 创建时间  十位 时间戳
-        /// </summary>
-        public long createTime { get; set; }
-
-        /// <summary>
-        /// 学生的专业id
-        /// </summary>
-        public string majorId { get; set; }
-        /// <summary>
-        /// 學生的OpenID (TW教育雲綁定ID)
-        /// </summary>
-        public string openId { get; set; }
-    }
-
-    /// <summary>
-    /// 教师
-    /// </summary>
-    public class Teacher : CosmosEntity
-    {
-        public Teacher()
-        {
-            pk = "Teacher";
-        }
-
-        /// <summary>
-        /// 系统权限信息
-        /// </summary>
-        public HashSet<string> permissions { get; set; } = new HashSet<string>();
-        /// <summary>
-        /// 组织信息
-        /// </summary>
-
-        //常用设备信息,及IP登录信息。
-    }
+   
 }

+ 1 - 1
TEAMModelOS.Extension/HTEX.Complex/Controllers/ScreenController.cs

@@ -53,7 +53,7 @@ namespace HTEX.Complex.Controllers
                     {
                         _logger.LogInformation($"客户端当前空闲=>{screenClient.name},{screenClient.region},{screenClient.clientid},分发任务......");
                         //连接成功,马上分发任务。
-                        var task = await GenPDFService.SentTask(_azureRedis, _azureStorage);
+                        var task = await TaskService.SentTask(_azureRedis, _azureStorage);
                         if (task.genQueue!=null && task.genRedis!=null  && !string.IsNullOrWhiteSpace(task.genQueue.cntName))
                         {
                             screenClient.status =  ScreenConstant.busy;

+ 2 - 0
TEAMModelOS.Extension/HTEX.Complex/HTEX.Complex.csproj

@@ -13,6 +13,8 @@
 	<PackageReference Include="Hangfire" Version="1.8.11" />
 	<PackageReference Include="Hangfire.Dashboard.BasicAuthorization" Version="1.0.2" />
 	<PackageReference Include="Hangfire.Redis.StackExchange" Version="1.9.3" />
+	<PackageReference Include="MQTTnet.AspNetCore" Version="4.3.6.1152" />
+	<PackageReference Include="MQTTnet.AspNetCore.Routing" Version="0.4.37" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\TEAMModelOS.SDK\TEAMModelOS.SDK.csproj" />

+ 82 - 11
TEAMModelOS.Extension/HTEX.Complex/Program.cs

@@ -7,14 +7,39 @@ using System.IdentityModel.Tokens.Jwt;
 using TEAMModelOS.SDK.DI;
 using TEAMModelOS.SDK;
 using HTEX.Complex.Services;
+using HTEX.Complex.Services.MQTT;
+using MathNet.Numerics;
+using MQTTnet.AspNetCore;
+using MQTTnet.Server;
+using Microsoft.Extensions.Options;
+using System.Threading;
+using Microsoft.Extensions.Logging;
+using System.Text.Json;
+using Microsoft.Extensions.Hosting;
+using MQTTnet.AspNetCore.Routing;
+using Microsoft.AspNetCore.Builder;
 namespace HTEX.Complex
 {
     public class Program
     {
         public static void Main(string[] args)
         {
-            var builder = WebApplication.CreateBuilder(args);
-
+            var builder = WebApplication.CreateBuilder(args);            //builder.WebHost.ConfigureKestrel(options =>
+            //{
+            //    //options.ListenAnyIP(4001, options => {
+            //    //   // options.UseHttps("Crt/iteden.pfx", "iteden"); 
+            //    //});
+            //    options.ListenAnyIP(1883, options => { options.UseMqtt();/*options.UseHttps("Crt/iteden.pfx", "iteden");*/ });
+            //    options.ListenAnyIP(1884, options => { options.UseMqtt();/* options.UseHttps("Configs/Crt/iteden.pfx", "iteden"); */}); // Default HTTP pipeline
+            //});
+            //builder.WebHost.ConfigureKestrel(options =>
+            //{
+            //    //options.ListenAnyIP(4001, options => {
+            //    //   // options.UseHttps("Crt/iteden.pfx", "iteden"); 
+            //    //});
+            //    options.ListenAnyIP(1883, options => { options.UseMqtt();/*options.UseHttps("Crt/iteden.pfx", "iteden");*/ });
+            //    options.ListenAnyIP(1884, options => { options.UseMqtt();/* options.UseHttps("Configs/Crt/iteden.pfx", "iteden"); */}); // Default HTTP pipeline
+            //});
 
             // Add services to the container.
 
@@ -59,15 +84,14 @@ namespace HTEX.Complex
             builder.Services.AddHttpClient();
             string? StorageConnectionString = builder.Configuration.GetValue<string>("Azure:Storage:ConnectionString");
             string? RedisConnectionString = builder.Configuration.GetValue<string>("Azure:Redis:ConnectionString");
-            string? CosmosConnectionString = builder.Configuration.GetValue<string>("Azure:Cosmos:ConnectionString");
             //Storage
             builder.Services.AddAzureStorage(StorageConnectionString, "Default");
             //Redis
             builder.Services.AddAzureRedis(RedisConnectionString, "Default");
-            //Cosmos
-            builder.Services.AddAzureCosmos(CosmosConnectionString, "Default");
+            string? StorageConnectionStringTest = builder.Configuration.GetValue<string>("Azure:Storage:ConnectionString-Test");
+            //Storage
+            builder.Services.AddAzureStorage(StorageConnectionString, "Test");
 
-            //MQTT  服务端API 发送消息到MQTT客户端 https://www.cnblogs.com/weskynet/p/16441219.html
             builder.Services.AddSignalR();
             builder.Services.AddHttpContextAccessor();
             builder.Services.AddHttpClient<DingDing>();
@@ -93,9 +117,36 @@ namespace HTEX.Complex
 #endif
 
             builder.Services.AddControllersWithViews();
+            //MQTT  服务端API 发送消息到MQTT客户端 https://www.cnblogs.com/weskynet/p/16441219.html
+            // builder.Services.AddHostedService<MqttHostService>();
 
-            var app = builder.Build();
 
+            #region MQTT配置
+            //string hostIp = Configuration["MqttOption:HostIp"];//IP地址
+            //int hostPort = int.Parse(Configuration["MqttOption:HostPort"]);//端口号
+            //int timeout = int.Parse(Configuration["MqttOption:Timeout"]);//超时时间
+            //string username = Configuration["MqttOption:UserName"];//用户名
+            //string password = Configuration["MqttOption:Password"];//密码
+
+
+
+
+            builder.Services.AddSingleton<MQTTEvents>();
+            builder.Services.AddSingleton(new JsonSerializerOptions(JsonSerializerDefaults.Web));
+      
+            builder.Services.AddHostedMqttServerWithServices(x => {
+                x.WithDefaultEndpoint()
+                //.WithDefaultEndpointBoundIPAddress(System.Net.IPAddress.Parse("127.0.0.1"))
+                //.WithDefaultEndpointPort(1883)
+                //.WithEncryptedEndpointPort(1884)
+                .WithConnectionBacklog(1000)
+                .WithPersistentSessions(true).WithKeepAlive()
+                .WithDefaultCommunicationTimeout(TimeSpan.FromMilliseconds(30));
+            }).AddMqttConnectionHandler().AddConnections().AddMqttControllers();
+            #endregion
+
+            var app = builder.Build();
+            var events = app.Services.GetRequiredService<MQTTEvents>();
             // Configure the HTTP request pipeline.
             if (!app.Environment.IsDevelopment())
             {
@@ -115,10 +166,30 @@ namespace HTEX.Complex
             //app.MapControllerRoute(
             //    name: "default",
             //    pattern: "{controller=Home}/{action=Index}/{id?}");
-            app.UseEndpoints(endpoints => {
-                endpoints.MapControllers();
-                endpoints.MapFallbackToFile("index.html");
-                endpoints.MapHub<SignalRScreenServerHub>("/signalr/screen").RequireCors("any");
+            //app.UseEndpoints(endpoints => {
+            //    endpoints.MapControllers();
+            //    endpoints.MapConnectionHandler<MqttConnectionHandler>("/mqtt", opts => opts.WebSockets.SubProtocolSelector = protocolList => protocolList.FirstOrDefault() ?? string.Empty);
+            //    endpoints.MapFallbackToFile("index.html");
+            //    endpoints.MapHub<SignalRScreenServerHub>("/signalr/screen").RequireCors("any");
+            //});
+            app.MapControllers();
+            app.MapConnectionHandler<MqttConnectionHandler>("/mqtt", opts => opts.WebSockets.SubProtocolSelector = protocolList => protocolList.FirstOrDefault() ?? string.Empty);
+            app.MapFallbackToFile("index.html");
+            app.MapHub<SignalRScreenServerHub>("/signalr/screen").RequireCors("any");
+            app.UseMqttServer(server =>
+            {
+                server.WithAttributeRouting(app.Services, allowUnmatchedRoutes: false);
+                server.ClientSubscribedTopicAsync+=events._mqttServer_ClientSubscribedTopicAsync;// 客户端订阅主题事件
+                server.StoppedAsync+=events._mqttServer_StoppedAsync;// 关闭后事件
+                server.ValidatingConnectionAsync+=events._mqttServer_ValidatingConnectionAsync; // 用户名和密码验证有关
+                server.InterceptingPublishAsync+=events._mqttServer_InterceptingPublishAsync;// 消息接收事件
+                server.StartedAsync+=events._mqttServer_StartedAsync;// 启动后事件
+                server.ClientUnsubscribedTopicAsync+=events._mqttServer_ClientUnsubscribedTopicAsync;// 客户端取消订阅事件
+                server.ApplicationMessageNotConsumedAsync+=events._mqttServer_ApplicationMessageNotConsumedAsync; // 消息接收事件,应用程序消息未使用
+                server.ClientDisconnectedAsync+=events._mqttServer_ClientDisconnectedAsync;// 客户端关闭事件
+                server.ClientConnectedAsync+=events._mqttServer_ClientConnectedAsync;//客户端连接事件
+                //server.InterceptingClientEnqueueAsync += events._mqttServer_InterceptingClientEnqueueAsync;//拦截客户端排队
+                //server.ClientAcknowledgedPublishPacketAsync += events._mqttServer_ClientAcknowledgedPublishPacketAsync;//已确认发布数据包
             });
             app.Run();
         }

+ 136 - 0
TEAMModelOS.Extension/HTEX.Complex/Services/MQTT/MQTTEvents.cs

@@ -0,0 +1,136 @@
+using Microsoft.Extensions.Options;
+using MQTTnet.Protocol;
+using MQTTnet.Server;
+using System.Text;
+
+namespace HTEX.Complex.Services.MQTT
+{
+    public class MQTTEvents
+    {
+        
+        public MQTTEvents()
+        {
+            
+
+        }
+
+        /// <summary>
+        /// 客户端订阅主题事件
+        /// </summary>
+        /// <param name="arg"></param>
+        /// <returns></returns>
+        public Task _mqttServer_ClientSubscribedTopicAsync(ClientSubscribedTopicEventArgs arg)
+        {
+            Console.WriteLine($"ClientSubscribedTopicAsync:客户端ID=【{arg.ClientId}】订阅的主题=【{arg.TopicFilter}】 ");
+            return Task.CompletedTask;
+        }
+
+        /// <summary>
+        /// 关闭后事件
+        /// </summary>
+        /// <param name="arg"></param>
+        /// <returns></returns>
+        public Task _mqttServer_StoppedAsync(EventArgs arg)
+        {
+            Console.WriteLine($"StoppedAsync:MQTT服务已关闭……");
+            return Task.CompletedTask;
+        }
+
+        /// <summary>
+        /// 用户名和密码验证有关
+        /// </summary>
+        /// <param name="arg"></param>
+        /// <returns></returns>
+        public Task _mqttServer_ValidatingConnectionAsync(ValidatingConnectionEventArgs arg)
+        {
+            arg.ReasonCode = MqttConnectReasonCode.Success;
+            //if ((arg.UserName ?? string.Empty) != _mqttOption.UserName || (arg.Password ?? String.Empty) != _mqttOption.Password)
+            //{
+            //    arg.ReasonCode = MqttConnectReasonCode.Banned;
+            //    Console.WriteLine($"ValidatingConnectionAsync:客户端ID=【{arg.ClientId}】用户名或密码验证错误 ");
+
+            //}
+            return Task.CompletedTask;
+        }
+
+        /// <summary>
+        /// 消息接收事件
+        /// </summary>
+        /// <param name="arg"></param>
+        /// <returns></returns>
+        public Task _mqttServer_InterceptingPublishAsync(InterceptingPublishEventArgs arg)
+        {
+            //if (string.Equals(arg.ClientId,_mqttOption. ServerClientId))
+            //{
+            //    return Task.CompletedTask;
+            //}
+
+            Console.WriteLine($"InterceptingPublishAsync:客户端ID=【{arg.ClientId}】 Topic主题=【{arg.ApplicationMessage.Topic}】 消息=【{Encoding.UTF8.GetString(arg.ApplicationMessage.PayloadSegment)}】 qos等级=【{arg.ApplicationMessage.QualityOfServiceLevel}】");
+            return Task.CompletedTask;
+
+        }
+
+        /// <summary>
+        /// 启动后事件
+        /// </summary>
+        /// <param name="arg"></param>
+        /// <returns></returns>
+        public Task _mqttServer_StartedAsync(EventArgs arg)
+        {
+            Console.WriteLine($"StartedAsync:MQTT服务已启动……");
+            return Task.CompletedTask;
+        }
+
+        /// <summary>
+        /// 客户端取消订阅事件
+        /// </summary>
+        /// <param name="arg"></param>
+        /// <returns></returns>
+        public Task _mqttServer_ClientUnsubscribedTopicAsync(ClientUnsubscribedTopicEventArgs arg)
+        {
+            Console.WriteLine($"ClientUnsubscribedTopicAsync:客户端ID=【{arg.ClientId}】已取消订阅的主题=【{arg.TopicFilter}】  ");
+            return Task.CompletedTask;
+        }
+
+        public Task _mqttServer_ApplicationMessageNotConsumedAsync(ApplicationMessageNotConsumedEventArgs arg)
+        {
+            Console.WriteLine($"ApplicationMessageNotConsumedAsync:发送端ID=【{arg.SenderId}】 Topic主题=【{arg.ApplicationMessage.Topic}】 消息=【{Encoding.UTF8.GetString(arg.ApplicationMessage.PayloadSegment)}】 qos等级=【{arg.ApplicationMessage.QualityOfServiceLevel}】");
+            return Task.CompletedTask;
+
+        }
+
+        /// <summary>
+        /// 客户端断开时候触发
+        /// </summary>
+        /// <param name="arg"></param>
+        /// <returns></returns>
+        /// <exception cref="NotImplementedException"></exception>
+        public Task _mqttServer_ClientDisconnectedAsync(ClientDisconnectedEventArgs arg)
+        {
+            Console.WriteLine($"ClientDisconnectedAsync:客户端ID=【{arg.ClientId}】已断开, 地址=【{arg.Endpoint}】  ");
+            return Task.CompletedTask;
+
+        }
+
+        /// <summary>
+        /// 客户端连接时候触发
+        /// </summary>
+        /// <param name="arg"></param>
+        /// <returns></returns>
+        public Task _mqttServer_ClientConnectedAsync(ClientConnectedEventArgs arg)
+        {
+            Console.WriteLine($"ClientConnectedAsync:客户端ID=【{arg.ClientId}】已连接, 用户名=【{arg.UserName}】地址=【{arg.Endpoint}】  ");
+            return Task.CompletedTask;
+        }
+
+        //public async Task _mqttServer_InterceptingClientEnqueueAsync(InterceptingClientApplicationMessageEnqueueEventArgs args)
+        //{
+        //    throw new NotImplementedException();
+        //}
+
+        //public async Task _mqttServer_ClientAcknowledgedPublishPacketAsync(ClientAcknowledgedPublishPacketEventArgs args)
+        //{
+        //    throw new NotImplementedException();
+        //}
+    }
+}

+ 28 - 0
TEAMModelOS.Extension/HTEX.Complex/Services/MQTT/MqttService.cs

@@ -0,0 +1,28 @@
+using MQTTnet.Protocol;
+using MQTTnet.Server;
+using MQTTnet;
+using System.Text;
+
+namespace HTEX.Complex.Services.MQTT
+{
+    public class MqttService
+    {
+        public static MqttServer _mqttServer { get; set; }
+
+        public static void PublishData(string data)
+        {
+            var message = new MqttApplicationMessage
+            {
+                Topic = "topic_01",
+                Payload = Encoding.Default.GetBytes(data),
+                QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce,
+                Retain = true  // 服务端是否保留消息。true为保留,如果有新的订阅者连接,就会立马收到该消息。
+            };
+
+            _mqttServer.InjectApplicationMessage(new InjectedMqttApplicationMessage(message) // 发送消息给有订阅 topic_01的客户端
+            {
+                SenderClientId = "Server_01"
+            }).GetAwaiter().GetResult();
+        }
+    }
+}

+ 3 - 7
TEAMModelOS.Extension/HTEX.Complex/Services/SignalRScreenServerHub.cs

@@ -1,6 +1,4 @@
-using Grpc.Core;
-using Microsoft.AspNetCore.SignalR;
-using Microsoft.Azure.Cosmos.Linq;
+using Microsoft.AspNetCore.SignalR;
 using Microsoft.Extensions.Primitives;
 using TEAMModelOS.SDK.DI;
 using TEAMModelOS.SDK.Extension;
@@ -9,8 +7,6 @@ using System.Web;
 using System.Text;
 
 using StackExchange.Redis;
-using Azure.Storage.Blobs.Models;
-using Azure.Storage.Sas;
 namespace HTEX.Complex.Services
 {
     public class SignalRScreenServerHub : Hub<IClient>
@@ -109,7 +105,7 @@ namespace HTEX.Complex.Services
                             if (screenClient.status!.Equals(ScreenConstant.idle)) {
                                 _logger.LogInformation($"客户端当前空闲=>{screenClient.name},{screenClient.region},{clientid},分发任务......");
                                 //连接成功,马上分发任务。
-                                var task = await GenPDFService.SentTask(_azureRedis,_azureStorage);
+                                var task = await TaskService.SentTask(_azureRedis,_azureStorage);
                                 if (task.genQueue!=null && task.genRedis!=null  && !string.IsNullOrWhiteSpace(task.genQueue.cntName))
                                 {
                                     screenClient.status =  ScreenConstant.busy;
@@ -255,7 +251,7 @@ namespace HTEX.Complex.Services
             }
             if (screenClient!=null && screenClient.status!.Equals(ScreenConstant.idle))
             {
-                var taskData = await GenPDFService.SentTask(_azureRedis, _azureStorage);
+                var taskData = await TaskService.SentTask(_azureRedis, _azureStorage);
                 if (taskData.genQueue!=null && taskData.genRedis!=null  && !string.IsNullOrWhiteSpace(taskData.genQueue.cntName))
                 {
                     screenClient.status =  ScreenConstant.busy;

+ 94 - 0
TEAMModelOS.Extension/HTEX.Complex/Services/TaskService.cs

@@ -0,0 +1,94 @@
+using Azure.Storage.Sas;
+using StackExchange.Redis;
+using System.Web;
+using TEAMModelOS.SDK.DI;
+using TEAMModelOS.SDK;
+using TEAMModelOS.SDK.Extension;
+
+namespace HTEX.Complex.Services
+{
+    public class TaskService
+    {
+        public static async Task<(PDFGenRedis genRedis, PDFGenQueue genQueue, string msg)> SentTask(AzureRedisFactory _azureRedis, AzureStorageFactory _azureStorage)
+        {
+            string msg = string.Empty;
+            //从尾部弹出元素,队列先进先出
+            long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+            var queueValue = await _azureRedis.GetRedisClient(8).ListRightPopAsync("PDFGen:Queue");
+            PDFGenRedis genRedis = null;
+            PDFGenQueue genQueue = null;
+            if (queueValue!=default && queueValue.HasValue)
+            {
+                genQueue = queueValue.ToString().ToObject<PDFGenQueue>();
+                RedisValue redisValue = await _azureRedis.GetRedisClient(8).HashGetAsync($"PDFGen:{genQueue.sessionId}", genQueue.id);
+                if (redisValue!=default)
+                {
+                    genRedis = redisValue.ToString().ToObject<PDFGenRedis>();
+                    //计算等待了多久的时间才开始生成。
+                    var wait = now- genRedis.join;
+                    genRedis.wait  = wait;
+                    genRedis.status = 1;
+                    try
+                    {
+                        Uri uri = new Uri(genQueue.pageUrl);
+                        var query = HttpUtility.ParseQueryString(uri.Query);
+                        string? url = query["url"];
+                        if (!string.IsNullOrWhiteSpace(url))
+                        {
+                            url=  HttpUtility.UrlDecode(url);
+                            uri = new Uri(url);
+                            string host = uri.Host;
+                            string azure_region = genQueue.env.Equals(ScreenConstant.env_develop) ? "Test" : "Default";
+                            var blobServiceClient = _azureStorage.GetBlobServiceClient("Test");
+                            if (host.Equals(blobServiceClient.Uri.Host))
+                            {
+                                // 获取容器名,它是路径的第一个部分
+                                string containerName = uri.Segments[1].TrimEnd('/');
+                                // 获取文件的完整同级目录,这是文件路径中除了文件名和扩展名之外的部分
+                                // 由于文件名是路径的最后一个部分,我们可以通过连接除了最后一个部分之外的所有部分来获取目录路径
+                                string directoryPath = string.Join("", uri.Segments, 2, uri.Segments.Length - 3);
+                                string? fileName = Path.GetFileNameWithoutExtension(uri.AbsolutePath);
+                                string blobPath = $"{directoryPath}{fileName}.pdf";
+                                var urlSas = _azureStorage.GetBlobSAS(containerName, blobPath, BlobSasPermissions.Write|BlobSasPermissions.Read, hour: 1, name: azure_region);
+                                genQueue.blobSas =  urlSas.sas;
+                                genQueue.blobName=blobPath;
+                                genQueue.cntName=containerName;
+                                genQueue.blobFullUrl=urlSas.fullUri;
+                            }
+                            else
+                            {
+                                msg="数据地址与服务提供的站点不一致!";
+                                genRedis.status=3;
+                                genRedis.msg = msg;
+
+                            }
+                        }
+                        else
+                        {
+                            msg="数据地址解析异常!";
+                            genRedis.status=3;
+                            genRedis.msg = msg;
+
+                        }
+                    }
+                    catch (Exception ex)
+                    {
+                        msg=$"数据地址处理异常,异常信息:{ex.Message}";
+                        genRedis.status=3;
+                        genRedis.msg = ex.Message;
+                    }
+                    await _azureRedis.GetRedisClient(8).HashSetAsync($"PDFGen:{genQueue.sessionId}", genQueue.id, genRedis.ToJsonString());
+                }
+                else
+                {
+                    msg="队列任务关联数据为空!";
+                }
+            }
+            else
+            {
+                msg="队列暂无任务!";
+            }
+            return (genRedis, genQueue, msg);
+        }
+    }
+}

+ 2 - 7
TEAMModelOS.Extension/HTEX.Complex/appsettings.Development.json

@@ -8,16 +8,11 @@
   "AllowedHosts": "*",
   "Azure": {
     "Storage": {
-      "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=teammodeltest;AccountKey=O2W2vadCqexDxWO+px+QK7y1sHwsYj8f/WwKLdOdG5RwHgW/Dupz9dDUb4c1gi6ojzQaRpFUeAAmOu4N9E+37A==;EndpointSuffix=core.chinacloudapi.cn"
-    },
-    "ServiceBus": {
-      "ConnectionString": "Endpoint=sb://coreservicebuscn.servicebus.chinacloudapi.cn/;SharedAccessKeyName=TEAMModelOS;SharedAccessKey=xO8HcvXXuuEkuFI0KlV5uXs8o6vyuVqTR+ASbPGMhHo="
+      "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=teammodeltest;AccountKey=O2W2vadCqexDxWO+px+QK7y1sHwsYj8f/WwKLdOdG5RwHgW/Dupz9dDUb4c1gi6ojzQaRpFUeAAmOu4N9E+37A==;EndpointSuffix=core.chinacloudapi.cn",
+      "ConnectionString-Test": "DefaultEndpointsProtocol=https;AccountName=teammodeltest;AccountKey=O2W2vadCqexDxWO+px+QK7y1sHwsYj8f/WwKLdOdG5RwHgW/Dupz9dDUb4c1gi6ojzQaRpFUeAAmOu4N9E+37A==;EndpointSuffix=core.chinacloudapi.cn"
     },
     "Redis": {
       "ConnectionString": "52.130.252.100:6379,password=habook,ssl=false,abortConnect=False,writeBuffer=10240"
-    },
-    "Cosmos": {
-      "ConnectionString": "AccountEndpoint=https://cdhabookdep-free.documents.azure.cn:443/;AccountKey=JTUVk92Gjsx17L0xqxn0X4wX2thDPMKiw4daeTyV1HzPb6JmBeHdtFY1MF1jdctW1ofgzqkDMFOtcqS46by31A==;"
     }
   }
 }

+ 2 - 7
TEAMModelOS.Extension/HTEX.Complex/appsettings.json

@@ -9,15 +9,10 @@
   "Azure": {
     "Storage": {
       "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=teammodelos;AccountKey=Dl04mfZ9hE9cdPVO1UtqTUQYN/kz/dD/p1nGvSq4tUu/4WhiKcNRVdY9tbe8620nPXo/RaXxs+1F9sVrWRo0bg==;EndpointSuffix=core.chinacloudapi.cn",
-    },
-    "ServiceBus": {
-      "ConnectionString": "Endpoint=sb://coreservicebuscn.servicebus.chinacloudapi.cn/;SharedAccessKeyName=TEAMModelOS;SharedAccessKey=xO8HcvXXuuEkuFI0KlV5uXs8o6vyuVqTR+ASbPGMhHo=",
+      "ConnectionString-Test": "DefaultEndpointsProtocol=https;AccountName=teammodeltest;AccountKey=O2W2vadCqexDxWO+px+QK7y1sHwsYj8f/WwKLdOdG5RwHgW/Dupz9dDUb4c1gi6ojzQaRpFUeAAmOu4N9E+37A==;EndpointSuffix=core.chinacloudapi.cn"
     },
     "Redis": {
-      "ConnectionString": "CoreRedisCN.redis.cache.chinacloudapi.cn:6380,password=LyJWP1ORJdv+poXWofAF97lhCEQPg1wXWqvtzXGXQuE=,ssl=True,abortConnect=False",
-    },
-    "Cosmos": {
-      "ConnectionString": "AccountEndpoint=https://teammodelos.documents.azure.cn:443/;AccountKey=clF73GwPECfP1lKZTCvs8gLMMyCZig1HODFbhDUsarsAURO7TcOjVz6ZFfPqr1HzYrfjCXpMuVD5TlEG5bFGGg==;",
+      "ConnectionString": "CoreRedisCN.redis.cache.chinacloudapi.cn:6380,password=LyJWP1ORJdv+poXWofAF97lhCEQPg1wXWqvtzXGXQuE=,ssl=True,abortConnect=False"
     }
   }
 }

+ 45 - 1
TEAMModelOS.Function/IESServiceBusTrigger.cs

@@ -83,7 +83,51 @@ namespace TEAMModelOS.Function
             _httpClient=httpClient;
             _environment = hostingEnvironment;
         }
-        [Function("GenPDF")]
+        [Function("GenPdf")]
+        public async Task GenPdfFunc([ServiceBusTrigger("%Azure:ServiceBus:GenPdfQueue%", Connection = "Azure:ServiceBus:ConnectionString", IsBatched = false)] ServiceBusReceivedMessage message,
+         ServiceBusMessageActions messageActions)
+        {
+            //https://github.com/aafgani/AzFuncWithServiceBus/blob/a0da42f59b5fc45655b73b85bae932e84520db70/ServiceBusTriggerFunction/host.json
+            //  messageHandlerOptions 设置
+
+            // https://dotblogs.com.tw/yc421206/2013/04/25/102300  // C#  原子操作。Interlocked
+            // ConcurrentQueue  http://t.zoukankan.com/hohoa-p-12622459.html
+            // await messageActions.RenewMessageLockAsync(message);
+            long time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+            Console.WriteLine($"開始:{time}");
+            string apiUri = "http://52.130.252.100:13000";
+            long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+            _logger.LogInformation("Message ID: {id}", message.MessageId);
+            _logger.LogInformation("Message Body: {body}", message.Body);
+            _logger.LogInformation("Message Content-Type: {contentType}", message.ContentType);
+            var json = JsonDocument.Parse(message.Body);
+
+            try
+            {
+                switch (true)
+                {
+                    case bool when json.RootElement.TryGetProperty("bizType", out JsonElement _bizType) && $"{_bizType}".Equals("OfflineRecord"):
+                        //处理教师线下研修报告的生成。
+                        //await GenOfflineRecordPdf(element, msg);
+                        break;
+                    case bool when json.RootElement.TryGetProperty("bizType", out JsonElement _bizType) && $"{_bizType}".Equals("ArtStudentPdf"):
+                        await GenPDFService.GenArtStudentPdf(_azureRedis, _azureCosmos,
+                  _coreAPIHttpService, _dingDing, _azureStorage,_configuration, json.RootElement);
+                        break;
+                }
+            }
+            catch (Exception ex)
+            {
+                await _dingDing.SendBotMsg($"{Environment.GetEnvironmentVariable("Option:Location")}-ServiceBus,GenPDF()\n{ex.Message}\n{ex.StackTrace}\n\n{json.RootElement.ToJsonString()}", GroupNames.醍摩豆服務運維群組);
+            }
+            finally
+            {  // Complete the message
+                await messageActions.CompleteMessageAsync(message);
+            }
+        }
+
+
+       // [Function("GenPDF")]
         public async Task Run(
          [ServiceBusTrigger("dep-genpdf", Connection = "Azure:ServiceBus:ConnectionString" , IsBatched =false) ]
             ServiceBusReceivedMessage message,

+ 1 - 0
TEAMModelOS.SDK/Models/Cosmos/Student/StudentArtResult.cs

@@ -101,6 +101,7 @@ namespace TEAMModelOS.SDK.Models
         public List<ArtQuotaPdf> allSubjectQuotas { get; set; } = new List<ArtQuotaPdf>();
         public List<ArtSubjectPdf> subjectPdfs { get; set; } = new List<ArtSubjectPdf>();
         public string blob { get; set;}
+        public string blobFullUrl {  get; set; }
     }
     /// <summary>
     /// 艺术评测科目PDF

+ 2 - 2
TEAMModelOS.SDK/Models/Service/ArtService.cs

@@ -82,7 +82,7 @@ namespace TEAMModelOS.SDK.Models.Service
             await _serviceBus.GetServiceBusClient().SendMessageAsync(GenPdfQueue, messageBlobPDF);
         }
 
-        public async static Task<(List<ArtStudentPdf> studentPdfs, List<StudentArtResult> artResults)> GenStuArtPDF(List<string> studentIds, string _artId,string _schoolId,string head_lang ,AzureCosmosFactory _azureCosmos, 
+        public async static Task<(List<ArtStudentPdf> studentPdfs, List<StudentArtResult> artResults)> GenStuArtPDF(List<string> studentIds, string _artId, ArtEvaluation art, string _schoolId,string head_lang ,AzureCosmosFactory _azureCosmos, 
               CoreAPIHttpService _coreAPIHttpService, DingDing _dingDing) {
             var client = _azureCosmos.GetCosmosClient();
 
@@ -138,7 +138,7 @@ namespace TEAMModelOS.SDK.Models.Service
                     comment_subject_painting = $"{_subject_painting}";
                 }
             }
-            ArtEvaluation art = await client.GetContainer(Constant.TEAMModelOS, "Common").ReadItemAsync<ArtEvaluation>($"{_artId}", new PartitionKey($"Art-{_schoolId}"));
+           
             ArtSetting artSetting = await client.GetContainer(Constant.TEAMModelOS, Constant.Normal).ReadItemAsync<ArtSetting>($"{school.areaId}", new PartitionKey($"ArtSetting"));
 
             var allExamIds = art.settings.SelectMany(x => x.task).Where(z => z.type == 1);

+ 113 - 86
TEAMModelOS.SDK/Models/Service/GenPDFService.cs

@@ -22,90 +22,16 @@ using TEAMModelOS.SDK.Models;
 using Microsoft.Extensions.Configuration;
 using System.Net.Http.Headers;
 using Azure.Storage.Sas;
+using TEAMModelOS.SDK.Models.Service;
+using Azure.Core;
+using TEAMModelOS.SDK.Models.Cosmos.Common;
+using System.Configuration;
 
 namespace TEAMModelOS.SDK
 {
     public static class GenPDFService
     {
-        public static async Task<(PDFGenRedis genRedis, PDFGenQueue genQueue,string msg )> SentTask(AzureRedisFactory _azureRedis, AzureStorageFactory _azureStorage) 
-        {
-            string msg = string.Empty;
-            //从尾部弹出元素,队列先进先出
-            long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
-            var queueValue = await _azureRedis.GetRedisClient(8).ListRightPopAsync("PDFGen:Queue");
-            PDFGenRedis genRedis = null;
-            PDFGenQueue genQueue = null;
-            if (queueValue!=default && queueValue.HasValue)
-            {
-                genQueue = queueValue.ToString().ToObject<PDFGenQueue>();
-                RedisValue redisValue = await _azureRedis.GetRedisClient(8).HashGetAsync($"PDFGen:{genQueue.sessionId}", genQueue.id);
-                if (redisValue!=default)
-                {
-                    genRedis = redisValue.ToString().ToObject<PDFGenRedis>();
-                    //计算等待了多久的时间才开始生成。
-                    var wait = now- genRedis.join;
-                    genRedis.wait  = wait;
-                    genRedis.status = 1;
-                    try
-                    {
-                        Uri uri = new Uri(genQueue.pageUrl);
-                        var query = HttpUtility.ParseQueryString(uri.Query);
-                        string? url = query["url"];
-                        if (!string.IsNullOrWhiteSpace(url))
-                        {
-                            url=  HttpUtility.UrlDecode(url);
-                            uri = new Uri(url);
-                            string host = uri.Host;
-                            var blobServiceClient = _azureStorage.GetBlobServiceClient();
-                            if (host.Equals(blobServiceClient.Uri.Host))
-                            {
-                                // 获取容器名,它是路径的第一个部分
-                                string containerName = uri.Segments[1].TrimEnd('/');
-                                // 获取文件的完整同级目录,这是文件路径中除了文件名和扩展名之外的部分
-                                // 由于文件名是路径的最后一个部分,我们可以通过连接除了最后一个部分之外的所有部分来获取目录路径
-                                string directoryPath = string.Join("", uri.Segments, 2, uri.Segments.Length - 3);
-                                string? fileName = Path.GetFileNameWithoutExtension(uri.AbsolutePath);
-                                string blobPath = $"{directoryPath}{fileName}.pdf";
-                                var urlSas = _azureStorage.GetBlobSAS(containerName, blobPath, BlobSasPermissions.Write, hour: 1);
-                                genQueue.blobSas =  urlSas.sas;
-                                genQueue.blobName=blobPath;
-                                genQueue.cntName=containerName;
-                                genQueue.blobFullUrl=urlSas.fullUri;
-                            }
-                            else
-                            {
-                                msg="数据地址与服务提供的站点不一致!";
-                                genRedis.status=3;
-                                genRedis.msg = msg;
-                              
-                            }
-                        }
-                        else
-                        {
-                            msg="数据地址解析异常!";
-                            genRedis.status=3;
-                            genRedis.msg = msg;
-                            
-                        }
-                    }
-                    catch (Exception ex)
-                    {
-                        msg=$"数据地址处理异常,异常信息:{ex.Message}";
-                        genRedis.status=3;
-                        genRedis.msg = ex.Message;
-                    }
-                    await _azureRedis.GetRedisClient(8).HashSetAsync($"PDFGen:{genQueue.sessionId}", genQueue.id, genRedis.ToJsonString());
-                }
-                else {
-                    msg="队列任务关联数据为空!";
-                }
-            }
-            else
-            {
-                msg="队列暂无任务!";
-            }
-            return (genRedis, genQueue, msg);
-        }
+       
 
 
 
@@ -431,15 +357,17 @@ namespace TEAMModelOS.SDK
 
 
 
+
+
+
         /// <summary>
         /// 加入PDF生成队列服务
         /// https://github.com/wuxue107/bookjs-eazy  https://github.com/wuxue107/screenshot-api-server
         /// </summary>
-        /// <param name="azureServiceBus"></param>
         /// <param name="azureRedis"></param>
         /// <param name="data"></param>
         /// <returns></returns>
-        public static async Task<(int total,int add )> AddGenPdfQueue(AzureServiceBusFactory azureServiceBus , AzureRedisFactory azureRedis, GenPDFData data) 
+        public static async Task<(int total,int add )> AddGenPdfQueue( AzureRedisFactory azureRedis, GenPDFData data) 
         {
             long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
             List<PDFGenRedis> genRedis = new List<PDFGenRedis>();
@@ -492,6 +420,7 @@ namespace TEAMModelOS.SDK
                         dbData.url= item.url;
                         dbData.scope = data.scope;
                         dbData.owner= data.owner;
+                        dbData.env= data.env;
                         genRedis.Add(dbData);
                     }
                 }
@@ -505,7 +434,8 @@ namespace TEAMModelOS.SDK
                         name=item.name,
                         url= item.url,
                         scope=data.scope,
-                        owner= data.owner
+                        owner= data.owner,
+                        env= data.env,
                     });
                 }
             }
@@ -524,6 +454,7 @@ namespace TEAMModelOS.SDK
                     sessionId= data.sessionId,
                     timeout =data.timeout,
                     name=item.name,
+                    env= data.env,
                 };
                 //string message = JsonSerializer.Serialize(genQueue);
                 //从头部压入元素,队列先进先出
@@ -540,6 +471,87 @@ namespace TEAMModelOS.SDK
             return ( countProcess , genRedis.Count() ); 
         }
 
+        public static async Task GenArtStudentPdf( AzureRedisFactory _azureRedis, AzureCosmosFactory _azureCosmos, 
+            CoreAPIHttpService _coreAPIHttpService, DingDing _dingDing, AzureStorageFactory _azureStorage, IConfiguration _configuration, JsonElement json)
+        {
+            try
+            {
+                json.TryGetProperty("studentIds", out JsonElement _studentIds);
+                json.TryGetProperty("artId", out JsonElement _artId);
+                json.TryGetProperty("schoolCode", out JsonElement _schoolId);
+                json.TryGetProperty("headLang", out JsonElement headLang);
+                List<string> studentIds = _studentIds.ToObject<List<string>>();
+                string _schoolCode = $"{_schoolId}";
+                ArtEvaluation art = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, "Common").ReadItemAsync<ArtEvaluation>($"{_artId}", new PartitionKey($"Art-{_schoolId}"));
+                (List<ArtStudentPdf> studentPdfs, List<StudentArtResult> artResults) = await ArtService.GenStuArtPDF(studentIds, $"{_artId}", art, $"{_schoolId}", $"{headLang}", _azureCosmos, _coreAPIHttpService, _dingDing);
+                foreach (var artResult in artResults)
+                {
+                    if (artResult.pdf == null || string.IsNullOrWhiteSpace(artResult.pdf.blob) || string.IsNullOrWhiteSpace(artResult.pdf.url))
+                    {
+                        artResult.pdf = new Attachment
+                        {
+                            // name = $"{artResult.studentId}.pdf",
+                            extension = "pdf",
+                            type = "doc",
+                            cnt = artResult.school,
+                            prime = false,//此处的作用是判断是否已经生成OK.
+                        };
+                    }
+                    else
+                    {
+                        artResult.pdf.prime = false;//此处的作用是判断是否已经生成OK.
+                    }
+                    await _azureRedis.GetRedisClient(8).HashSetAsync($"ArtPDF:{_artId}:{_schoolCode}", artResult.studentId, artResult.ToJsonString());
+                }
+                //2个小时。
+                await _azureRedis.GetRedisClient(8).KeyExpireAsync($"ArtPDF:{_artId}:{_schoolCode}", new TimeSpan(2, 0, 0));
+                List<Task<string>> uploads = new List<Task<string>>();
+                studentPdfs.ForEach(x => {
+                    x.blob = $"art/{x.artId}/report/{x.studentId}.json";
+                    var urlSas = _azureStorage.GetBlobSAS($"{_schoolCode}", x.blob, BlobSasPermissions.Write|BlobSasPermissions.Read, hour: 24);
+                    x.blobFullUrl=urlSas.fullUri;
+                    uploads.Add(_azureStorage.GetBlobContainerClient($"{_schoolCode}").UploadFileByContainer(x.ToJsonString(), "art", $"{x.artId}/report/{x.studentId}.json", true));
+                });
+                var uploadJsonUrls = await Task.WhenAll(uploads);
+                var list = uploadJsonUrls.ToList();
+                //List<string> urls = new List<string>();
+                //(string uri, string sas) = _azureStorage.GetBlobContainerSAS($"{_schoolCode}", Azure.Storage.Sas.BlobContainerSasPermissions.Read);
+                //studentPdfs.ForEach(x => {
+                //    string atrUrl = "https://teammodeltest.blob.core.chinacloudapi.cn/0-public/bookjs/art/index.html";
+                //    var s = _azureStorage.GetBlobSAS($"{_schoolCode}", x.blob, BlobSasPermissions.Read);
+                //    s.fullUri = $"{HttpUtility.UrlEncode($"{s}", Encoding.UTF8)}";
+                //    string url = $"{atrUrl}?url={s.fullUri}";
+                //    urls.Add(url);
+                //});
+                string env = ScreenConstant.env_release;
+                if (_configuration.GetValue<string>("Option:Location").Contains("Test", StringComparison.OrdinalIgnoreCase) ||
+                    _configuration.GetValue<string>("Option:Location").Contains("Dep", StringComparison.OrdinalIgnoreCase))
+                {
+                    env = ScreenConstant.env_develop;
+                }
+               var addData = await GenPDFService.AddGenPdfQueue(_azureRedis,
+                    new GenPDFData
+                    {
+                        env =env,
+                        timeout=30000,
+                        delay=1000,
+                        checkPageCompleteJs=true,
+                        sessionId=$"{_artId}",
+                        taskName = art.name,
+                        taskType="Art",
+                        owner=art.owner,
+                        scope=art.scope,
+                        pageUrl="https://teammodeltest.blob.core.chinacloudapi.cn/0-public/bookjs/art/index.html",
+                        datas= studentPdfs.Select(x => new PDFData{ id= x.studentId, name=x.studentName, url =x.blobFullUrl }).ToList()
+                    });
+                Console.WriteLine($"{addData.total},{addData.add}");
+            }
+            catch (Exception ex)
+            {
+                await _dingDing.SendBotMsg($"{ex.Message}{ex.StackTrace}", GroupNames.成都开发測試群組);
+            }
+        }
+
     }
 
     public class GenPDFData
@@ -593,6 +605,8 @@ namespace TEAMModelOS.SDK
         /// 数据范围
         /// </summary>
         public string scope { get; set; } 
+
+        public string env { get; set; }
     }
     public class GenPDFSchema 
     {
@@ -666,6 +680,10 @@ namespace TEAMModelOS.SDK
         /// 数据范围
         /// </summary>
         public string scope { get; set; }
+        /// <summary>
+        /// 环境变量
+        /// </summary>
+        public string env { get; set; }
     }
 
 
@@ -683,6 +701,10 @@ namespace TEAMModelOS.SDK
     /// </summary>
     public class PDFGenQueue 
     {
+        /// <summary>
+        /// 环境变量
+        /// </summary>
+        public string env { get; set;}
         /// <summary>
         /// 姓名
         /// </summary>
@@ -856,10 +878,15 @@ namespace TEAMModelOS.SDK
         public static readonly string error = "error";
         public static readonly string offline = "offline";
         public static readonly string grant_type = "bookjs_api";
-        /// <summary>
-        /// 冗余时间
-        /// </summary>
-        public static readonly long time_excess = 5000;
+
+
+        public static readonly string env_release = "release";
+        public static readonly string env_develop = "develop";
+                
+    /// <summary>
+    /// 冗余时间
+    /// </summary>
+    public static readonly long time_excess = 5000;
     }
     public enum MessageType {
         conn_success,//连接成功

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

@@ -60,6 +60,13 @@ namespace TEAMModelOS.SDK.Models.Service
             {
                 dict.Add("category[*]", category);
             }
+            if (request.TryGetProperty("doubleGray", out JsonElement doubleGray) && doubleGray.GetBoolean())
+            {
+                dict.Add("=.tLevel", -1);
+                dict.Add("=.pLevel", -1);
+                //dict.Add(">=.tScore", 70);
+                //dict.Add(">=.pScore", 70);
+            }
             if (request.TryGetProperty("doubleGreen", out JsonElement doubleGreen) && doubleGreen.GetBoolean())
             {
                 dict.Add("=.tLevel", 2);

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

@@ -8009,7 +8009,9 @@ const LANG_EN_US = {
         signup: 'Sign up',
         exam: 'Exam',
         other: 'Other',
-        activityPlace:'Place',        
+        activityPlace:'Place',                
+        teacher:'teacher',        
+        class:'class',
         fileUrl: 'FileUrl',
         joinStatus: 'Registration status',
         tmid: 'TEAM Model ID',

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

@@ -8010,7 +8010,9 @@ const LANG_ZH_CN = {
         signup: '报名',
         exam: '评量',
         other: '其他',
-        activityPlace:'地点',        
+        activityPlace:'地点',                
+        teacher:'老师',        
+        class:'班级',
         fileUrl: '档案连结',
         joinStatus: '报名状况',
         tmid: '醍摩豆帐号',

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

@@ -8010,7 +8010,9 @@ const LANG_ZH_TW = {
         signup: '報名',
         exam: '評量',
         other: '其他',
-        activityPlace:'地點',        
+        activityPlace:'地點',                
+        teacher:'老師',        
+        class:'班級',
         fileUrl: '檔案連結',
         joinStatus: '報名狀況',
         tmid: '醍摩豆帳號',

+ 8 - 0
TEAMModelOS/ClientApp/src/api/htcommunity.js

@@ -28,6 +28,14 @@ export default {
     jointExamFind: function(data) {
         return post('/joint/exam/find', data)
     },
+    // 取得學生評量結果
+    commonExamFindSummaryRecord: function(data) {
+        return post('/common/exam/find-summary-record', data)
+    },
+    // 新建變更統測評量
+    jointExamUpsert: function(data) {
+        return post('/joint/exam/upsert', data)
+    },    
     // 取得報名教師課程
     jointCourseFind: function (data) {
         return post('/joint/course/find', data)

+ 8 - 3
TEAMModelOS/ClientApp/src/components/student-web/EventView/EventList.vue

@@ -609,8 +609,13 @@ import ArtTestReport from "./EventContentTypeTemplate/PaperViewBox/ArtTestReport
                 this.isLoad = true
                 this.eventList.length = 0
                 this.eventShow.length = 0
-                this.isListNoItem = true;
+                this.isListNoItem = true
 
+                if(this.$route.query.subIds && this.courseNow.subjectList) {
+                    this.nowSub = this.courseNow.subjectList.findIndex(item => {
+                        return this.$route.query.subIds.includes(item.id) || this.$route.query.subIds.includes(item.bindId)
+                    })
+                }
                 let params = {
                     groupListIds: [],
                     subjects: [],
@@ -711,7 +716,7 @@ import ArtTestReport from "./EventContentTypeTemplate/PaperViewBox/ArtTestReport
                                 }
                             })
                         } else {
-                            if(this.$route.query.aId && this.courseNow.subjectList) {
+                            /* if(this.$route.query.aId && this.courseNow.subjectList) {
                                 let info = this.eventList.find(item => {
                                     return item.id === this.$route.query.aId
                                 })
@@ -720,7 +725,7 @@ import ArtTestReport from "./EventContentTypeTemplate/PaperViewBox/ArtTestReport
                                         return info.subjects.includes(item.id) || info.subjects.includes(item.bindId)
                                     })
                                 }
-                            }
+                            } */
                             // 2023.11.09 评测列表展示: 科目($sotre.state.user.curPeriod.subjects) + 班级
                             /* this.eventShow = this.eventList.filter(eveList => {
                                 if(this.courseNow.subjectList) {

+ 1 - 1
TEAMModelOS/ClientApp/src/components/student-web/HomeView/HomeView.vue

@@ -771,7 +771,7 @@ export default {
                     let path = item.type === 'Homework' ? '/studentWeb/homeworkView' : '/studentWeb/examView'
                     this.$router.push({
                         path,
-                        query: {aId: item.id}
+                        query: {aId: item.id, subIds: subjectIds}
                     })
                 }
             } else {

+ 9 - 0
TEAMModelOS/ClientApp/src/router/routes.js

@@ -759,6 +759,15 @@ export const routes = [{
             isKeep: true
         }
     },
+    {
+        path: 'htCreateEva',
+        name: 'htCreateEva',
+        component: () => import('@/view/learnactivity/htCreateEva.vue'),
+        meta: {
+            activeName: '',
+            isKeep: true
+        }
+    },    
     // 创建个人评测
     {
         path: 'createPrivExam',

+ 1 - 0
TEAMModelOS/ClientApp/src/store/module/config.js

@@ -103,6 +103,7 @@ export default {
                 stationSetting = {}
                 stationSetting.station = 'test'
                 stationSetting.srvAdr = 'China'
+                //stationSetting.srvAdr = 'Global'
                 //这里异常如果本地为 null 本地测试会重复设置为null
                 // if (localStorage.getItem('srvAdr')) stationSetting.srvAdr = localStorage.getItem('srvAdr')
             }

+ 1 - 1
TEAMModelOS/ClientApp/src/utils/blobTool.js

@@ -6,7 +6,7 @@ import store from '@/store'
 import FileSaver from "file-saver";
 import JSZip from "jszip";
 
-const BLOB_PATH = ['avatar', 'audio', 'doc', 'exam', 'image', 'elegant', 'item', 'notice', 'other', 'paper', 'syllabus', 'res', 'records', 'student', 'survey', 'temp', 'thum', 'video', 'vote', 'jyzx', 'train', 'yxpt', 'homework', 'policy', 'public', 'art', 'activity', 'banner']
+const BLOB_PATH = ['jointexam', 'avatar', 'audio', 'doc', 'exam', 'image', 'elegant', 'item', 'notice', 'other', 'paper', 'syllabus', 'res', 'records', 'student', 'survey', 'temp', 'thum', 'video', 'vote', 'jyzx', 'train', 'yxpt', 'homework', 'policy', 'public', 'art', 'activity', 'banner']
 const { BlobServiceClient } = require("@azure/storage-blob")
 
 //获取文件后缀和类型

+ 1 - 1
TEAMModelOS/ClientApp/src/view/Home.vue

@@ -455,7 +455,7 @@
 					if (this.teacherRoutes.includes(val.path)) {						
 						this.isBaseMgtExpire = false;
 					}
-					console.error("路由切换", val);
+					console.log("路由切换", val);
 					/* 判断学校管理模块是否到期 */
 					let schoolVersions = JSON.parse(decodeURIComponent(localStorage.school_profile || "{}", "utf-8"));
 					let isSchoolRoute = !this.teacherRoutes.includes(this.$route.path)									

+ 13 - 0
TEAMModelOS/ClientApp/src/view/evaluation/index/TestPaper.vue

@@ -75,6 +75,9 @@
         <TabPane :label="$t('evaluation.cpTip2')" name="name4" tab="newExerciseTab">
           <BasePaperItemPicker v-if="curModalTab === 'name4'" ref='paperPicker' :subjectCode="subjectCode" :periodCode="periodCode" :gradeCode="gradeCode"></BasePaperItemPicker>
         </TabPane>
+        <TabPane :label="$t('evaluation.cpTip3')" name="name5" tab="newExerciseTab">
+          <ManualCreateNew ref="syllabusPicker" :subjectCode="subjectCode" :periodCode="periodCode" :gradeCode="gradeCode" isAddModel></ManualCreateNew>
+        </TabPane>
       </Tabs>
       <div slot="footer">
         <Button @click="addNewModal = false">{{$t('evaluation.cancel')}}</Button>
@@ -104,11 +107,13 @@
 import blobTool from '@/utils/blobTool.js'
 import ExamPaperAnalysis from '@/view/learnactivity/ExamPaperAnalysis.vue'
 import ManualCreate from '@/view/learnactivity/ManualCreate.vue'
+import ManualCreateNew from '@/view/learnactivity/ManualCreateNew.vue'
 
 export default {
   components: {
     ManualCreate,
     ExamPaperAnalysis,
+    ManualCreateNew,
   },
   props: {
     paper: {
@@ -258,6 +263,8 @@ export default {
         this.$refs.bankPicker && (this.$refs.bankPicker.shoppingQuestionList = [])
         this.$refs.bankPicker && (this.$refs.bankPicker.$refs.exList.selectList = [])
         this.$refs.paperPicker && (this.$refs.paperPicker.checkList = [])
+        this.$refs.syllabusPicker && (this.$refs.syllabusPicker.shoppingQuestionList = [])
+        this.$refs.syllabusPicker && (this.$refs.syllabusPicker.$refs.syllabusTree.selectList = [])
       }
     },
     /* 退出预览 */
@@ -292,6 +299,9 @@ export default {
       } else if (this.curModalTab === 'name3') {
         let newItems = this.$refs.bankPicker.shoppingQuestionList
         this.onAddNewFinish(newItems)
+      } else if (this.curModalTab === 'name5') {
+        let newItems = this.$refs.syllabusPicker.shoppingQuestionList
+        this.onAddNewFinish(newItems)
       } else {
         this.$refs.newEdit.getContent(this.$refs.newEdit.exersicesType)
       }
@@ -708,6 +718,9 @@ export default {
             }
             this.viewModel = newValue.itemSort === 1 ? 'list' : 'type'
             console.log('TestPaper > paper', newValue)
+            if(newValue.createType === 'manual') {
+              this.$EventBus.$emit('onPaperItemChange', newValue.item)
+            }
           })
           this.curOrderTempIndex = newValue.orderTemp || 0
           this.paperInfo = newValue

+ 782 - 0
TEAMModelOS/ClientApp/src/view/evaluation/index/htTestPaper.vue

@@ -0,0 +1,782 @@
+<template>
+  <div class="paper-container">
+    <div class="back-to-top flex-col-center" :title="$t('evaluation.backToTop')" v-if="isShowBackToTop" @click="handleBackToTop">
+      <Icon type="ios-arrow-up" />
+    </div>
+    <div class="paper-main-wrap">
+      <!-- 试卷内容 -->
+      <div class="paper-content">
+        <div class="paper-body">
+          <!-- 试卷基础信息 -->
+          <div class="paper-base-info" v-show="!isPreview &&  !isPreviewItems">
+            <div style="display: flex;">
+              <div class="paper-info-items">
+                <span class="base-info-item">{{$t('evaluation.paperList.paperScore')}}:<span class="analysis-info" style="cursor: pointer;">{{ paperInfo.score }}</span>{{$t('evaluation.paperList.score')}}</span>
+                <span class="base-info-item" @click="onShowScoreTable" style="cursor: pointer;">{{$t('evaluation.paperList.alreadyScore')}}:<span class="analysis-info">{{ allocatedScore || 0 }}</span>{{$t('evaluation.paperList.score')}}</span>
+              </div>
+              <div class="paper-info-items">
+                <span class="base-info-item">{{$t('evaluation.filter.diff')}}:
+                  <Rate allow-half v-model="paperInfo.item.length ? +paperDiff : 0" disabled />
+                </span>
+                <span class="base-info-item">{{$t('evaluation.paperList.itemCount')}}:<span class="analysis-info">{{ paperInfo.item ? paperInfo.item.length : 0 }}</span></span>
+              </div>
+            </div>
+            <div v-if="isShowBaseInfo && !isExamPaper && !isPreviewItems">
+              <Button class="base-info-btn" type="info" @click="showPaperAttachments" v-if="isHiteachPaper">{{ $t('evaluation.quickPaper.attachments') }}</Button>
+              <Button class="base-info-btn" type="info" @click="addNewModal = true" v-show="!isShowAnalysis">{{ $t('evaluation.index.addExercise') }}</Button>
+              <Button class="base-info-btn" type="info" @click="onHandleToggle" v-show="paperInfo.item.length && !isShowAnalysis">{{ isAllOpen ? $t('evaluation.index.collapseAll') : $t('evaluation.index.openAll')}}</Button>
+              <Button class="base-info-btn" type="info" @click="onSetScoreByType" v-show="paperInfo.item.length && !isShowAnalysis">{{$t('evaluation.exerciseList.typeScore')}}</Button>
+              <Button class="base-info-btn" type="info" @click="onViewModelChange" v-show="paperInfo.item.length && !isShowAnalysis">{{ `${ viewModel === 'type' ? this.$t('evaluation.paperList.sortByOrder') : this.$t('evaluation.paperList.sortByType') }` }}</Button>
+              <Button class="base-info-btn" type="info" @click="isShowAnalysis = !isShowAnalysis" v-show="paperInfo.item.length">{{ isShowAnalysis ? this.$t('evaluation.paperList.paperDetails') : this.$t('evaluation.paperList.paperAnalysis')}}</Button>
+              <Button class="base-info-btn" type="info" v-show="paperInfo.item.length && paperInfo.id" @click="onChooseOrderTemp">{{ $t('evaluation.orderTemp') }}</Button>
+              <Button class="base-info-btn" type="info" @click="downloadSheet" :loading="downLoading" v-show="isMarkModel && paperInfo.item.length && paperInfo.id && paper.sheetNo">{{ $t('evaluation.paperList.goAnswerSheet') }}<span v-if="paperInfo.mode">-{{ paperInfo.mode }}</span></Button>
+              <Button class="base-info-btn" type="info" @click="goAnswerSheet" v-show="isMarkModel && paperInfo.item.length && paperInfo.id">{{ paper.sheetNo ?  $t('evaluation.paperList.reCreateSheet') : $t('evaluation.paperList.createSheet') }}</Button>
+            </div>
+            <div v-if="isExamPaper && !isPreviewItems && paperInfo.item.length">
+              <Button class="base-info-btn" type="info" @click="showPaperAttachments" v-show="!isChangePaper" v-if="isHiteachPaper">{{ $t('evaluation.quickPaper.attachments') }}</Button>
+              <!-- <Button class="base-info-btn" type="info" @click="showChangePaper" v-show="isExamInfoPaper && !isChangePaper">{{ $t('evaluation.quickPaper.modifyPaper') }}</Button> -->
+              <Button class="base-info-btn" type="info" @click="onHandleToggle" v-show="!isShowAnalysis && !isChangePaper">{{ isAllOpen ? $t('evaluation.index.collapseAll') : $t('evaluation.index.openAll')}}</Button>
+              <Button class="base-info-btn" type="info" @click="onViewModelChange" v-show="!isShowAnalysis && !isChangePaper">{{ `${ viewModel === 'type' ? this.$t('evaluation.paperList.sortByOrder') : this.$t('evaluation.paperList.sortByType') }` }}</Button>
+              <Button class="base-info-btn" type="info" @click="isShowAnalysis = !isShowAnalysis" v-show="!isHideAnalysis && !isChangePaper">{{ isShowAnalysis ? this.$t('evaluation.paperList.paperDetails') : this.$t('evaluation.paperList.paperAnalysis')}}</Button>
+              <Button class="base-info-btn" type="info" @click="downloadSheet" :loading="downLoading" v-show="isMarkModel && paperInfo.id && paper.sheetNo && !hideSheet && !isSharePreview && !isChangePaper">{{ $t('evaluation.paperList.goAnswerSheet') }}<span v-if="paperInfo.mode">-{{ paperInfo.mode }}</span></Button>
+              <Button class="base-info-btn" type="info" @click="goAnswerSheet" v-show="isMarkModel && paperInfo.id && !hideSheet && !isSharePreview && !isChangePaper">{{ paper.sheetNo ?  $t('evaluation.paperList.reCreateSheet') : $t('evaluation.paperList.createSheet') }}</Button>
+              <Button class="base-info-btn" type="info" @click="exitPreview" v-show="paperInfo.id && isSharePreview && !isChangePaper">{{ $t('evaluation.index.backList') }}</Button>
+              <Button class="base-info-btn" type="success" @click="showChangePaper(1)" v-show="isChangePaper">{{ $t('cusMgt.save') }}</Button>
+              <Button class="base-info-btn" @click="showChangePaper" v-show="isChangePaper">{{ $t('evaluation.cancel') }}</Button>
+            </div>
+          </div>
+          <!-- 试卷头部信息 -->
+          <div class="paper-header flex-col-center">
+            <p class="paper-title">{{paperInfo.name}}</p>
+          </div>
+          <!-- 试卷分析部分 -->
+          <ExamPaperAnalysis :testPaper="paperInfo" v-if="isShowAnalysis" :hidePie="hidePie">
+          </ExamPaperAnalysis>
+          <!-- 题目类型及列表 -->
+          <BaseExerciseList :paper="paperInfo" @dataUpdate="onListUpdate" v-show="!isShowAnalysis" ref="exList" :isChangePaper="isChangePaper" :isShowTools="!isPreview" :canFix="canFix" :isExamPaper="isExamPaper || isPreviewItems" @toggleChange="onToggleChange" @scoreUpdate="scoreUpdate"></BaseExerciseList>
+        </div>
+        <!-- <BaseHiteachPaper v-if="isHiteachPaper" :paper="paperInfo"></BaseHiteachPaper> -->
+      </div>
+    </div>
+
+    <!-- 新建试题 -->
+    <Modal v-model="addNewModal" @on-visible-change="onAddNewModalChange" :mask-closable="false" :title="$t('evaluation.index.addExercise')" width="1000px" class="related-point-modal edit-exercise-modal">
+      <Tabs value="name1" name="newExerciseTab" @on-click="onTabChange" :animated="false">
+        <TabPane :label="$t('evaluation.autoCreate')" name="name1" tab="newExerciseTab">
+          <BaseCreateChild @addFinish="onAddNewFinish" ref="newEdit" v-if="addNewModal" :curPeriodIndex="paperInfo.paperPeriod" :curSubjectIndex="paperInfo.paperSubject">
+          </BaseCreateChild>
+        </TabPane>
+        <!-- <TabPane :label="$t('evaluation.quickCreate')" name="name2" tab="newExerciseTab" v-if="isDevEnv">
+          <BasePasteTool v-if="addNewModal" @addFinish="onAddNewFinish"></BasePasteTool>
+        </TabPane> -->
+        <TabPane :label="$t('evaluation.cpTip1')" name="name3" tab="newExerciseTab">
+          <ManualCreate ref="bankPicker" :subjectCode="subjectCode" :periodCode="periodCode" :gradeCode="gradeCode" :isMarkModel="isMarkMode"></ManualCreate>
+        </TabPane>
+        <TabPane :label="$t('evaluation.cpTip2')" name="name4" tab="newExerciseTab">
+          <BasePaperItemPicker v-if="curModalTab === 'name4'" ref='paperPicker' :subjectCode="subjectCode" :periodCode="periodCode" :gradeCode="gradeCode"></BasePaperItemPicker>
+        </TabPane>
+      </Tabs>
+      <div slot="footer">
+        <Button @click="addNewModal = false">{{$t('evaluation.cancel')}}</Button>
+        <Button type="success" :loading="editLoading" @click="doSaveEdit">{{$t('evaluation.confirm')}}</Button>
+      </div>
+    </Modal>
+
+    <!-- 选择题号模板 -->
+    <Modal v-model="orderTempModal" :title="$t('evaluation.orderTempChoos')" width="600px" class="related-point-modal order-temp-modal" @on-ok="onConfirmOrderTemp">
+      <div :class="['order-temp-item',curOrderTempIndex === 0 ? 'order-temp-item-active' : '']" @click="onClickOrderTemp(0)">
+        <p>{{ $t('evaluation.order1') }} <span class="order-temp-item-info">({{ $t('evaluation.orderTip1') }})</span> </p>
+      </div>
+      <div :class="['order-temp-item',curOrderTempIndex === 1 ? 'order-temp-item-active' : '']" @click="onClickOrderTemp(1)">
+        <p>{{ $t('evaluation.order2') }} <span class="order-temp-item-info">({{ $t('evaluation.orderTip2') }})</span></p>
+      </div>
+      <div :class="['order-temp-item',curOrderTempIndex === 2 ? 'order-temp-item-active' : '']" @click="onClickOrderTemp(2)">
+        <p>{{ $t('evaluation.order3') }} <span class="order-temp-item-info">({{ $t('evaluation.orderTip3') }})</span></p>
+      </div>
+      <!-- <div :class="['order-temp-item',curOrderTempIndex === 3 ? 'order-temp-item-active' : '']" @click="onClickOrderTemp(3)">
+				<p>中文大写 <span class="order-temp-item-info">(例:壹、贰、叁、肆、伍......)</span></p>
+			</div> -->
+    </Modal>
+  </div>
+</template>
+<script>
+import blobTool from '@/utils/blobTool.js'
+import ExamPaperAnalysis from '@/view/learnactivity/ExamPaperAnalysis.vue'
+import ManualCreate from '@/view/learnactivity/ManualCreate.vue'
+
+export default {
+  components: {
+    ManualCreate,
+    ExamPaperAnalysis,
+  },
+  props: {    
+    jointGroupId: {
+      type: String,
+      default: ''
+    },
+    jointScheduleId: {
+      type: String,
+      default: ''
+    },
+    paper: {
+      type: Object,
+      default: null
+    },
+    /* 是否阅卷专用 */
+    isMarkMode: {
+      type: Boolean,
+      default: false
+    },
+    isShowTools: {
+      type: Boolean,
+      default: true
+    },
+    isPreview: {
+      type: Boolean,
+      default: false
+    },
+    isShowBaseInfo: {
+      type: Boolean,
+      default: true
+    },
+    isExamPaper: {
+      type: Boolean,
+      default: false
+    },
+    isExamInfoPaper: { //评测页面下的试卷,需调整答案、配分等
+      type: Boolean,
+      default: false
+    },
+    hidePie: {
+      type: Boolean,
+      default: false
+    },
+    isHideAnalysis: {
+      type: Boolean,
+      default: false
+    },
+    isPreviewItems: {
+      type: Boolean,
+      default: false
+    },
+    hideSheet: {
+      type: Boolean,
+      default: false
+    },
+    isSharePreview: {
+      type: Boolean,
+      default: false
+    },
+    canFix: {
+      type: Boolean,
+      default: false
+    },
+    gradeCode: {
+      type: Array,
+      default: () => []
+    },
+    subjectCode: {
+      type: String,
+      default: ''
+    },
+    periodCode: {
+      type: String,
+      default: ''
+    },
+    activityIndex: {
+        type: Number,
+        default: 0
+    },
+    subjectIndex: {
+        type: Number,
+        default: 0
+    },
+    refreshExam: {
+        type: Function,
+        require: true,
+        default: null
+    },
+  },
+  data() {
+    return {
+      isHiteachPaper: false,
+      isMarkModel:false,
+      editLoading: false,
+      hasModify: false,
+      curOrderTempIndex: 0,
+      orderTempModal: false,
+      isShowPasteTool: false,
+      addNewModal: false,
+      scoreTableModal: false,
+      isShowAnswerSheet: false,
+      isShowBackToTop: false,
+      isAllOpen: false,
+      isShowAnalysis: false,
+      isSetPaperName: false,
+      isSetScore: false,
+      isSetRules: false,
+      isShowSave: false,
+      exersicesList: [],
+      allocatedScore: 0,
+      list: [],
+      schoolInfo: {},
+      paperInfo: {
+        name: "",
+        score: 100,
+        answers: [],
+        multipleRule: 1,
+        item: [],
+        paperSubject: 0,
+        paperPeriod: 0
+      },
+      paperInfoOld: {},
+      exersicesType: this.$GLOBAL.EXERCISE_TYPES(),
+      exersicesDiff: this.$GLOBAL.EXERCISE_DIFFS(),
+      exersicesField: this.$GLOBAL.EXERCISE_LEVELS(),
+      diffColors: ['#32CF74', '#E8BE15', '#F19300', '#EB5E00', '#D30000'],
+      filterType: '0',
+      filterDiff: '0',
+      filterSort: '0',
+      pageSize: 5,
+      pageNum: 1,
+      totalNum: 100,
+      allList: [],
+      isShowAnswer: false,
+      isShowPart: false,
+      isShowConcept: false,
+      isShowTitle: true,
+      isShowInfo: false,
+      downLoading: false,
+      analysisTableData: [],
+      viewModel: 'type',
+      paperDiff: 0,
+      curModalTab: '',
+      isChangePaper: false,
+    }
+  },
+  methods: {
+    getSelectedQuestion(val) {
+      // this.onAddNewFinish(val.item)
+      // this.paperInfo.item = this._.cloneDeep(val.item)
+
+    },
+    onAddNewModalChange(val) {
+      if (val) {
+        this.$refs.bankPicker && (this.$refs.bankPicker.shoppingQuestionList = [])
+        this.$refs.bankPicker && (this.$refs.bankPicker.$refs.exList.selectList = [])
+        this.$refs.paperPicker && (this.$refs.paperPicker.checkList = [])
+      }
+    },
+    /* 退出预览 */
+    exitPreview() {
+      this.$emit('exitPreview')
+    },
+    async showPaperAttachments(){
+      let paper = this.paperInfo
+      if(!paper.attachments.length){
+        this.$Message.warning(this.$t('homework.noAttachments'))
+        return
+      }
+      console.error(paper)
+      // let curScope = paper.examScope || paper.scope
+      let curScope = paper.scope // 个人评测使用校本试卷 examScope会是 private 所以改为直接读取 scope
+      let blobHost = curScope === 'school' ? JSON.parse(decodeURIComponent(localStorage.school_profile, "utf-8")).blob_uri : JSON.parse(decodeURIComponent(localStorage.user_profile, "utf-8")).blob_uri
+      let sasString = curScope === 'school' ? await this.$tools.getSchoolSas() : await this.$tools.getPrivateSas()
+      let fullImgArr = paper.attachments?.map(imgName => blobHost + paper.blob + '/' + imgName + sasString.sas)
+      this.$hevueImgPreview({
+					multiple: true,
+					keyboard: true,
+					nowImgIndex: 0,
+					imgList: fullImgArr
+				});
+    },
+    /* 手动保存 */
+    doSaveEdit() {
+      this.editLoading = true
+      if (this.curModalTab === 'name4') {
+        let newItems = this.$refs.paperPicker.checkList
+        this.onAddNewFinish(newItems)
+      } else if (this.curModalTab === 'name3') {
+        let newItems = this.$refs.bankPicker.shoppingQuestionList
+        this.onAddNewFinish(newItems)
+      } else {
+        this.$refs.newEdit.getContent(this.$refs.newEdit.exersicesType)
+      }
+    },
+    onTabChange(val) {
+      this.curModalTab = val
+      // if (val === 'name1') {
+      // 	this.isShowPasteTool = true
+      // }
+    },
+    onClickOrderTemp(val) {
+      this.curOrderTempIndex = val
+    },
+    onChooseOrderTemp() {
+      this.orderTempModal = true
+    },
+    onConfirmOrderTemp() {
+      this.$refs.exList.onConfirmOrderTemp(this.curOrderTempIndex)
+    },
+    /* 下载Blob答题卡 */
+    async downloadSheet() {
+      this.downLoading = true
+      let examId = this.paperInfo.examId
+      console.error(this.paperInfo)
+      let sheetMode = this.paperInfo.mode ? `(${this.paperInfo.mode})` : ``
+      let fileName = `${this.paperInfo.name}-${this.paperInfo.sheetNo}${sheetMode}.pdf`
+      // let pdfName = `${this.paperInfo.name}-${this.paperInfo.sheetNo}.pdf`
+      let path = examId ? `exam/${examId}/paper/${this.paperInfo.subjectId}/` : `paper/${this.paperInfo.name}/`
+      let curScope = examId ? this.paperInfo.examScope : this.paperInfo.scope
+      let cntr = curScope === 'school' ? this.$store.state.userInfo.schoolCode : this.$store.state.userInfo.TEAMModelId
+      let sasData = curScope === 'school' ? await this.$tools.getSchoolSas() : await this.$tools.getPrivateSas()
+      let blobHost = this.$evTools.getBlobHost()
+      let fullPath = blobHost + '/' + cntr + '/' + path + fileName + sasData.sas
+      const downloadRes = async () => {
+        let response = await fetch(fullPath); // 内容转变成blob地址
+        if (response.status !== 200) {
+          this.$Message.warning(this.$t('answerSheet.sheetTip5'))
+        } else {
+          let blob = await response.blob();  // 创建隐藏的可下载链接
+          let objectUrl = window.URL.createObjectURL(blob);
+          let a = document.createElement('a');
+          a.href = objectUrl;
+          a.download = fileName;
+          a.click()
+          a.remove();
+        }
+        this.downLoading = false
+      }
+      downloadRes();
+      console.log(fullPath);
+    },
+    goAnswerSheet() {
+      console.log(this.paperInfo)
+
+      if (this.paperInfo.itemSort === 1) {
+        this.$Message.warning(this.$t('answerSheet.noCreateTip'))
+        return
+      }
+      if (!this.isExamPaper) {
+        localStorage.setItem('c_edit_paper', JSON.stringify(this.paperInfo))
+      }
+      this.$store.commit('clearAllConfig')
+      this.$router.push({
+        name: 'answerSheet',
+        params: {
+          paper: this.paperInfo
+        }
+      })
+    },
+    onShowScoreTable() {
+      this.$refs.exList.scoreTableModal = true
+    },
+    /* 新增题目回调 */
+    onAddNewFinish(item) {
+      sessionStorage.setItem('isSave', 0)
+      if (Array.isArray(item)) {
+        if(this.isMarkMode && item.some(i => i.type === 'compose')){
+          this.$Message.warning(this.$t('evaluation.markMode.tip2'))
+        }
+        let noComposeItems = item.filter(o => o.type !== 'compose')
+        this.paperInfo.item.push(...noComposeItems)
+      } else {
+        if(this.isMarkMode && item.type === 'compose'){
+          this.$Message.warning(this.$t('evaluation.markMode.tip2'))
+        }else{
+          this.paperInfo.item.push(item)
+        }
+      }
+      this.addNewModal = false
+      this.editLoading = false
+    },
+    /**
+     * 标题切换
+     * @param e
+     */
+    titleChange(e) {
+      this.paperInfo.name = e.target.innerHTML
+    },
+
+    handleBackToTop() {
+      console.log(this)
+      this.$EventBus.$emit('onBackToTop')
+    },
+
+    onSetMarkingRules() {
+      this.isSetRules = false
+    },
+
+    onShowAnalysis() {
+      this.isShowAnalysis = true
+    },
+
+    /** 切换全部展开与折叠 */
+    onHandleToggle() {
+      this.$refs.exList.onHandleToggle(this.isAllOpen)
+      this.isAllOpen = !this.isAllOpen
+    },
+
+    /**
+     * exList的collapseList变化
+     * @param list
+     */
+    onToggleChange(list) {
+      this.isAllOpen = list.length !== 0
+    },
+
+    /** 返回试卷库 */
+    onBackToBank() {
+      this.$router.push({
+        name: 'testPaperList',
+        params: {
+          tabName: 'paper'
+        }
+      })
+    },
+
+    /* 清空试卷试题 */
+    onCleanPaper() {
+      this.paperInfo.item = []
+      this.$refs.exList.errorList = []
+      this.$refs.exList.noAnswerList = []
+      this.$EventBus.$emit('onPaperItemChange', [])
+    },
+
+    onSetScore() {
+      this.isSetScore = false
+      this.$refs.exList.exerciseList.map(item => item.score = 0)
+    },
+
+    /**
+     * 组件更新删除题目时的处理
+     * @param list
+     */
+    onListUpdate(list) {
+      this.paperInfo.item = list
+      this.hasModify = true
+      console.log(list);
+    },
+    /** 重新挑题 */
+    goToPickExercise() {
+      this.$router.push({
+        name: 'exercisesList',
+        params: {
+          paperInfo: this.paperInfo
+        }
+      })
+    },
+
+    /** 点击题型配分 */
+    onSetScoreByType() {
+      this.$refs.exList.onSetScoreByType()
+    },
+
+
+    /**
+     * 试卷的视图模式切换
+     * @param val
+     */
+    onViewModelChange(val) {
+      if(this.isMarkMode){
+        this.$Message.warning(this.$t('evaluation.markMode.tip3'))
+        return
+      }
+      this.viewModel = this.viewModel === 'type' ? 'list' : 'type'
+      this.$refs.exList.viewModel = this.viewModel
+      this.$refs.exList.collapseList = []
+      // this.$refs.exList.doRenderMathJax()
+      this.$emit('onViewModelChange', this.viewModel)
+    },
+
+    /**
+     * 计算试卷题目平均难度
+     * @param arr 试题集合
+     */
+    handleDiffCalc(arr) {
+      let levelArr = arr.map(i => i.level)
+      return this._.meanBy(levelArr).toFixed(1)
+    },
+
+    scoreUpdate(score) {
+      this.allocatedScore = this.paperInfo.score - score
+    },
+    async showChangePaper(type) {
+        if(type === 1) {// 修改試卷 > 儲存按鈕
+            if(this.allocatedScore != this.paperInfo.score) {
+                this.$Message.warning(this.$t('evaluation.paperList.noCompleteScoreTip'))
+                return
+            }
+            
+            // 活動板修改試卷 先暫停
+            // let params1 = {
+            //     jointEventId: this.$route.params.data.id,
+            //     jointGroupId: this.jointGroupId,
+            //     jointScheduleId: this.jointScheduleId,
+            //     jointExamId: this.paperInfo.examId,                                            
+            //     papers: this.paperInfo
+            // }
+            // 重新保存试卷 && 调接口保存答案、配分
+            let params = {
+                id: this.paperInfo.examId, //活动ID
+                code: this.paperInfo.examScope === 'school' ? this.paperInfo.examSchool : this.paperInfo.creatorId, //个人ID或者学校编码
+                scode: this.paperInfo.examCode, //原本评测活动的完整code:'Exam-hbcn'
+                paperId: this.paperInfo.id, //试卷ID
+                //subjectId: this.paperInfo.examSubject[this.subjectIndex]?.id, //科目ID
+                paperAns: [], //作答记录的完整数组
+                point: [], //完整配分的数组
+                kno: [], //知识点
+                multipleRule: this.paperInfo.multipleRule, //评分规则
+            }
+            this.paperInfo.item.forEach(item => {
+              if(item.type === 'compose') {
+                item.children.forEach(child => {
+                  if(child.type === 'single' || child.type === 'multiple' || child.type === 'judge') {
+                    params.paperAns.push(child.answer)
+                  } else {
+                    params.paperAns.push([])
+                  }
+                  params.point.push(child.score)
+                  params.kno.push(child.knowledge)
+                })
+              } else {
+                if(item.type === 'single' || item.type === 'multiple' || item.type === 'judge') {
+                  params.paperAns.push(item.answer)
+                } else {
+                  params.paperAns.push([])
+                }
+                params.point.push(item.score)
+                params.kno.push(item.knowledge)
+              }
+            })            
+            //this.paperInfo.slides 分數是未更新前的   this.paperInfo.item 分數是更新後的
+            const itemJsonFile111 = await this.$evTools.createBlobPaper(this.paperInfo, this.paperInfo.slides);
+           
+            this.$api.htcommunity.jointExamUpsert(params).then(async res => {
+              if(res.code === 200) {
+                const itemJsonFile = await this.$evTools.createBlobPaper(this.paperInfo, this.paperInfo.slides);
+                // let blobPaper = await this.$evTools.createBlobPaper(paperItem, res.slides)
+                // 首先保存新题目的JSON文件到Blob 然后返回URL链接
+                let paperFile = new File([JSON.stringify(itemJsonFile)], "index.json");
+                // 获取初始化Blob需要的数据
+                let sasData = this.paperInfo.scope === 'school' ? await this.$tools.getSchoolSas() : await this.$tools.getPrivateSas()
+                //初始化Blob
+                let containerClient = new blobTool(sasData.url, sasData.name, sasData.sas, this.paperInfo.scope)
+                try {
+                    let promiseArr = []
+                    let blobFile = null
+                    let blobUrl = this.paperInfo.blob.slice(1)
+                    // 放入试题json文件
+                    for (let i = 0; i < this.paperInfo.item.length; i++) {
+                        promiseArr.push(new Promise(async (r, j) => {
+                            try {
+                                let item = this.paperInfo.item[i]
+                                const itemJsonFile = await this.$evTools.createBlobItem(item)
+                                let file = new File([JSON.stringify(itemJsonFile)], item.id + ".json");
+                                containerClient.upload(file, {
+                                    path: blobUrl,
+                                    checkSize: false
+                                }).then(res => {
+                                    r(200)
+                                })
+                            } catch (e) {
+                                console.log(e)
+                            }
+                        }))
+                        if(this.paperInfo.item[i].type === 'compose') {
+                          this.paperInfo.item[i].children.forEach(child => {
+                            promiseArr.push(new Promise(async (r, j) => {
+                              try {
+                                  let item = child
+                                  const itemJsonFile = await this.$evTools.createBlobItem(item)
+                                  let file = new File([JSON.stringify(itemJsonFile)], item.id + ".json");
+                                  containerClient.upload(file, {
+                                      path: blobUrl,
+                                      checkSize: false
+                                  }).then(res => {
+                                      r(200)
+                                  })
+                              } catch (e) {
+                                  console.log(e)
+                              }
+                          }))
+                          })
+                        }
+                    }
+                    // 放入index.json文件
+                    promiseArr.push(new Promise(async (r, j) => {
+                        try {
+                            blobFile = await containerClient.upload(paperFile, {
+                                path: blobUrl,
+                                checkSize: false
+                            })
+                            console.log('上传到试卷目录下', blobFile)
+                            r(blobFile)
+                        } catch (e) {
+                            j(e)
+                            this.$Message.error(e.spaceError)
+                            this.isLoading = false
+                        }
+                    }))
+                    // 不存在多媒体
+
+                    // 进行试卷文件上传Blob 先上传所有题目 再上传index.json文件
+                    Promise.all(promiseArr).then(async result => {
+                        try {
+                            this.refreshExam(this.paperInfo, this.subjectIndex)
+                            if (blobFile.blob) {
+                                // 试卷保存更新后需要重新生成答题卡
+                                /* paperItem.blob = blobFile.blob.split('/index.json')[0]
+
+                                let params = {
+                                    paper: await this.$evTools.createCosmosPaper(paperItem),
+                                    option: this.isEditPaper ? 'update' : 'insert'
+                                }
+                                //  保存试卷到cosmos
+                                this.$api.learnActivity.SaveExamPaper(params).then(res => {
+                                    if (res.error == null) {
+                                        this.$Message.success(this.$t('evaluation.paperList.saveSuc'))
+                                        this.isLoading = false
+                                        this.$Spin.hide()
+                                        // this.savePeriodInfos()
+                                        // 清空已选试题
+                                        this.showQues = false
+                                        this.selectedArr = []
+                                        this.groupList = []
+                                        this.orderList = []
+                                        this.selShowList = []
+                                    } else {
+                                        this.$Message.error(this.$t('evaluation.paperList.saveFail'))
+                                        this.$Spin.hide()
+                                        this.isLoading = false
+                                    }
+                                },err => {
+                                    this.$Message.error(this.$t('evaluation.paperList.saveFail'))
+                                    this.isLoading = false
+                                    this.$Spin.hide()
+                                }) */
+                            } else {
+                                console.error(blobFile)
+                            }
+                        } catch (e) {
+                            console.log(e)
+                        }
+                        this.$Message.success(this.$t("evaluation.editSuc"))
+                        this.isChangePaper = !this.isChangePaper
+                    })
+                } catch (e) {
+                    console.log(e)
+                    this.$Message.error(this.$t('evaluation.paperList.saveItemsFailTip'))
+                    this.isLoading = false
+                    this.isChangePaper = !this.isChangePaper
+                }
+              } else {
+                this.$t('learnActivity.mgtScEv.updErr')
+              }
+            })
+        } else {
+            this.isChangePaper = !this.isChangePaper
+            if(!this.isChangePaper) {
+              this.paperInfo = JSON.parse(JSON.stringify(this.paperInfoOld))
+              this.paper.item = this.paperInfo.item
+              this.$forceUpdate()
+            }
+        }
+    },
+
+
+  },
+  mounted() {
+    // this.isShowSave = window.location.pathname === '/home/evaluation/testPaper'
+    this.$Spin.hide()
+    let paper = this.paper || this.$route.params.paper || JSON.parse(localStorage.getItem('c_edit_paper'))
+    if (!paper || !paper.item) return
+    this.paperInfoOld = JSON.parse(JSON.stringify(paper))
+    console.log('xxxx', paper)
+    this.$EventBus.$emit('getNewPaper', paper)
+    // localStorage.setItem('_paperInfo', JSON.stringify(paper))
+    this.viewModel = paper.itemSort === 1 ? 'list' : 'type'
+    this.paperInfo = paper // 自己页面的值
+    this.paperDiff = paper.item ? this.handleDiffCalc(paper.item) : 0
+    this.curOrderTempIndex = paper.orderTemp || 0
+  },
+  computed: {
+    isAuto() {
+      return this.paperInfo.markConfig ? this.paperInfo.markConfig.auto : false
+    },
+    ruleType() {
+      return this.paperInfo.markConfig ? this.paperInfo.markConfig.type : 0
+    },
+    isSchool() {
+      return this.$route.name === 'newSchoolPaper'
+    },
+    isDevEnv() {
+      return process.env.NODE_ENV === 'development'
+    }
+  },
+  watch: {
+    paper: {
+      handler(newValue, oldValue) {
+        if (newValue.item && newValue.item.length) {
+          this.$nextTick(() => {            
+            this.isHiteachPaper = newValue.qamode === 1 // 是否是快速组卷或者Hiteach的课中试卷
+            this.isMarkModel = newValue.markModel === 1 // 是否是阅卷专用
+            if (!this.isHiteachPaper) {
+              this.$EventBus.$emit('getNewPaper', newValue)
+            }
+            this.viewModel = newValue.itemSort === 1 ? 'list' : 'type'
+            console.log('TestPaper > paper', newValue)
+          })
+          this.curOrderTempIndex = newValue.orderTemp || 0          
+          this.paperInfo = newValue
+        }
+        this.paperDiff = newValue.item ? this.handleDiffCalc(newValue.item) : 4
+      },
+      immediate: true,
+      deep: true
+    },
+    activityIndex: {
+      handler(n, o) {
+        if(this.isExamInfoPaper) this.isChangePaper = false
+        this.paperInfoOld = JSON.parse(JSON.stringify(this.paper))
+      }
+    },
+    subjectIndex: {
+      handler(n, o) {
+        if(this.isExamInfoPaper) this.isChangePaper = false
+        this.paperInfoOld = JSON.parse(JSON.stringify(this.paper))
+      }
+    }
+  }
+}
+</script>
+<style src="../index/TestPaper.less" lang="less" scoped>
+</style>
+<style lang="less">
+.complete-line {
+  padding: 0 45px;
+  border-bottom: 2px solid rgb(128, 128, 128);
+}
+
+.order-temp-modal {
+  .order-temp-item {
+    margin: 10px 0;
+    border: 1px solid #dddddd;
+    padding: 20px 15px;
+    border-radius: 5px;
+    font-size: 16px;
+    font-weight: bold;
+    cursor: pointer;
+    &-info {
+      font-size: 14px;
+      font-weight: 400;
+      margin-left: 15px;
+    }
+
+    &-active {
+      background-color: #72cbff;
+      color: #fff;
+    }
+  }
+}
+</style>

+ 7 - 0
TEAMModelOS/ClientApp/src/view/htcommunity/htMgtExam.less

@@ -176,4 +176,11 @@
     &:hover{
         color: #2d8cf0;
     }
+}
+.selected,
+.fistTag:first-child {
+  background: white;
+  color: #606266;
+  font-weight: bolder;
+  border: white 2px solid;
 }

Fichier diff supprimé car celui-ci est trop grand
+ 862 - 846
TEAMModelOS/ClientApp/src/view/htcommunity/htMgtExam.vue


+ 2 - 2
TEAMModelOS/ClientApp/src/view/htcommunity/htMgtHome.vue

@@ -18,8 +18,8 @@
             </el-table-column>
             <el-table-column :label="this.$t('htcommunity.eventExam')">
                 <template slot-scope="scope">
-                    <!-- <el-button type="text" @click="viewExams(scope.row)">{{$t("htcommunity.view")}}</el-button> -->
-                    <el-button type="text" @click="return false">{{$t("htcommunity.view")}}</el-button>
+                    <el-button type="text" @click="viewExams(scope.row)">{{$t("htcommunity.view")}}</el-button>
+                    <!-- <el-button type="text" @click="return false">{{$t("htcommunity.view")}}</el-button> -->
                 </template>
             </el-table-column>
         </el-table>

+ 1 - 0
TEAMModelOS/ClientApp/src/view/learnactivity/CreatePrivEva.vue

@@ -483,6 +483,7 @@
 								}
 								return;
 							}
+							debugger
 							// 挑选的是校本试卷
 							if (item.scope == "school") {
 								if (!schoolBlob) schoolBlob = new BlobTool(schoolSas.url, schoolSas.name, schoolSas.sas, "school");

+ 23 - 7
TEAMModelOS/ClientApp/src/view/learnactivity/ManualCreateNew.vue

@@ -1,6 +1,6 @@
 <template>
     <div style="height: 100%; position: relative">
-		<Poptip padding="0px 0px" offset="10" placement="top-end" width="300" trigger="hover" :class="shoppingCarClass">
+		<Poptip padding="0px 0px" offset="10" placement="top-end" width="300" trigger="hover" :class="shoppingCarClass" v-if="!isAddModel">
 			<Badge type="primary" :count="shoppingQuestionList.length" show-zero :offset="[-3, -3]" @mouseenter.native="changeActive($event)" @mouseleave.native="removeActive($event)">
 				<Icon custom="iconfont icon-sp-car" color="#05C19C" size="50" />
 			</Badge>
@@ -58,7 +58,7 @@
 		<vuescroll ref="manualScroll">
 			<div class="manual-filter-wrap">
 				<Col style="width: 100%">
-                    <div class="manual-filter-item">
+                    <div class="manual-filter-item" v-if="!isAddModel">
                         <span class="manual-filter-label">{{ $t('learnActivity.mgtScEv.ftType') }}</span>
                         <Select v-model="manualFilter.source" size="small" style="display: inline-block; width: 150px" @on-change="checkAll($event, 'source')">
                             <Option value="quesBank">{{ $t('evaluation.cpTip1') }}</Option>
@@ -170,14 +170,20 @@ export default {
         ExerciseList, BasePaperItemPicker, SyllabusPicker
     },
     props: {
+        /* 是否阅卷专用 */
         isMarkModel: {
             type: Boolean,
             default: false
         },
+        /* 是否添加题目弹窗中显示 */
+        isAddModel: {
+            type: Boolean,
+            default: false
+        },
         selQue: {
             type: Array,
             default: () => {
-                return [];
+                return []
             }
         },
         gradeCode: {
@@ -243,6 +249,9 @@ export default {
         }
     },
     mounted() {
+        if(this.isAddModel) {
+            this.manualFilter.source = 'syllabus'
+        }
         console.error(this.selQue);
         if (this.selQue.length) {
             let data = this.selQue;
@@ -350,7 +359,7 @@ export default {
                 }
             },
             deep: true
-        }
+        },
     },
     methods: {
         goToPreview() {
@@ -399,6 +408,13 @@ export default {
                 return
             }
 
+            if(key === 'source') {
+                this.tags = []
+                this.manualFilter.tag = ["all"]
+                this.volumeList = []
+                this.manualFilter.volume = ''
+            }
+
             // 切换其余筛选项
             if (this.manualFilter[key].length !== 1 && this.manualFilter[key].indexOf("all") === 0) {
                 this.manualFilter[key].splice(this.manualFilter[key].indexOf("all"), 1);
@@ -448,9 +464,6 @@ export default {
                 } else if(this.manualFilter.source === 'paper') {
                     this.paperList = resultList
                     this.originList = this._.cloneDeep(resultList)
-                    if (resultList.length > 0) {
-                        this.tags = [...new Set(resultList.map(i => i.tags).flat(1))]
-                    }
                 }
                 if(key === 'source') {
                     if (this.$refs.exList) {
@@ -462,6 +475,9 @@ export default {
                     if(this.$refs.syllabusTree) {
                         this.$refs.syllabusTree.checkList = JSON.parse(JSON.stringify(this.shoppingQuestionList));
                     }
+                    if(this.manualFilter.source === 'paper' && resultList.length > 0) {
+                        this.tags = [...new Set(resultList.map(i => i.tags).flat(1))]
+                    }
                 }
                 this.isLoading = false;
             } else {

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

@@ -300,7 +300,11 @@ export default {
                 if (this.source) {
                     this.scope = this.source
                 } else {
-                    this.scope = this.$route.name == 'createPrivEva' ? this.$store.state.userInfo.TEAMModelId : this.$store.state.userInfo.schoolCode
+                    if(this.$route.name == 'createPrivEva' || this.$route.name == 'htCreateEva'){
+                        this.scope = this.$store.state.userInfo.TEAMModelId;
+                    } else {
+                        this.scope = this.$store.state.userInfo.schoolCode;
+                    }                    
                 }
             },
             immediate: true

+ 704 - 0
TEAMModelOS/ClientApp/src/view/learnactivity/htCreateEva.vue

@@ -0,0 +1,704 @@
+<template>
+	<div class="create-priv-container">
+		<div class="create-header">
+			<p class="create-header-title" @click="consData">{{ $t("learnActivity.createEv.createLabel") }}</p>
+			<Button class="btn-save" type="text" :loading="isLoading" ghost icon="ios-albums-outline" @click="saveEvaluation" style="margin-right: 30px">
+				{{ $t("learnActivity.createEv.publishEv") }}
+			</Button>
+			<Button class="btn-save" type="text" ghost icon="md-arrow-back" @click="confirmToManage">
+				{{ $t("learnActivity.createEv.return") }}
+			</Button>
+		</div>
+		<div class="create-body">
+			<div class="evaluation-attr-wrap">
+				<p class="wrap-label">{{ $t("learnActivity.createEv.baseInfo") }}</p>
+				<div style="width: 100%; height: calc(100% - 45px); padding-top: 15px" class="ivu-select-nochoose light-iview-form light-el-input">
+					<vuescroll>
+						<Form ref="evaForm" :model="evaluationInfo" label-position="top" class="evaluation-attr-form" label-colon :rules="ruleValidate">
+							<FormItem :label="$t('learnActivity.createEv.evName')" prop="name">
+								<Input v-special-char v-model="evaluationInfo.name" :placeholder="$t('learnActivity.createEv.evName')"></Input>
+							</FormItem>
+							<FormItem prop="source">
+								<label slot="label" style="width:200px">
+									<span>{{ $t("learnActivity.createEv.evMode") }}</span>
+									<Tooltip :content="$t('tip.examMode')" transfer theme="light" max-width="300">
+										<Icon type="ios-information-circle-outline" color="#1cc0f3" style="margin-left: 5px" />
+									</Tooltip>
+								</label>
+								<Select v-model="evaluationInfo.source" @on-change="checkItemSort">
+									<!-- 暂时取消创建课中评量 -->
+									<!-- 20230105課中評測放回 -->
+									<Option v-for="(item, index) in $GLOBAL.EV_MODE()" v-show="true" :label="item.label" :value="item.value" :key="index" :disabled="index > 1 && (!$store.state.userInfo.hasSchool || !$store.state.user.schoolProfile.svcStatus.VABAJ6NV)">
+										<div>
+											<span>
+												{{ item.label }}
+											</span>
+											<span @click.stop="toProduct" v-show="index > 1 && scaleStatusText()" class="no-auth-tag">
+												{{ scaleStatusText() }}
+											</span>
+										</div>
+									</Option>
+								</Select>
+							</FormItem>
+							<!-- 去掉系统预设考试类型 20220413 -->
+							<!-- <FormItem :label="$t('learnActivity.createEv.evType')" prop="type">
+                                <Select v-model="evaluationInfo.type">
+                                    <Option v-for="(item,index) in $GLOBAL.EV_TYPE()" :value="item.value" :key="index">{{ item.label }}</Option>
+                                </Select>
+                            </FormItem> -->
+							<!-- 使用级联选择 课程--》班级 -->
+							<!-- <FormItem :label="$t('learnActivity.createEv.evTarget')" prop="targets">
+								<label slot="label">
+									<span>{{ $t("learnActivity.createEv.evTarget") }}</span>
+									<Tooltip :content="$t('tip.targetTips')" transfer theme="light" max-width="300">
+										<Icon type="ios-information-circle-outline" color="#1cc0f3" style="margin-left: 5px" />
+									</Tooltip>
+								</label>
+								<PrivateTargetSingle :defaultScope="defaultScope" :defTargets="defTargets" @on-scope-change="getScope" @on-target-change="treeChange"></PrivateTargetSingle>
+							</FormItem> -->
+							<!-- <FormItem :label="$t('learnActivity.createEv.publishType')" prop="publish">
+                                <Checkbox v-model="evaluationInfo.publish" :true-value="$GLOBAL.PUBLISH_TYPE()[0].value" :false-value="$GLOBAL.PUBLISH_TYPE()[1].value" @on-change="publishChange">
+                                    <span style="margin-left:5px;user-select: none;">{{$GLOBAL.PUBLISH_TYPE()[0].label}}</span>
+                                </Checkbox>
+                            </FormItem> -->
+							<FormItem :label="$t('learnActivity.createEv.startTime') + '(' + $t('learnActivity.noStartTimeTip') + ')'">
+								<DatePicker :options="dateOpt" type="datetime" format="yyyy/MM/dd HH:mm" v-model="startTime" split-panels :placeholder="$t('learnActivity.createEv.sTimeHolder')" style="width: 100%" @on-change="getDate($event, 0)"></DatePicker>
+							</FormItem>
+							<FormItem :label="$t('learnActivity.createEv.endTime')" prop="endTime">
+								<DatePicker :options="dateOpt1" type="datetime" format="yyyy/MM/dd HH:mm" v-model="endTime" split-panels :placeholder="$t('learnActivity.createEv.eTimeHolder')" style="width: 100%" @on-change="getDate($event, 1)"></DatePicker>
+							</FormItem>
+							<FormItem>
+								<Checkbox v-model="evaluationInfo.isCompletion">{{ $t("learnActivity.createEv.completeScore") }}</Checkbox>
+							</FormItem>
+						</Form>
+					</vuescroll>
+				</div>
+			</div>
+			<div class="evaluation-question-wrap">
+				<div class="evaluation-question-main">
+					<Tabs v-model="activeTab" type="card" class="question-main-tabs" name="createTest">
+						<TabPane :label="$t('learnActivity.createEv.papersLabel')" name="manualPaper" v-if="createType == 'manualPaper'" :index="1" tab="createTest">
+							<ManualPaper ref="manualPaper" @selectPaper="selectPaper" :selectedId="evaluationInfo.paperInfo[0] ? evaluationInfo.paperInfo[0].id : ''"></ManualPaper>
+						</TabPane>
+						<TabPane :label="$t('learnActivity.createEv.perviewLabel')" name="preview" :index="2" tab="createTest">
+							<div class="teacher-preview-container">
+								<!--返回顶部-->
+								<!-- <div class="back-to-top" :title="$t('learnActivity.mgtScEv.returnTop')" v-show="showBack" @click="handleBackToTop">
+                                    <Icon type="ios-arrow-up" />
+                                </div> -->
+								<BackToTop @on-to-top="handleBackToTop"></BackToTop>
+								<vuescroll ref="paper-preview" @handle-scroll="checkBackTop">
+									<TestPaper v-if="activeTab == 'preview'" :paper="evaluationInfo.paperInfo.length ? evaluationInfo.paperInfo[0] : { item: [] }" isExamPaper hideSheet></TestPaper>
+								</vuescroll>
+							</div>
+						</TabPane>
+					</Tabs>
+				</div>
+			</div>
+		</div>
+	</div>
+</template>
+<script>
+	import BlobTool from "@/utils/blobTool.js";
+	import TestPaper from "@/view/evaluation/index/TestPaper.vue";
+	import ManualPaper from "./ManualPaper.vue";
+	export default {
+		components: {
+			TestPaper,
+			ManualPaper
+		},
+		data() {
+			const _this = this;
+			return {
+				dateOpt: {
+					disabledDate(date) {
+						return date && date.valueOf() < Date.now() - 86400000;
+					}
+				},
+				dateOpt1: {
+					disabledDate(date) {
+						let d = _this.evaluationInfo.startTime ? _this.evaluationInfo.startTime : Date.now();
+						return d && d > date.valueOf();
+					}
+				},
+				defaultScope: "",
+				defTargets: [],
+				createType: "manualPaper",
+				startTime: "",
+				endTime: "",
+				isLoading: false,
+				ruleValidate: {
+					name: [{ required: true, message: this.$t("learnActivity.createEv.errTips1"), trigger: "change" }],
+					"type.id": [{ required: true, message: this.$t("learnActivity.createEv.errTips2"), trigger: "change" }],
+					source: [{ required: true, message: this.$t("learnActivity.createEv.errTips3"), trigger: "change" }],
+					type: [{ required: true, message: this.$t("learnActivity.createEv.errTips4"), trigger: "change" }],
+					"examType.id": [{ required: true, message: this.$t("learnActivity.createEv.errTips5"), trigger: "change" }],
+					targets: [{ required: true, message: this.$t("learnActivity.createEv.errTips6"), type: "array", trigger: "change" }],
+					classes: [{ required: true, message: this.$t("learnActivity.createEv.errTips6"), type: "array", trigger: "change" }],
+					publish: [{ required: true, message: this.$t("learnActivity.createEv.errTips7"), trigger: "change" }],
+					startTime: [{ required: true, type: "number", message: this.$t("learnActivity.createEv.errTips8"), trigger: "change" }],
+					endTime: [{ required: true, type: "number", message: this.$t("learnActivity.createEv.errTips9"), trigger: "change" }]
+				},
+				activeTab: "manualPaper",
+				isMarkMode: false,
+				evaluationInfo: {
+					name: "",
+					targets: [],
+					classes: [],
+					scope: "",
+					type: "", //测试类别
+					source: "",
+					publish: "0",
+					examType: null, //测试类型
+					startTime: undefined,
+					endTime: undefined,
+					paperInfo: [],
+					papers: [],
+					isCompletion: 0
+				},
+				mode: "",
+				selectBefore: [],
+				stuList: [],
+				showBack: false,
+				isComplete: false
+			};
+		},
+		methods: {
+			scaleStatusText() {
+				if (!this.$store.state.userInfo.hasSchool) {
+					return this.$t("learnActivity.createEv.noSchool");
+				}
+				if (this.$store.state.user.schoolProfile.svcStatus.VABAJ6NV) {
+					return "";
+				} else {
+					return this.$t("learnActivity.createEv.noAuth");
+				}
+			},
+			toProduct() {
+				if (this.$store.state.userInfo.hasSchool) {
+					this.$router.push({
+						name: "product"
+					});
+				}
+			},
+			getScope(data) {
+				this.evaluationInfo.scope = data;
+			},
+			/**vuescroll回到顶部 */
+			handleBackToTop() {
+				this.$refs["paper-preview"].scrollTo(
+					{
+						y: "0"
+					},
+					300
+				);
+			},
+			/**
+			 * 判断是否显示回到顶部按钮
+			 * @param vertical
+			 * @param horizontal
+			 * @param nativeEvent
+			 */
+			checkBackTop(vertical, horizontal, nativeEvent) {
+				if (vertical.scrollTop > 100) {
+					this.showBack = true;
+				} else {
+					this.showBack = false;
+				}
+			},
+			publishChange(data) {
+				if (data == 0) {
+					this.startTime = new Date();
+					this.evaluationInfo.startTime = Date.now();
+				}
+			},
+			treeChange(data) {
+				this.evaluationInfo.targets = data.targets;
+				this.$refs["evaForm"]?.validate();
+				// 1、设置发布对象
+				//个人课程
+				if (this.evaluationInfo.scope == "private") {
+					this.evaluationInfo.stuLists = this.evaluationInfo.targets.map((item) => {
+						return item[1];
+					});
+					this.evaluationInfo.classes = [];
+				}
+				//学校课程教学班
+				else if (this.evaluationInfo.scope == "school" && this.evaluationInfo.targets.length && this.evaluationInfo.targets[0][1] == "teach") {
+					this.evaluationInfo.stuLists = this.evaluationInfo.targets.map((item) => {
+						return item[2];
+					});
+					this.evaluationInfo.classes = [];
+					this.evaluationInfo.grades = [];
+				}
+				// 学校课程行政班
+				else {
+					this.evaluationInfo.classes = this.evaluationInfo.targets.map((item) => {
+						return item[2];
+					});
+					this.evaluationInfo.stuLists = [];
+					//补充年级信息
+					this.evaluationInfo.grades = [];
+					let courseClassList = data.course?.children?.find((item) => item.id == "class")?.children;
+					if (courseClassList && courseClassList.length) {
+						let sltClass = courseClassList.filter((item) => this.evaluationInfo.classes.includes(item.id));
+						let grades = Array.from(new Set(sltClass?.map((item) => item.year)));
+
+						let gradesInfo = grades.map((item) => {
+							return this.$jsFn.getGradeInfoByYear(item, data.course.periodId);
+						});
+						gradesInfo = gradesInfo.filter((item) => !!item);
+						gradesInfo.forEach((item) => {
+							item.id = item.id.toString();
+						});
+						this.evaluationInfo.grades = gradesInfo;
+					}
+				}
+
+				//2、设置评测“学科”
+				this.evaluationInfo.subjects = [];
+				// 个人课程用课程名称代替学科
+				if (this.evaluationInfo.scope == "private") {
+					if (data.targets.length && data.course) {
+						this.evaluationInfo.subjects.push({
+							id: data.course.id,
+							name: data.course.name
+						});
+					}
+					this.evaluationInfo.period = undefined;
+				}
+				//校本课程用课程关联的学生,并补充学段信息
+				else {
+					if (data.targets.length && data.course) {
+						this.evaluationInfo.subjects.push({
+							id: data.course.subjectId,
+							name: data.course.subject
+						});
+						this.evaluationInfo.period = {
+							id: data.course.periodId,
+							name: data.course.period
+						};
+					}
+				}
+			},
+			/*
+			 * 返回上一级
+			 */
+			confirmToManage() {
+				this.$router.go(-1);
+				// if (this.mode == 'private') {
+				//     this.$router.push({
+				//         name: 'privExam'
+				//     })
+				// } else {
+				//     this.$router.push({
+				//         name: 'schoolExam'
+				//     })
+				// }
+			},
+			//设置评测模式后检查试卷itemSort
+			checkItemSort() {
+				this.evaluationInfo.paperInfo = [];
+				this.evaluationInfo.papers = [];
+				this.activeTab = "manualPaper";
+				this.$Message.warning(this.$t('evaluation.markMode.tip5'));
+				this.isMarkMode = this.evaluationInfo.source == "2";
+				this.$refs.manualPaper.getPaperList();
+			},
+			/**
+			 * 处理挑选试卷事件
+			 * @param data
+			 */
+			selectPaper(data) {				
+				if (this.evaluationInfo.source == "2" && data.itemSort == 1) {
+					return this.$Message.warning(this.$t("learnActivity.createEv.sort3"));
+				}
+				let simplePaper = data;
+				this.$Modal.confirm({
+					title: this.$t("learnActivity.createEv.stPaperTitle"),
+					content: `${this.$t("learnActivity.createEv.stPaperContent")}${data.name}?`,
+					onOk: async () => {
+						let fullPaper = await this.$evTools.getFullPaper(simplePaper);
+						this.comfirmSelectPaper(simplePaper, fullPaper);
+					}
+				});
+			},
+			comfirmSelectPaper(simplePaper, fullPaper) {
+				if (!this.evaluationInfo.name) {
+					this.evaluationInfo.name = simplePaper.name;
+				}
+				fullPaper.examId = "";
+				fullPaper.examScope = this.mode;
+				fullPaper.examCode = this.$store.state.userInfo.schoolCode;
+				this.evaluationInfo.paperInfo[0] = fullPaper;
+				this.evaluationInfo.papers[0] = simplePaper;
+				this.goToPreview();
+			},
+
+			/**
+			 * 将日期控件时间格式转成时间戳
+			 * @param value
+			 * @param date
+			 */
+			getDate(value, flag) {
+				if (flag == 0) {
+					this.startTime = value;
+					this.evaluationInfo.startTime = new Date(value).getTime();
+					if (this.evaluationInfo.startTime >= this.evaluationInfo.endTime) {
+						this.endTime = undefined;
+						this.evaluationInfo.endTime = undefined;
+					}
+				} else if (flag == 1) {
+					if (value.indexOf("00:00") > 0) {
+						value = value.replace("00:00", "23:59:59");
+					}
+					this.endTime = value;
+					this.evaluationInfo.endTime = new Date(value).getTime();
+				}
+			},
+			goToPreview() {
+				this.activeTab = "preview";
+			},
+			consData() {
+				console.log("evaluationInfo", this.evaluationInfo);
+			},			
+			async saveEvaluation() {
+				//表单验证
+				let flag = true;
+				this.$refs["evaForm"].validate((valid) => {
+					if (!valid) {
+						this.$Message.warning(this.$t("learnActivity.createEv.formWarning"));
+						flag = false;
+					}
+				});
+				if (!flag) {
+					return;
+				}
+				//检查试卷是否添加
+				if (this.evaluationInfo.papers.length < 1) {
+					this.$Message.warning({
+						content: this.$t("learnActivity.createEv.paperWarning"),
+						duration: 2
+					});
+					flag = false;
+				}
+				if (!flag) {
+					return;
+				}
+				let examId = this.$tools.guid();
+				let apiPapers = this.getPaperInfo(examId);
+				let requestData = {
+					jointEventId: this.$route.params.data.id,
+					jointGroupId: this.$route.params.jointGroupId,
+					jointScheduleId: this.$route.params.jointScheduleId,
+					jointExamId: "",
+					creatorId: this.$route.params.data.creatorId,
+					name: this.evaluationInfo.name,
+					source: this.evaluationInfo.source,
+					papers: apiPapers
+				}
+				//发布成功需要备份试卷数据
+				
+				let privateSas = {};
+				if (this.$store.state.user.userProfile) {
+					let blobInfo = this.$store.state.user.userProfile;
+					privateSas.sas = "?" + blobInfo.blob_sas;
+					privateSas.url = blobInfo.blob_uri.slice(0, blobInfo.blob_uri.lastIndexOf(this.$store.state.userInfo.TEAMModelId) - 1);
+					privateSas.name = this.$store.state.userInfo.TEAMModelId;
+				}
+
+				let schoolSas = {};
+				if (this.$store.state.userInfo.hasSchool) {
+					schoolSas = {
+						sas: "?" + this.$store.state.user.schoolProfile.blob_sas,
+						//url: this.$store.state.user.schoolProfile.blob_uri.slice(0, this.$store.state.user.schoolProfile.blob_uri.lastIndexOf(this.$store.state.userInfo.schoolCode) - 1),
+						url: this.$store.state.user.schoolProfile.blob_uri.slice(0, this.$store.state.user.schoolProfile.blob_uri.lastIndexOf("/")),
+						name: this.$store.state.userInfo.schoolCode
+					};
+				}
+				//校本课程复制到学校容器 个人课程复制到个人容器
+				let targetBlob = undefined;
+				if (this.evaluationInfo.scope == "school") {
+					targetBlob = new BlobTool(schoolSas.url, schoolSas.name, schoolSas.sas, "school");
+				} else {
+					targetBlob = new BlobTool(privateSas.url, privateSas.name, privateSas.sas, "private");
+				}
+				let privateBlob = new BlobTool(privateSas.url, privateSas.name, privateSas.sas, "private");
+				let schoolBlob = undefined;
+				let targetFolder = "jointexam/" + examId + "/paper/";
+				let count = 0;
+				requestData.papers.forEach(async (item, index) => {
+					if (item.blobUrl.indexOf("/exam/") == 0) {
+						if (++count == requestData.papers.length) {
+							this.$Message.success(this.$t("learnActivity.createEv.publishOk"));
+							this.isLoading = false;
+							// let route = this.mode + 'Evaluation'
+							this.$router.push({
+								name: "htMgtExam",
+								params: {
+									data: this.$route.params.data
+								}
+							});
+						}
+						return;
+					}
+					// 挑选的是校本试卷
+					if (item.scope == "school") {
+						if (!schoolBlob) schoolBlob = new BlobTool(schoolSas.url, schoolSas.name, schoolSas.sas, "school");
+						// 將公用試卷複製一份到該評量的位置
+						targetBlob
+							.copyFolder(targetFolder + requestData.papers[0].id + "/", item.blobUrl.substring(1), schoolBlob)
+							.then()
+							.finally(() => {
+								if (++count == requestData.papers.length) {
+									requestData.jointExamId = examId;
+									this.upsertData(requestData);
+								}
+							});
+					}
+					//挑选的是个人试卷
+					else if (item.scope == "private") {
+						if (!privateBlob) privateBlob = new BlobTool(privateSas.url, privateSas.name, privateSas.sas, "private");
+						// 將公用試卷複製一份到該評量的位置
+						targetBlob
+							.copyFolder(targetFolder + requestData.papers[0].id + "/", item.blobUrl.substring(1), privateBlob)
+							.then()
+							.finally(() => {
+								if (++count == requestData.papers.length) {// 上傳blob成功
+									requestData.jointExamId = examId;
+									this.upsertData(requestData);
+								}
+							});
+					}
+				});
+
+			},
+			// 呼叫寫入API 
+			upsertData(requestData){
+				this.$api.htcommunity.jointExamUpsert(requestData).then(// 呼叫API寫入評量
+					(res) => {
+						this.isComplete = true;
+						if (res.err === "") {
+							this.$Message.success(this.$t("learnActivity.createEv.publishOk"));
+							this.isLoading = false;
+							let route = this.mode + 'Evaluation'
+							this.$router.push({
+								name: "htMgtExam",
+								params: {
+									data: this.$route.params.data
+								}
+							});							
+						} else {						
+							// this.$Message.error('API ERROR!')
+							this.$Message.error(this.$t("learnActivity.mark.saveErr"));
+							this.isLoading = false;
+							return;
+						}
+					},
+					(err) => {
+						this.$Message.error(this.$t("learnActivity.mgtScEv.updErr"));
+					})		
+			},
+			//拼接API需要的paper数据
+			getPaperInfo(examId) {				
+				let data = this.evaluationInfo.papers;
+				let rule = this.evaluationInfo.paperInfo;
+				if (data) {
+					let paperDto = [];
+					let typeArr = ["single", "multiple", "judge"];
+					if (this.evaluationInfo.isCompletion) {
+						typeArr.push("complete");
+					}
+					for (let i = 0; i < data.length; i++) {
+						if (data[i].blob.indexOf("/exam/") == 0) {
+							paperDto.push(data[i]);
+						} else {
+							let paper = {};
+							paper.id = data[i].id;
+							paper.code = data[i].code;
+							paper.name = data[i].name;
+							paper.blob = "/jointexam/" + examId + "/paper/" + data[i].id;// 公用試卷庫的blob位置							
+							paper.blobUrl = data[i].blob;// 新增評量試卷放的blob位置
+							paper.scope = data[i].scope;
+							paper.sheet = data[i].sheet;
+							paper.periodId = data[i].periodId;
+							paper.multipleRule = rule[i].multipleRule;
+							paper.answers = [];
+							paper.point = [];
+							paper.knowledge = [];
+							paper.field = [];
+							paper.type = []; //后面新增字段, 保存每个题目类型
+							for (let k = 0; k < rule[i].item.length; k++) {
+								// scoring中只有客观题答案,这里改为从题目中获取客观题和填空题的答案
+								if (rule[i].item[k].children.length) {
+									for (let m = 0; m < rule[i].item[k].children.length; m++) {
+										paper.answers.push(typeArr.includes(rule[i].item[k].children[m].type) ? (rule[i].item[k].children[m].answer ? rule[i].item[k].children[m].answer : []) : []);
+										paper.point.push(rule[i].item[k].children[m].score);
+										paper.knowledge.push(rule[i].item[k].children[m].knowledge ? rule[i].item[k].children[m].knowledge : []);
+										paper.field.push(rule[i].item[k].children[m].field ? rule[i].item[k].children[m].field : []);
+										paper.type.push(rule[i].item[k].children[m].type); //后面新增字段, 保存每个题目类型
+									}
+								} else {
+									paper.answers.push(typeArr.includes(rule[i].item[k].type) ? (rule[i].item[k].answer ? rule[i].item[k].answer : []) : []);
+									paper.point.push(rule[i].item[k].score);
+									paper.knowledge.push(rule[i].item[k].knowledge ? rule[i].item[k].knowledge : []);
+									paper.field.push(rule[i].item[k].field ? rule[i].item[k].field : []);
+									paper.type.push(rule[i].item[k].type); //后面新增字段, 保存每个题目类型
+								}
+							}
+							/* for (let k = 0; k < rule[i].slides.length; k++) {
+                if (rule[i].slides[k].type !== 'compose') {
+                    paper.answers.push(rule[i].slides[k].scoring.ans ? rule[i].slides[k].scoring.ans : [])
+                    paper.point.push(rule[i].slides[k].scoring.score)
+                    paper.knowledge.push(rule[i].slides[k].scoring.knowledge ? rule[i].slides[k].scoring.knowledge : [])
+                    paper.field.push(rule[i].slides[k].scoring.field ? rule[i].slides[k].scoring.field : [])
+                    paper.type.push(rule[i].slides[k].type) //后面新增字段, 保存每个题目类型
+                }
+            } */
+							paper.subjectName = this.evaluationInfo.subjects && this.evaluationInfo.subjects.length ? this.evaluationInfo.subjects[0].name : "";
+							paper.subjectId = this.evaluationInfo.subjects && this.evaluationInfo.subjects.length ? this.evaluationInfo.subjects[0].id : "";
+							paperDto.push(paper);
+						}
+					}
+					return paperDto;
+				} else {
+					return [];
+				}
+			},
+			//复制或编辑评测初始化数据
+			async initData(data) {
+				let subjects = this._.cloneDeep(data.subjects);
+				data.id = "";
+				data.name = `${data.name}(${this.$t("learnActivity.mgtScEv.copy")})`;
+				this.startTime = new Date(data.startTime);
+				this.endTime = new Date(data.endTime);
+				let pp = {
+					"@DESC": "createTime",
+					code: data.papers[0].code?.replace("Paper-", ""),
+					scope: data.papers[0].scope,
+					id: data.papers.map((item) => item.id)
+				};
+				data.paperInfo = [];
+				data.papers = [];
+				this.defTargets = data.targets;
+				this.defaultScope = data.scope;
+				data.publish = "0";				
+				this.evaluationInfo = data;
+				console.log(JSON.stringify(this.evaluationInfo));
+				try {
+					let paperInfo = await this.$api.learnActivity.FindExamPaper(pp);
+					if (!paperInfo.length) {
+						// pp.sc
+						paperInfo = await this.$api.learnActivity.FindExamPaper(pp);
+					}
+					for (let i = 0; i < paperInfo.papers.length; i++) {
+						let fullPaper = await this.$evTools.getFullPaper(paperInfo.papers[i]);
+						this.comfirmSelectPaper(paperInfo.papers[i], fullPaper);
+					}
+					this.evaluationInfo.subjects = subjects;
+				} catch (e) {}
+				this.activeTab = "preview";
+				this.endTime = new Date(new Date(new Date().toLocaleDateString()).getTime() + 2 * 24 * 60 * 60 * 1000 - 1);
+				this.evaluationInfo.endTime = new Date(new Date().toLocaleDateString()).getTime() + 2 * 24 * 60 * 60 * 1000 - 1;
+				// data.id = ''
+				// data.name = `${data.name}(${this.$t('learnActivity.mgtScEv.copy')})`
+				// this.startTime = new Date(data.startTime)
+				// this.endTime = new Date(data.endTime)
+				// data.paperInfo = []
+				// this.defTargets = data.targets
+				// for (let i = 0; i < data.papers.length; i++) {
+				//     let paper = this._.cloneDeep(data.papers[i])
+				//     data.paperInfo.push(paper)
+				//     data.paperInfo[i].subjectId = data.subjects[i].id
+				//     data.paperInfo[i].subjectName = data.subjects[i].name
+				// }
+				// data.publish = '0'
+				// this.evaluationInfo = data
+				// this.activeTab = 'preview'
+			}
+		},
+		mounted(){
+			// 如果取不到活動資料則返回活動列表頁
+			if(!this.$route.params.data){
+				this.$router.push({
+						name: "htMgtHome"
+					});
+			}
+		},
+		created() {
+			// 处理默认时间
+			// this.startTime = new Date()
+			// this.evaluationInfo.startTime = new Date().getTime()
+			this.endTime = new Date(new Date(new Date().toLocaleDateString()).getTime() + 2 * 24 * 60 * 60 * 1000 - 1);
+			this.evaluationInfo.endTime = new Date(new Date().toLocaleDateString()).getTime() + 2 * 24 * 60 * 60 * 1000 - 1;
+			//判断创建个人还是校本评测
+			this.mode = "private";
+			//编辑评测逻辑
+			let routerData = this.$route.params.evaluationInfo;
+			if (routerData) {
+				// console.log(JSON.stringify(routerData))
+				console.log(routerData.subjects);
+				this.initData(routerData);
+			}
+		},
+		beforeRouteLeave(to, from, next) {
+			if (this.isComplete) {
+				if (to.name == "answerSheet") {
+					from.meta.isKeep = true;
+				} else {
+					from.meta.isKeep = false;
+				}
+				next();
+			} else {
+				this.$Modal.confirm({
+					title: this.$t("evaluation.newExercise.modalTip"),
+					content: this.$t("learnActivity.createEv.unSaveTip"),
+					cancelText: this.$t("auth.cancel"),
+					onOk: () => {
+						if (to.name == "answerSheet") {
+							from.meta.isKeep = true;
+						} else {
+							from.meta.isKeep = false;
+						}
+						next();
+					}
+				});
+			}
+		}
+	};
+</script>
+<style scoped lang="less">
+	@import "./CreatePrivEva.less";
+</style>
+<style lang="less">
+	.one-hidden-check-box {
+		.el-cascader-menu:nth-child(1) .el-checkbox {
+			display: none;
+		}
+	}
+	.teacher-preview-container .paper-main-wrap {
+		padding: 0px;
+		margin-top: 0px;
+	}
+	.evaluation-attr-wrap .ivu-form .ivu-form-item-label {
+		color: var(--second-text-color);
+	}
+
+	.evaluation-question-main .ivu-tabs-tab-active {
+		font-weight: 600;
+	}
+
+	.evaluation-question-main .ivu-tabs .ivu-tabs-content-animated {
+		height: 100%;
+		margin-bottom: 10px;
+	}
+
+	.evaluation-question-main .ivu-tabs-bar {
+		border-color: #404040;
+		margin-bottom: 0px;
+		border-bottom: none;
+	}
+	.evaluation-question-main .ivu-tabs.ivu-tabs-card .ivu-tabs-bar .ivu-tabs-tab {
+		margin-right: 2px;
+	}
+</style>

+ 145 - 0
TEAMModelOS/ClientApp/src/view/learnactivity/tabs/htAnswerTable.less

@@ -0,0 +1,145 @@
+.data-count-wrap {
+    padding: 20px 0px;
+    display: flex;
+    background: white;
+    justify-content: space-between;
+    box-shadow: 0 0 10px 2px var(--card-shadow);
+}
+
+.data-count-item {
+    width: 150px;
+    text-align: center;
+
+    .data-value {
+        font-size: 30px;
+        font-weight: 600;
+        color: #17233d;
+    }
+}
+
+.table-data-wrap {
+    width: 100%;
+    box-shadow: 0 0 10px 2px var(--card-shadow);
+    margin-top: 10px;
+    // min-height: 600px;
+    min-height: ~"calc(100% - 118px)";
+    background: white;
+    padding: 10px 10px 60px 10px;
+}
+
+.ev-target-box {
+    color: var(--label-text-color);
+    padding: 10px 10px 10px 0;
+    position: sticky;
+    top: 0px;
+    background: white;
+    overflow: hidden;
+
+    .filter-select {
+        display: inline-block;
+        width: 120px;
+        margin-right: 25px;
+    }
+}
+
+.export-btn {
+    float: right;
+    margin-left: 20px;
+    margin-top: 5px;
+}
+
+.header-tips {
+    color: #515a6e;
+    float: right;
+    margin-top: 5px;
+}
+
+.stu-status-tag {
+    cursor: pointer;
+    background: #ed4014;
+    font-size: 12px;
+    padding: 3px 8px;
+    font-weight: 800;
+    border-radius: 4px;
+    color: white;
+}
+
+.score-box {
+    margin-top: 10px;
+}
+
+.page-wrap {
+    float: right;
+    margin-top: 15px;
+}
+
+.common-icon-text {
+    color: var(--primary-text-color);
+    cursor: pointer;
+    user-select: none;
+    float: right;
+    margin-right: 11px;
+}
+
+.mark-view-header {
+    top: 0px;
+    z-index: 9;
+    padding: 15px;
+    color: var(--label-text-color);
+    position: sticky;
+    top: 0px;
+    background: white;
+    overflow:visible;
+
+    .filter-select {
+        display: inline-block;
+        width: 120px;
+        margin-right: 25px;
+    }
+}
+
+.select-status-tag {
+    display: inline-block;
+    width: 7px;
+    height: 7px;
+    border-radius: 50%;
+    margin-right: 5px;
+}
+
+.qu-type-tag {
+    background: #2db7f5;
+    padding: 0px 4px;
+    color: white;
+    border-radius: 2px;
+    width: fit-content;
+    word-break: keep-all;
+    height: fit-content;
+}
+
+.correct-tips {
+    width: 300px;
+}
+
+.chart-wrap {
+    display: flex;
+    // align-items: center;
+}
+
+.correct-tips-label {
+    color: black;
+    margin-bottom: 15px;
+    font-weight: 600;
+}
+
+.stu-info {
+    margin-right: 30px;
+    font-size: 16px;
+}
+
+.stu-value {
+    font-weight: 600;
+}
+
+.stu-label {
+    color: #808695;
+}

Fichier diff supprimé car celui-ci est trop grand
+ 1490 - 0
TEAMModelOS/ClientApp/src/view/learnactivity/tabs/htAnswerTable.vue


+ 106 - 0
TEAMModelOS/ClientApp/src/view/learnactivity/tabs/htExamPaper.vue

@@ -0,0 +1,106 @@
+<template>
+    <div class="exam-paper-container">        
+        <vuescroll ref="exam-paper-wrap" @handle-scroll="checkBackTop">
+            <div class="subjects-wrap" v-if="examInfo && examInfo.subjects && examInfo.owner == 'school' && !examInfo.examPaperErr">
+                <span>{{$t('learnActivity.mgtScEv.evSubject')}}</span>
+                <RadioGroup v-model="curSubIndex" type="button" button-style="solid">
+                    <Radio :label="index" :key="item.id" v-for="(item,index) in examInfo.subjects">
+                        {{item.name}}
+                    </Radio>
+                </RadioGroup>
+            </div>            
+            <div class="paper-wrap" v-if="!examInfo.examPaperErr">                
+                <TestPaper v-if="examInfo && examInfo.papers && examInfo.papers[curSubIndex] && examInfo.papers[curSubIndex].item" 
+                :paper="examInfo.papers[curSubIndex]" style="color:#515a6e;" :isShowTools="false" isExamPaper isExamInfoPaper :examId="examInfo.id" :activityIndex="activityIndex" :subjectIndex="curSubIndex" :refreshExam="refreshExam" canFix
+                :jointGroupId="examInfo.jointGroupId" :jointScheduleId="examInfo.jointScheduleId"
+                ></TestPaper>                
+                <EmptyData v-else style="padding-top:60px;" :textContent="$t('learnActivity.simple.paperErr')"></EmptyData>                
+            </div>
+            <div v-else class="paper-err-wrap">                
+                <EmptyData :top="0" style="padding-top:60px;" :textContent="$t('learnActivity.simple.paperErr')"></EmptyData>                
+            </div>
+        </vuescroll>
+        <BackToTop @on-to-top="handleToTop" v-show="showBack"></BackToTop>
+    </div>
+</template>
+<script>
+import TestPaper from '@/view/evaluation/index/htTestPaper.vue'
+export default {
+    components: {
+        TestPaper
+    },
+    props: {
+        examInfo: {
+            type: Object,
+            default: () => {
+                return {}
+            }
+        },
+        activityIndex: {
+            type: Number,
+            default: 0
+        },
+        refreshExam: {
+            type: Function,
+            require: true,
+            default: null
+        },
+    },
+    data() {
+        return {
+            showBack: false,
+            curSubIndex: 0,
+            curSubject: '',
+            subjects: [
+                {
+                    name: '语文',
+                    id: '1'
+                },
+                {
+                    name: '数学',
+                    id: '2'
+                }
+            ]
+        }
+    },
+    created(){
+        //debugger
+        console.log('ssss', this.examInfo)
+    },
+    methods: {
+        //返回顶部
+        handleToTop() {
+            this.$refs['exam-paper-wrap'].scrollTo(
+                {
+                    y: '0'
+                },
+                300
+            )
+        },
+        /**
+         * 判断是否显示回到顶部按钮
+         * @param vertical
+         * @param horizontal
+         * @param nativeEvent
+         */
+        checkBackTop(vertical, horizontal, nativeEvent) {
+            if (vertical.scrollTop > 100) {
+                this.showBack = true
+            } else {
+                this.showBack = false
+            }
+        },
+    }
+}
+</script>
+<style lang="less" scoped>
+@import "./ExamPaper.less";
+</style>
+<style lang="less">
+.exam-paper-container .__view {
+    padding: 10px;
+}
+.exam-paper-container .paper-main-wrap {
+    margin-top: 0px;
+}
+</style>

+ 27 - 2
TEAMModelOS/Controllers/System/GenPDFController.cs

@@ -31,8 +31,13 @@ namespace TEAMModelOS.Controllers
         private readonly AzureServiceBusFactory _azureServiceBus ;
         private readonly IPSearcher _ipSearcher;
         private readonly Option _option;
+        private readonly AzureCosmosFactory _azureCosmos;
+        private readonly CoreAPIHttpService _coreAPIHttpService;
         private readonly Region2LongitudeLatitudeTranslator _longitudeLatitudeTranslator;
-        public GenPDFController(AzureServiceBusFactory  azureServiceBus,AzureRedisFactory azureRedis, Region2LongitudeLatitudeTranslator longitudeLatitudeTranslator, IHttpClientFactory httpClient, IConfiguration configuration, AzureStorageFactory azureStorage, IPSearcher searcher, DingDing dingDing, IOptionsSnapshot<Option> option)
+        public GenPDFController( AzureServiceBusFactory  azureServiceBus,AzureRedisFactory azureRedis, 
+            Region2LongitudeLatitudeTranslator longitudeLatitudeTranslator, IHttpClientFactory httpClient, 
+            IConfiguration configuration, AzureStorageFactory azureStorage, IPSearcher searcher, DingDing dingDing, 
+            IOptionsSnapshot<Option> option,AzureCosmosFactory azureCosmos,CoreAPIHttpService coreAPIHttpService)
         {
             _httpClient = httpClient;
             _configuration = configuration;
@@ -43,6 +48,8 @@ namespace TEAMModelOS.Controllers
             _azureRedis=azureRedis;
             _longitudeLatitudeTranslator = longitudeLatitudeTranslator;
             _azureServiceBus = azureServiceBus;
+            _azureCosmos = azureCosmos;
+            _coreAPIHttpService = coreAPIHttpService;
         }
         /// <summary>
         /// 艺术评测报告生成
@@ -58,9 +65,27 @@ namespace TEAMModelOS.Controllers
         public async Task<IActionResult> ArtReport (GenPDFData request) 
         {
             
-            var data = await GenPDFService.AddGenPdfQueue(_azureServiceBus, _azureRedis, request);
+            var data = await GenPDFService.AddGenPdfQueue(  _azureRedis, request);
             return Ok(new { total= data.total,add= data .add});
         }
+        /// <summary>
+        /// 艺术评测报告生成
+        /// </summary>
+        /// <param name="request"></param>
+        /// <returns></returns>
+        [ProducesDefaultResponseType]
+        [HttpPost("art-pdf")]
+        //#if !DEBUG
+        //        [AuthToken(Roles = "teacher,admin")]
+        //        [Authorize(Roles = "IES")]
+        //#endif
+        public async Task<IActionResult> GenArtStudentPdf(JsonElement request)
+        {
+
+            await GenPDFService.GenArtStudentPdf(_azureRedis, _azureCosmos,_coreAPIHttpService, _dingDing, _azureStorage, _configuration, request);
+            return Ok();
+        }
+        
     }
 
 }