Explorar o código

1. 統測活動匯出成績(進行中) 2.統測活動寄發熱身賽通知、決賽通知

jeff hai 8 meses
pai
achega
281139ad54

+ 207 - 66
TEAMModelOS.Function/IESServiceBusTrigger.cs

@@ -2651,78 +2651,135 @@ namespace TEAMModelOS.Function
                                     await table.Save<ChangeRecord>(changeRecord);
                                 }
                                 //寄發Email
-                                ///取得收信者Email ※ jointCourse.creatorEmail
-                                //List<dynamic> users = new List<dynamic>();
-                                //if (jointEventSchedule.type.Equals("exam"))
-                                //{
-                                //    //一般競賽 ※決賽 名單架構確定後再取
-                                //    if (jointEventSchedule.examType.Equals("regular"))
-                                //    {
-                                //        await foreach (var jc in client.GetContainer(Constant.TEAMModelOS, Constant.Teacher).GetItemQueryStreamIteratorSql(queryText: $"SELECT DISTINCT c.creatorId, c.creatorName, c.creatorEmail FROM c WHERE c.jointEventId = '{jointEvent.id}' ", requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"JointCourse") }))
-                                //        {
-                                //            using var json = await JsonDocument.ParseAsync(jc.Content);
-                                //            if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
-                                //            {
-                                //                foreach (var obj in json.RootElement.GetProperty("Documents").EnumerateArray())
-                                //                {
-                                //                    EmailUser user = new EmailUser();
-                                //                    user.tmid = (obj.TryGetProperty("creatorId", out JsonElement _creatorId)) ? _creatorId.GetString() : string.Empty;
-                                //                    user.name = (obj.TryGetProperty("creatorName", out JsonElement _creatorName)) ? _creatorName.GetString() : string.Empty;
-                                //                    user.email = (obj.TryGetProperty("creatorEmail", out JsonElement _creatorEmail)) ? _creatorEmail.GetString() : string.Empty;
-                                //                    if (!string.IsNullOrWhiteSpace(user.tmid) && !string.IsNullOrWhiteSpace(user.email))
-                                //                    {
-                                //                        users.Add(user);
-                                //                    }
-                                //                }
-                                //            }
-                                //        }
-                                //    }
-                                //}
-                                //if (users.Count > 0)
-                                //{
-                                //    //取得模板ID ※國際站就用繁體,大陸站就用簡體 [待定]
-                                //    string location = Environment.GetEnvironmentVariable("Option:Location");
-                                //    //string tid = (location.Contains("China")) ? "" : "d-833d40ac6397414b852b91e2fa45850a";
-                                //    string tid = string.Empty;
-                                //    string lang = (location.Contains("China")) ? "zh-cn" : "zh-tw";
-                                //    //取得通知內文
-                                //    string mailTitle = string.Empty;
-                                //    string mailBody = string.Empty;
-                                //    JObject jsonObject = langDic[lang];
-                                //    if (jsonObject.TryGetValue("joint-schedule-start", out JToken msgToken))
-                                //    {
-                                //        JArray msgArray = (JArray)msgToken;
-                                //        mailTitle = msgArray[0].ToString();
-                                //        mailBody = $"{msgArray[1].ToString()}";
-                                //    }
-                                //    foreach (dynamic user in users)
-                                //    {
-                                //        if (!string.IsNullOrWhiteSpace(tid)) //※大陸站無模板,暫時不發
-                                //        {
-                                //            mailBody = mailBody.Replace("{tmdname}", user.name).Replace("{event}", jointEventSchedule.name);
-                                //            await _coreAPIHttpService.SendMail(new Dictionary<string, object> { { "to", user.email }, { "tid", tid }, { "vars", new { name = user.name } } }, location, _configuration);
-                                //        }
-                                //    }
-                                //}
-                                break;
-                            case "finish":
-                                string pk = string.Format("{0}{1}{2}{3}{4}{5}{6}", "JointEvent", "-", $"{jointEventId}", "-", "schedule", "-", "going");
-                                await table.DeleteSingle<ChangeRecord>(pk, jointEventSchedule.id);
-                                //寄發Email [待模板ID]
-                                //熱身賽行程結束、決賽名單計算
-                                if(jointEventSchedule.type.Equals("exam") && jointEventSchedule.examType.Equals("regular"))
+                                if(jointEventSchedule.type.Equals("exam"))
                                 {
-                                    string scope = "private"; //先寫死為個人
-                                    List<string> jointGroupIds = jointEvent.groups.Select(g => g.id).ToList();
-                                    if (jointGroupIds.Count > 0)
+                                    string type = string.Empty;
+                                    string tablePartitionKey = string.Empty;
+                                    string tableRowKey = string.Empty;
+                                    long emitTime = 0;
+                                    //熱身賽
+                                    if (jointEventSchedule.examType.Equals("regular"))
+                                    {
+                                        //熱身賽開始
+                                        type = "regularExamStart";
+                                        emitTime = jointEventSchedule.startTime;
+                                        var messageRegularSt = new ServiceBusMessage(new { jointEventId = $"{jointEventId}", jointScheduleId = $"{jointEventSchedule.id}", type = type, time = emitTime }.ToJsonString());
+                                        messageRegularSt.ApplicationProperties.Add("name", "JointMessage");
+                                        tablePartitionKey = string.Format("{0}{1}{2}{3}{4}", "JointMessage", "-", $"{jointEventId}", "-", $"{type}"); //主key: JointMessage-{jointEventId}-{type}  RowKey: {jointScheduleId}
+                                        tableRowKey = $"{jointEventSchedule.id}";
+                                        List<ChangeRecord> recordRegularSt = await table.FindListByDict<ChangeRecord>(new Dictionary<string, object>() { { "RowKey", tableRowKey }, { "PartitionKey", tablePartitionKey } });
+                                        if (recordRegularSt.Count > 0)
+                                        {
+                                            long sr = await _serviceBus.GetServiceBusClient().SendScheduleMessageAsync(Environment.GetEnvironmentVariable("Azure:ServiceBus:ActiveTask"), messageRegularSt, DateTimeOffset.FromUnixTimeMilliseconds(emitTime));
+                                            await _serviceBus.GetServiceBusClient().CancelMessageAsync(Environment.GetEnvironmentVariable("Azure:ServiceBus:ActiveTask"), records[0].sequenceNumber);
+                                            records[0].sequenceNumber = sr;
+                                            await table.SaveOrUpdate<ChangeRecord>(records[0]);
+                                        }
+                                        else
+                                        {
+                                            long sr = await _serviceBus.GetServiceBusClient().SendScheduleMessageAsync(Environment.GetEnvironmentVariable("Azure:ServiceBus:ActiveTask"), messageRegularSt, DateTimeOffset.FromUnixTimeMilliseconds(emitTime));
+                                            ChangeRecord changeRecord = new ChangeRecord
+                                            {
+                                                RowKey = tableRowKey,
+                                                PartitionKey = tablePartitionKey,
+                                                sequenceNumber = sr,
+                                                msgId = messageRegularSt.MessageId
+                                            };
+                                            await table.Save<ChangeRecord>(changeRecord);
+                                        }
+                                        //熱身賽即將結束
+                                        type = "regularExamEndSoon";
+                                        DateTimeOffset dateTimeEnd = DateTimeOffset.FromUnixTimeMilliseconds(jointEventSchedule.endTime);
+                                        dateTimeEnd = dateTimeEnd.AddDays(-1);
+                                        DateTimeOffset emitTimeOffset = new DateTimeOffset(dateTimeEnd.Year, dateTimeEnd.Month, dateTimeEnd.Day, 0, 0, 0, DateTimeOffset.UtcNow.Offset);
+                                        emitTime = emitTimeOffset.ToUnixTimeMilliseconds();
+                                        var messageRegularSo = new ServiceBusMessage(new { jointEventId = $"{jointEventId}", jointScheduleId = $"{jointEventSchedule.id}", type = type, time = emitTime }.ToJsonString());
+                                        messageRegularSo.ApplicationProperties.Add("name", "JointMessage");
+                                        tablePartitionKey = string.Format("{0}{1}{2}{3}{4}", "JointMessage", "-", $"{jointEventId}", "-", $"{type}"); //主key: JointMessage-{jointEventId}-{type}  RowKey: {jointScheduleId}
+                                        tableRowKey = $"{jointEventSchedule.id}";
+                                        List<ChangeRecord> recordRegularSo = await table.FindListByDict<ChangeRecord>(new Dictionary<string, object>() { { "RowKey", tableRowKey }, { "PartitionKey", tablePartitionKey } });
+                                        if (recordRegularSo.Count > 0)
+                                        {
+                                            long sr = await _serviceBus.GetServiceBusClient().SendScheduleMessageAsync(Environment.GetEnvironmentVariable("Azure:ServiceBus:ActiveTask"), messageRegularSo, DateTimeOffset.FromUnixTimeMilliseconds(emitTime));
+                                            await _serviceBus.GetServiceBusClient().CancelMessageAsync(Environment.GetEnvironmentVariable("Azure:ServiceBus:ActiveTask"), records[0].sequenceNumber);
+                                            records[0].sequenceNumber = sr;
+                                            await table.SaveOrUpdate<ChangeRecord>(records[0]);
+                                        }
+                                        else
+                                        {
+                                            long sr = await _serviceBus.GetServiceBusClient().SendScheduleMessageAsync(Environment.GetEnvironmentVariable("Azure:ServiceBus:ActiveTask"), messageRegularSo, DateTimeOffset.FromUnixTimeMilliseconds(emitTime));
+                                            ChangeRecord changeRecord = new ChangeRecord
+                                            {
+                                                RowKey = tableRowKey,
+                                                PartitionKey = tablePartitionKey,
+                                                sequenceNumber = sr,
+                                                msgId = messageRegularSt.MessageId
+                                            };
+                                            await table.Save<ChangeRecord>(changeRecord);
+                                        }
+                                    }
+                                    //決賽
+                                    else if(jointEventSchedule.examType.Equals("custom"))
                                     {
-                                        foreach (string jointGroupId in jointGroupIds)
+                                        //決賽開始
+                                        type = "customExamStart";
+                                        emitTime = jointEventSchedule.startTime;
+                                        var messageRegularSt = new ServiceBusMessage(new { jointEventId = $"{jointEventId}", jointScheduleId = $"{jointEventSchedule.id}", type = type, time = emitTime }.ToJsonString());
+                                        messageRegularSt.ApplicationProperties.Add("name", "JointMessage");
+                                        tablePartitionKey = string.Format("{0}{1}{2}{3}{4}", "JointMessage", "-", $"{jointEventId}", "-", $"{type}"); //主key: JointMessage-{jointEventId}-{type}  RowKey: {jointScheduleId}
+                                        tableRowKey = $"{jointEventSchedule.id}";
+                                        List<ChangeRecord> recordRegularSt = await table.FindListByDict<ChangeRecord>(new Dictionary<string, object>() { { "RowKey", tableRowKey }, { "PartitionKey", tablePartitionKey } });
+                                        if (recordRegularSt.Count > 0)
                                         {
-                                            List<JointEventGroupDb> addResult = await JointService.CreatePassJointCourseBySchedule(client, $"{jointEventId}", jointGroupId, jointEventSchedule.id, scope);
+                                            long sr = await _serviceBus.GetServiceBusClient().SendScheduleMessageAsync(Environment.GetEnvironmentVariable("Azure:ServiceBus:ActiveTask"), messageRegularSt, DateTimeOffset.FromUnixTimeMilliseconds(emitTime));
+                                            await _serviceBus.GetServiceBusClient().CancelMessageAsync(Environment.GetEnvironmentVariable("Azure:ServiceBus:ActiveTask"), records[0].sequenceNumber);
+                                            records[0].sequenceNumber = sr;
+                                            await table.SaveOrUpdate<ChangeRecord>(records[0]);
+                                        }
+                                        else
+                                        {
+                                            long sr = await _serviceBus.GetServiceBusClient().SendScheduleMessageAsync(Environment.GetEnvironmentVariable("Azure:ServiceBus:ActiveTask"), messageRegularSt, DateTimeOffset.FromUnixTimeMilliseconds(emitTime));
+                                            ChangeRecord changeRecord = new ChangeRecord
+                                            {
+                                                RowKey = tableRowKey,
+                                                PartitionKey = tablePartitionKey,
+                                                sequenceNumber = sr,
+                                                msgId = messageRegularSt.MessageId
+                                            };
+                                            await table.Save<ChangeRecord>(changeRecord);
+                                        }
+                                        //決賽即將結束
+                                        type = "regularExamEndSoon";
+                                        DateTimeOffset dateTimeEnd = DateTimeOffset.FromUnixTimeMilliseconds(jointEventSchedule.endTime);
+                                        dateTimeEnd = dateTimeEnd.AddDays(-1);
+                                        DateTimeOffset emitTimeOffset = new DateTimeOffset(dateTimeEnd.Year, dateTimeEnd.Month, dateTimeEnd.Day, 0, 0, 0, DateTimeOffset.UtcNow.Offset);
+                                        emitTime = emitTimeOffset.ToUnixTimeMilliseconds();
+                                        var messageRegularSo = new ServiceBusMessage(new { jointEventId = $"{jointEventId}", jointScheduleId = $"{jointEventSchedule.id}", type = type, time = emitTime }.ToJsonString());
+                                        messageRegularSo.ApplicationProperties.Add("name", "JointMessage");
+                                        tablePartitionKey = string.Format("{0}{1}{2}{3}{4}", "JointMessage", "-", $"{jointEventId}", "-", $"{type}"); //主key: JointMessage-{jointEventId}-{type}  RowKey: {jointScheduleId}
+                                        tableRowKey = $"{jointEventSchedule.id}";
+                                        List<ChangeRecord> recordRegularSo = await table.FindListByDict<ChangeRecord>(new Dictionary<string, object>() { { "RowKey", tableRowKey }, { "PartitionKey", tablePartitionKey } });
+                                        if (recordRegularSo.Count > 0)
+                                        {
+                                            long sr = await _serviceBus.GetServiceBusClient().SendScheduleMessageAsync(Environment.GetEnvironmentVariable("Azure:ServiceBus:ActiveTask"), messageRegularSo, DateTimeOffset.FromUnixTimeMilliseconds(emitTime));
+                                            await _serviceBus.GetServiceBusClient().CancelMessageAsync(Environment.GetEnvironmentVariable("Azure:ServiceBus:ActiveTask"), records[0].sequenceNumber);
+                                            records[0].sequenceNumber = sr;
+                                            await table.SaveOrUpdate<ChangeRecord>(records[0]);
+                                        }
+                                        else
+                                        {
+                                            long sr = await _serviceBus.GetServiceBusClient().SendScheduleMessageAsync(Environment.GetEnvironmentVariable("Azure:ServiceBus:ActiveTask"), messageRegularSo, DateTimeOffset.FromUnixTimeMilliseconds(emitTime));
+                                            ChangeRecord changeRecord = new ChangeRecord
+                                            {
+                                                RowKey = tableRowKey,
+                                                PartitionKey = tablePartitionKey,
+                                                sequenceNumber = sr,
+                                                msgId = messageRegularSt.MessageId
+                                            };
+                                            await table.Save<ChangeRecord>(changeRecord);
                                         }
                                     }
                                 }
-
                                 break;
                         }
                     }
@@ -2743,6 +2800,90 @@ namespace TEAMModelOS.Function
 
         }
 
+        /// <summary>
+        /// 統測活動訊息寄送
+        /// </summary>
+        /// <param name="message"></param>
+        /// <param name="messageActions"></param>
+        /// <returns></returns>
+        [Function("JointEventSendMessage")]
+        public async Task JointEventSendMessageFunc(
+            [ServiceBusTrigger("%Azure:ServiceBus:ActiveTask%", "jointmessage", Connection = "Azure:ServiceBus:ConnectionString")]
+            ServiceBusReceivedMessage message,
+            ServiceBusMessageActions messageActions,
+            ExecutionContext context)
+        {
+            _logger.LogInformation("Message ID: {id}", message.MessageId);
+            _logger.LogInformation("Message Body: {body}", message.Body);
+            _logger.LogInformation("Message Content-Type: {contentType}", message.ContentType);
+            var jsonMsg = JsonDocument.Parse(message.Body).RootElement;
+
+            jsonMsg.TryGetProperty("jointEventId", out JsonElement jointEventId);
+            jsonMsg.TryGetProperty("jointScheduleId", out JsonElement jointScheduleId);
+            jsonMsg.TryGetProperty("type", out JsonElement type);
+            long time = (jsonMsg.TryGetProperty("time", out JsonElement _time)) ? _time.GetInt64() : new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds();
+
+            string location = Environment.GetEnvironmentVariable("Option:Location");
+            var client = _azureCosmos.GetCosmosClient();
+            string lang = (location.Contains("China")) ? "zh-cn" : "zh-tw";            
+
+            StringBuilder sqlStr = new StringBuilder($"SELECT DISTINCT c.creatorId, c.creatorName, c.creatorEmail FROM c WHERE c.jointEventId = '{jointEventId}' ");
+            string tid = string.Empty;
+            switch (type.ToString())
+            {
+                //熱身賽開始
+                case "regularExamStart":
+                    sqlStr.Append($" AND c.type = 'regular' ");
+                    tid = (location.Contains("China")) ? "" : "d-ba5d29036f81460c841fadaac7d35b6b"; //熱身賽開始
+                    break;
+                //熱身賽即將結束
+                case "regularExamEndSoon":
+                    sqlStr.Append($" AND c.type = 'regular' ");
+                    tid = (location.Contains("China")) ? "" : "d-359ec5e40e244aceb4d04f42e52af29d"; //熱身賽即將結束
+                    break;
+                //決賽開始
+                case "customExamStart":
+                    sqlStr.Append($" AND c.type = 'custom' ");
+                    break;
+                //決賽即將結束
+                case "customExamEndSoon":
+                    sqlStr.Append($" AND c.type = 'custom' ");
+                    break;
+            }
+            //取得收信者
+            List<dynamic> mailUsers = new List<dynamic>();
+            await foreach (var jc in client.GetContainer(Constant.TEAMModelOS, Constant.Teacher).GetItemQueryStreamIteratorSql(queryText: sqlStr.ToString(), requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"JointCourse") }))
+            {
+                using var json = await JsonDocument.ParseAsync(jc.Content);
+                if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
+                {
+                    foreach (var obj in json.RootElement.GetProperty("Documents").EnumerateArray())
+                    {
+                        EmailUser user = new EmailUser();
+                        user.tmid = (obj.TryGetProperty("creatorId", out JsonElement _creatorId)) ? _creatorId.GetString() : string.Empty;
+                        user.name = (obj.TryGetProperty("creatorName", out JsonElement _creatorName)) ? _creatorName.GetString() : string.Empty;
+                        user.email = (obj.TryGetProperty("creatorEmail", out JsonElement _creatorEmail)) ? _creatorEmail.GetString() : string.Empty;
+                        if (!string.IsNullOrWhiteSpace(user.tmid) && !string.IsNullOrWhiteSpace(user.email))
+                        {
+                            mailUsers.Add(user);
+                        }
+                    }
+                }
+            }
+            //寄發Email
+            if (mailUsers.Count > 0)
+            {
+                //熱身賽開始 Mail發送
+                foreach (dynamic user in mailUsers)
+                {
+                    if (!string.IsNullOrWhiteSpace(tid)) //※無模板ID不發
+                    {
+                        await _coreAPIHttpService.SendMail(new Dictionary<string, object> { { "to", user.email }, { "tid", tid }, { "vars", new { name = user.name } } }, location, _configuration);
+                    }
+                }
+            }
+        }
+
         private async Task RefreshBlob(string name, string u)
         {
             long statr = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

+ 4 - 4
TEAMModelOS.SDK/Models/Service/BI/TimeHelper.cs

@@ -311,12 +311,12 @@ namespace TEAMModelOS.SDK.Models.Service.BI
                             if (month >= 9)
                             {
                                 tempStrart = new DateTimeOffset(year, 9, 1, 0, 0, 0, TimeSpan.Zero);
-                                tempEnd = new DateTimeOffset(year + 1, 2, (year % 4 == 0 && year % 100 != 0 || year % 400 == 0) ? days = 29 : days = 28, 23, 59, 59, TimeSpan.Zero);
+                                tempEnd = new DateTimeOffset(year + 1, 2, DateTime.DaysInMonth(year + 1, 2), 23, 59, 59, TimeSpan.Zero);
                             }
                             else
                             {
                                 tempStrart = new DateTimeOffset(year - 1, 9, 1, 0, 0, 0, TimeSpan.Zero);
-                                tempEnd = new DateTimeOffset(year, 2, (year % 4 == 0 && year % 100 != 0 || year % 400 == 0) ? days = 29 : days = 28, 23, 59, 59, TimeSpan.Zero);
+                                tempEnd = new DateTimeOffset(year, 2, DateTime.DaysInMonth(year, 2), 23, 59, 59, TimeSpan.Zero);
                             }
                         }
                         break;
@@ -352,12 +352,12 @@ namespace TEAMModelOS.SDK.Models.Service.BI
                                 if (month >= 9)
                                 {
                                     tempStrart = new DateTimeOffset(year, 9, 1, 0, 0, 0, TimeSpan.Zero);
-                                    tempEnd = new DateTimeOffset(year + 1, 2, (year % 4 == 0 && year % 100 != 0 || year % 400 == 0) ? days = 29 : days = 28, 23, 59, 59, TimeSpan.Zero);
+                                    tempEnd = new DateTimeOffset(year + 1, 2, DateTime.DaysInMonth(year + 1, 2), 23, 59, 59, TimeSpan.Zero);
                                 }
                                 else
                                 {
                                     tempStrart = new DateTimeOffset(year - 1, 9, 1, 0, 0, 0, TimeSpan.Zero);
-                                    tempEnd = new DateTimeOffset(year, 2, (year % 4 == 0 && year % 100 != 0 || year % 400 == 0) ? days = 29 : days = 28, 23, 59, 59, TimeSpan.Zero);
+                                    tempEnd = new DateTimeOffset(year, 2, DateTime.DaysInMonth(year, 2), 23, 59, 59, TimeSpan.Zero);
                                 }
                             }
                         }

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

@@ -2874,7 +2874,8 @@ const LANG_EN_US = {
             paperExam: 'SelfPace Test (Instant Paper)',
             loadAll: 'All data has been loaded',
             crossSchool: "Cross-school assessment, you can't view the participants in the current school",
-            jointExam: 'Event'
+            jointExam: 'Event',
+            exportScore: 'Export grades',
         },
 
         //CreateEv

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

@@ -2873,7 +2873,8 @@ const LANG_ZH_CN = {
             paperExam: '纸本测验',
             loadAll: '已加载所有数据',
             crossSchool: '跨校评测,在当前学校无法查看发布对象',
-            jointExam: '活动评量'
+            jointExam: '活动评量',
+            exportScore: '汇出成绩',
         },
 
         //CreateEv

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

@@ -2876,7 +2876,12 @@ const LANG_ZH_TW = {
             paperExam: '紙本測驗',
             loadAll: '已載入所有數據',
             crossSchool: '跨校評量,在當前學校無法查看發布對象',
-            jointExam: '活動評量'
+            jointExam: '活動評量',
+            exportScore: '匯出成績',
+
+            teacherName: '老師',
+            courseName: '課程',
+            className: '班級',
         },
 
         //建立評量學校/個人

+ 4 - 1
TEAMModelOS/ClientApp/src/api/htcommunity.js

@@ -44,5 +44,8 @@ export default {
     jointTblobsas: function (data) {
         return post('/joint/tblobsas', data)
     },
-	
+    // 取得學生評量作答結果(複數)
+    findSummaryRecords: function (data) {
+        return post('/common/exam/find-summary-records', data)
+    },
 }

+ 33 - 0
TEAMModelOS/ClientApp/src/utils/excel.js

@@ -182,11 +182,44 @@ export const read = (data, type) => {
     return { header, results };
 }
 
+export const export_json_to_array = (key, jsonData) => {
+    return jsonData.map(v => key.map(j => { return v[j] }));
+}
+
+export const export_auto_width = (ws, data) => {
+    /*set worksheet max width per col*/
+    const colWidth = data.map(row => row.map(val => {
+        /*if null/undefined*/
+        if (val == null) {
+            return { 'wch': 140 };
+        }
+        /*if chinese*/
+        else if (val.toString().charCodeAt(0) > 255) {
+            return { 'wch': val.toString().length * 2 };
+        } else {
+            return { 'wch': val.toString().length };
+        }
+    }))
+    /*start in the first row*/
+    let result = colWidth[0];
+    for (let i = 1; i < colWidth.length; i++) {
+        for (let j = 0; j < colWidth[i].length; j++) {
+            if (result[j]['wch'] < colWidth[i][j]['wch']) {
+                result[j]['wch'] = colWidth[i][j]['wch'];
+            }
+        }
+    }
+    ws['!cols'] = result;
+    return ws;
+}
+
 export default {
     export_array_to_sheet,
     export_table_to_excel,
     export_array_to_excel,
     export_json_to_excel,
+    export_json_to_array,
+    export_auto_width,
     excelZipExport,
     read
 }

+ 291 - 160
TEAMModelOS/ClientApp/src/view/htcommunity/htMgtExam.vue

@@ -2,16 +2,16 @@
 	<div class="manage-evaluation-container custom-iview-split">
 		<Loading :top="200" type="1" style="text-align: center" v-show="isLoading"></Loading>
 		<div class="back-page"
-			style="margin-top: 15px;margin-left: 15px;margin-bottom: 15px;cursor: pointer;user-select: none;width: 50px;"
-			@click="goBack">
+			 style="margin-top: 15px;margin-left: 15px;margin-bottom: 15px;cursor: pointer;user-select: none;width: 50px;"
+			 @click="goBack">
 			<Icon type="md-arrow-round-back" />
 			{{ $t('cusMgt.rcd.rtn') }}
-		</div>		
+		</div>
 		<div style="margin: 10px">
-			<el-select v-model="filter.schedule"  style="margin-right: 30px;" @change="findEvaluation">
+			<el-select v-model="filter.schedule" style="margin-right: 30px;" @change="findEvaluation">
 				<el-option v-for="item in propSchedules" :key="item.id" :label="item.name" :value="item.id" />
 			</el-select>
-			<el-select v-model="filter.group"  @change="findEvaluation">
+			<el-select v-model="filter.group" @change="findEvaluation">
 				<el-option v-for="item in propGroups" :key="item.id" :label="item.name" :value="item.id" />
 			</el-select>
 		</div>
@@ -20,65 +20,65 @@
 			<div class="evaluation-list-wrap" slot="left">
 				<div class="evaluation-list-title">
 					<b class="title">
-						{{ scope == "school" ? $t("learnActivity.mgtScEv.listLabel1") :
-							$t("learnActivity.mgtScEv.listLabel2") }}
+						{{scope == "school" ? $t("learnActivity.mgtScEv.listLabel1") : $t("learnActivity.mgtScEv.listLabel2")}}
 					</b>
 					<div class="action-icon-wrap" v-if="!isSearch">
 						<Icon type="md-add" class="to-create-icon" @click="goToCreate"
-							:title="$t('learnActivity.mgtScEv.create')" v-show="isNotExamMark" />
+							  :title="$t('learnActivity.mgtScEv.create')" v-show="isNotExamMark" />
 						<!-- <Icon type="md-trash" v-show="htEvaListShow.length" class="to-create-icon"
-							:title="$t('learnActivity.mgtScEv.delete')" @click="deleteEvaluation" v-if="evAuthStatus" /> -->
+					:title="$t('learnActivity.mgtScEv.delete')" @click="deleteEvaluation" v-if="evAuthStatus" /> -->
 						<!-- 筛选 -->
 						<!-- <Poptip style="float: right" trigger="click" :offset="-10" theme="light">
-							<Icon type="ios-funnel" class="to-create-icon" />
-							<div slot="content">
-								<div class="filter-item">
-									<span>{{ $t("learnActivity.mgtScEv.ftStatus") }}</span>
-									<Select v-model="filter.progress" style="width: 100px" size="small" clearable
-										@on-change="filterEv">
-										<Option value="pending">{{ $t("learnActivity.mgtScEv.pending") }}</Option>
-										<Option value="going">{{ $t("learnActivity.mgtScEv.going") }}</Option>
-										<Option value="finish">{{ $t("learnActivity.mgtScEv.finish") }}</Option>
-									</Select>
-								</div>
-								<div class="filter-item">
-									<span>{{ $t("learnActivity.mgtScEv.ftMode") }}</span>
-									<Select v-model="filter.source" style="width: 100px" size="small" clearable
-										@on-change="filterEv">
-										<Option v-for="(item, index) in $GLOBAL.EV_MODE()" :value="item.value"
-											:key="index">{{ item.label }}</Option>
-									</Select>
-								</div>
+						<Icon type="ios-funnel" class="to-create-icon" />
+						<div slot="content">
+							<div class="filter-item">
+								<span>{{ $t("learnActivity.mgtScEv.ftStatus") }}</span>
+								<Select v-model="filter.progress" style="width: 100px" size="small" clearable
+									@on-change="filterEv">
+									<Option value="pending">{{ $t("learnActivity.mgtScEv.pending") }}</Option>
+									<Option value="going">{{ $t("learnActivity.mgtScEv.going") }}</Option>
+									<Option value="finish">{{ $t("learnActivity.mgtScEv.finish") }}</Option>
+								</Select>
 							</div>
-						</Poptip> -->
+							<div class="filter-item">
+								<span>{{ $t("learnActivity.mgtScEv.ftMode") }}</span>
+								<Select v-model="filter.source" style="width: 100px" size="small" clearable
+									@on-change="filterEv">
+									<Option v-for="(item, index) in $GLOBAL.EV_MODE()" :value="item.value"
+										:key="index">{{ item.label }}</Option>
+								</Select>
+							</div>
+						</div>
+					</Poptip> -->
 						<Icon type="md-search" class="to-create-icon" @click="isSearch = !isSearch"
-							:title="$t('learnActivity.mgtScEv.search')" />
+							  :title="$t('learnActivity.mgtScEv.search')" />
 					</div>
 					<div v-else style="float: right; width: 150px; padding-right: 10px" class="light-iview-input">
 						<Input v-special-char icon="ios-close" v-model="filter.name"
-							:placeholder="$t('schoolBaseInfo.codeSearchHolder')" autofocus style="width: 100%"
-							@on-click="closeKeySearch" @on-change="$jsFn.debounce(filterEv, 1000)()" />
+							   :placeholder="$t('schoolBaseInfo.codeSearchHolder')" autofocus style="width: 100%"
+							   @on-click="closeKeySearch" @on-change="$jsFn.debounce(filterEv, 1000)()" />
 					</div>
 				</div>
 				<Scroll class="evaluation-list-main" :on-reach-bottom="handleReachBottom" :distance-to-edge="[-1, -1]">
 					<div v-for="(item, index) in htEvaListShow" @click.capture="selectEvaluation(index)"
-						:class="['evaluation-item', 'block-bg', index == curEvaIndex ? 'block-bg-active' : '']"
-						:key="index">
+						 :class="['evaluation-item', 'block-bg', index == curEvaIndex ? 'block-bg-active' : '']"
+						 :key="index">
 						<p class="evaluation-name">
 							{{ item.name }}
 							<!-- 修改评测名称 -->
 							<Icon type="md-create" class="edit-end-time" @click="editEvName(index)"
-								:title="$t('learnActivity.mgtScEv.edName')" v-show="isNotExamMark" />
+								  :title="$t('learnActivity.mgtScEv.edName')" v-show="isNotExamMark" />
 						</p>
 						<div class="all-tag-box">
 							<div class="tags-wrap">
 								<!-- 活动进度状态 -->
 								<!-- <span class="evaluation-status-tag ev-tag-common" :style="{ background:item.progColor, color: item.progColor}">
-                                    {{ item.progText }}
-                                </span> -->
+									{{ item.progText }}
+								</span> -->
 								<!-- 评测模式 -->
-								<span class="ev-info-tag ev-tag-common">{{ getModeLabel(item.source)
-								}}</span>
+								<span class="ev-info-tag ev-tag-common">
+									{{getModeLabel(item.source)}}
+								</span>
 								<!-- <span class="ev-info-tag ev-tag-common" v-if="item.examType && item.examType.name">
 									{{ item.examType.name }}
 								</span> -->
@@ -90,12 +90,10 @@
 								<Icon class="exam-target" size="20" type="ios-people" @click="getExamTarget(item)" />
 							</div>
 							<!-- 立即结束 和评测状态 按钮-->
-							<!-- <span v-if="item.progress == 'going'" class="handle-end-tag ev-tag-common"
-								@click="handleEnd(index)">
+							<!-- <span v-if="item.progress == 'going'" class="handle-end-tag ev-tag-common" @click="handleEnd(index)">
 								{{ $t("learnActivity.mgtScEv.stop") }}
 							</span>
-							<span v-else class="handle-end-tag ev-tag-common"
-								:style="{ background: '#c0c0c0', color: item.progColor }">
+							<span v-else class="handle-end-tag ev-tag-common" :style="{ background: '#c0c0c0', color: item.progColor }">
 								{{ item.progText }}
 							</span> -->
 						</div>
@@ -106,13 +104,12 @@
 							-
 							<span>{{ $jsFn.timeFormat(item.endTime) }}</span>
 							<!-- 修改评测结束时间 -->
-							<!-- <Icon type="md-create" class="edit-end-time" v-if="item.progress == 'going'"
-								@click="editEvEndtime(index)" :title="$t('learnActivity.mgtScEv.editEndTime')" /> -->
+							<!-- <Icon type="md-create" class="edit-end-time" v-if="item.progress == 'going'" @click="editEvEndtime(index)" :title="$t('learnActivity.mgtScEv.editEndTime')" /> -->
 						</p>
 						<!-- 二维码分享 (暂时去掉二维码分享,仅提供链接,因为尚未兼容手机端)-->
 						<!-- <span class="ev-qr-tag" style="top:50%" @click="openQrcode(index)" v-show="item.source == '0' && item.progress == 'going'">
-                            <Icon size="20" custom="iconfont icon-qr-code" class="qr-code-icon" />
-                        </span> -->
+							<Icon size="20" custom="iconfont icon-qr-code" class="qr-code-icon" />
+						</span> -->
 						<!-- <span class="ev-qr-tag" style="top: 25%" v-if="item.source == '0' && item.progress == 'going'">
 							<Tooltip content="Here is the prompt text" transfer theme="light">
 								<Icon type="ios-send" size="24" />
@@ -126,64 +123,60 @@
 										{{ $t("learnActivity.mgtScEv.shareText10") }}
 									</p>
 								</div>
-							</Tooltip>
+								</Tooltip>
 						</span> -->
 						<!-- 立即结束 图标-->
 						<!-- <span class="ev-qr-tag" style="top:65%" @click="handleEnd(index)" :title="$t('learnActivity.mgtScEv.stop')">
-                                <Icon size="20" type="md-power" class="qr-code-icon" />
-                            </span> -->
+							<Icon size="20" type="md-power" class="qr-code-icon" />
+						</span> -->
 					</div>
-					<EmptyData v-if="htEvaListShow.length == 0" style="margin-top: 100px"
-						:textContent="`${$t('learnActivity.mgtScEv.nodata')}`"></EmptyData>
+					<EmptyData v-if="htEvaListShow.length == 0" style="margin-top: 100px" :textContent="`${$t('learnActivity.mgtScEv.nodata')}`"></EmptyData>
 				</Scroll>
 			</div>
 			<div slot="right" class="evaluation-detail-wrap">
 				<!--顶部菜单-->
-				<div class="evaluation-detail-bar">
-					<!-- 评测数据 -->
-					<!-- <span
-						:class="curBarIndex == 0 ? 'evalustion-bar-item line-bottom-active line-bottom' : 'evalustion-bar-item line-bottom'"
-						@click="selectBar(0)">
-						{{ $t("learnActivity.mgtScEv.tab1") }}
-					</span> -->
-					<!-- 作答详情 -->
-					<span
-						:class="curBarIndex == 3 ? 'evalustion-bar-item line-bottom-active line-bottom' : 'evalustion-bar-item line-bottom'"
-						@click="selectBar(3)">
-						{{ $t("learnActivity.mgtScEv.tab3") }}
-					</span>
-					<!-- 评测试卷 -->
-					<span
-						:class="curBarIndex == 1 ? 'evalustion-bar-item line-bottom-active line-bottom' : 'evalustion-bar-item line-bottom'"
-						@click="selectBar(1)" v-show="isNotExamMark">
-						{{ $t("learnActivity.mgtScEv.tab2") }}
-					</span>
-					<!-- 阅卷设置-->
-					<!-- <span
-						:class="curBarIndex == 2 ? 'evalustion-bar-item line-bottom-active line-bottom' : 'evalustion-bar-item line-bottom'"
-						v-show="scope == 'school'" @click="selectBar(2)">
-						{{ $t("learnActivity.mgtScEv.markSetting") }}
-					</span> -->
-					<!-- 小学情-->
-					<!-- <span
-						:class="curBarIndex == 4 ? 'evalustion-bar-item line-bottom-active line-bottom' : 'evalustion-bar-item line-bottom'"
-						v-show="scope !== 'school'" @click="selectBar(4)">
-						{{ $t("learnActivity.mgtScEv.tab4") }}
-					</span> -->
-				</div>
+				<el-row type="flex" justify="start" class="evaluation-detail-bar">
+					<!--Navi Tab-->
+					<el-col :span="22" >
+						<!-- 评测数据 -->
+						<!-- <span :class="curBarIndex == 0 ? 'evalustion-bar-item line-bottom-active line-bottom' : 'evalustion-bar-item line-bottom'" @click="selectBar(0)">
+							{{ $t("learnActivity.mgtScEv.tab1") }}
+						</span> -->
+						<!-- 作答详情 -->
+						<span :class="curBarIndex == 3 ? 'evalustion-bar-item line-bottom-active line-bottom' : 'evalustion-bar-item line-bottom'" @click="selectBar(3)">
+							{{ $t("learnActivity.mgtScEv.tab3") }}
+						</span>
+						<!-- 评测试卷 -->
+						<span :class="curBarIndex == 1 ? 'evalustion-bar-item line-bottom-active line-bottom' : 'evalustion-bar-item line-bottom'" @click="selectBar(1)" v-show="isNotExamMark">
+							{{ $t("learnActivity.mgtScEv.tab2") }}
+						</span>
+						<!-- 阅卷设置-->
+						<!-- <span :class="curBarIndex == 2 ? 'evalustion-bar-item line-bottom-active line-bottom' : 'evalustion-bar-item line-bottom'" v-show="scope == 'school'" @click="selectBar(2)">
+							{{ $t("learnActivity.mgtScEv.markSetting") }}
+						</span> -->
+						<!-- 小学情-->
+						<!-- <span :class="curBarIndex == 4 ? 'evalustion-bar-item line-bottom-active line-bottom' : 'evalustion-bar-item line-bottom'" v-show="scope !== 'school'" @click="selectBar(4)">
+							{{ $t("learnActivity.mgtScEv.tab4") }}
+						</span> -->
+					</el-col>
+					<!--右側按鈕-->
+					<el-col :span="2">
+						<el-row type="flex" justify="end">
+							<!-- 匯出成績 -->
+							<el-button @click="showExportScoreModalFunc" long type="primary" style="width: 100%; margin-top: 2px;" v-if="false">
+								{{$t("learnActivity.mgtScEv.exportScore")}}
+							</el-button>
+						</el-row>
+					</el-col>
+				</el-row>
 				<!-- 暂无数据 -->
 				<EmptyData v-if="!htEvaListShow.length" :top="120"></EmptyData>
-
 				<!-- 评测数据 -->
-				<DataView v-else-if="curBarIndex == 0" :examInfo="examDetaiInfo" :correctData="correctData"
-					:simpleData="simpleData" :dataErr="dataErr" class="evaluation-base-info"></DataView>
+				<DataView v-else-if="curBarIndex == 0" :examInfo="examDetaiInfo" :correctData="correctData" :simpleData="simpleData" :dataErr="dataErr" class="evaluation-base-info"></DataView>
 				<!-- 作答详情 -->
-				<AnswerTable v-else-if="curBarIndex == 3" :examInfo="examDetaiInfo" :correctData="correctData"
-					class="evaluation-base-info"></AnswerTable>
+				<AnswerTable v-else-if="curBarIndex == 3" :examInfo="examDetaiInfo" :correctData="correctData" class="evaluation-base-info"></AnswerTable>
 				<!-- 评测试卷 -->
-				<ExamPaper v-else-if="curBarIndex == 1" :examInfo="examDetaiInfo" :activityIndex="curEvaIndex"
-					:refreshExam="refreshExam" class="evaluation-base-info"></ExamPaper>
-
+				<ExamPaper v-else-if="curBarIndex == 1" :examInfo="examDetaiInfo" :activityIndex="curEvaIndex" :refreshExam="refreshExam" class="evaluation-base-info"></ExamPaper>
 			</div>
 		</Split>
 		<!-- 修改评测名称 -->
@@ -196,8 +189,9 @@
 					{{ $t("cusMgt.listName") }}
 				</p>
 				<Input v-model="editName" :placeholder="$t('learnActivity.mgtScEv.edNameHolder')" />
-				<Button :loading="btnLoading" @click="confirmEditName" long type="primary" class="confirm-btn">{{
-					$t("syllabus.confirm") }}</Button>
+				<Button :loading="btnLoading" @click="confirmEditName" long type="primary" class="confirm-btn">
+					{{$t("syllabus.confirm")}}
+				</Button>
 			</div>
 		</Modal>
 		<!-- 修改评测时间 -->
@@ -210,21 +204,35 @@
 					{{ $t("cusMgt.listName") }}
 				</p>
 				<DatePicker v-model="editTime" :options="dateOpt1" type="datetime" @on-change="handleTime"
-					format="yyyy-MM-dd HH:mm" :placeholder="$t('learnActivity.mgtScEv.endTimeHolder')" style="width: 100%">
+							format="yyyy-MM-dd HH:mm" :placeholder="$t('learnActivity.mgtScEv.endTimeHolder')" style="width: 100%">
 				</DatePicker>
-				<Button :loading="btnLoading" @click="confirmEditEndtime" long type="primary" class="confirm-btn">{{
-					$t("syllabus.confirm") }}</Button>
+				<Button :loading="btnLoading" @click="confirmEditEndtime" long type="primary" class="confirm-btn">
+					{{$t("syllabus.confirm")}}
+				</Button>
 			</div>
 		</Modal>
 		<!-- 分享二维码 -->
 		<QrcodeModal v-model="showQrStatus" :config="qrConfig"></QrcodeModal>
+		<!-- 匯出成績彈出視窗 -->
+		<el-dialog :title="this.$t('learnActivity.mgtScEv.exportScore')" :visible.sync="showExportScoreModal" width="40%">
+			<el-button type="primary" class="btn-new" size="small" style="margin-top: 0; margin-bottom: 10px; margin-left: 0; " @click="exportExcel('teacherCourse')">{{this.$t("htcommunity.export")}}</el-button>
+			<c-scrollbar ref="scrollbarRef" width="100%" height="66vh" trigger="hover" direction="y">
+				<el-table class="export-group-tb" :data="groupListForExport" style="width: 100%" @selection-change="handleSelectionChange">
+					<el-table-column prop="teacherName" :label="$t('learnActivity.mgtScEv.teacherName')"></el-table-column>
+					<el-table-column prop="courseName" :label="$t('learnActivity.mgtScEv.courseName')"></el-table-column>
+					<el-table-column prop="groupListName" :label="$t('learnActivity.mgtScEv.className')"></el-table-column>
+					<el-table-column type="selection" :label="$t('purchase.action')"></el-table-column>
+				</el-table>
+			</c-scrollbar>
+		</el-dialog>
 	</div>
 </template>
 <script>
 import DataView from "../learnactivity/tabs/DataView.vue";
 import AnswerTable from "../learnactivity/tabs/htAnswerTable.vue";
 import ExamPaper from "../learnactivity/tabs/htExamPaper.vue";
-
+import XLSX from 'xlsx';
+import excel from '@/utils/excel.js'
 
 export default {
 	components: {
@@ -278,6 +286,10 @@ export default {
 				group: ""
 			},
 			semesterRange: null,
+			exportScoreLoading: false,
+			showExportScoreModal: false,
+            groupListForExport: [], //匯出成績視窗 課程名單列表
+            importSelection: [], //(匯入成績時)選擇中的課程名單
 			// activeGroups:[
 			// 	{
 			// 		id:"1",
@@ -375,7 +387,6 @@ export default {
 					})
 				})
 			})
-
             this.$Modal.info({
 							title: this.$t("learnActivity.createEv.evTarget"),
 							render: (h) => {
@@ -631,6 +642,7 @@ export default {
 								this.isLoading = false;
 							}
 							this.isLoading = false;
+                            console.log('htEvaListShow', this.htEvaListShow);
 						} else {
 							this.isLoading = false;							
 						}
@@ -669,6 +681,7 @@ export default {
 		},
 		// 點擊左側個人評量列表項目的動作
 		selectEvaluation(index) {
+			console.log('點擊左側個人評量列表項目的動作');
 			this.checkScoreSave(this.toEvaluation, index);
 			this.$EventBus.$emit("onEvaChange", this.htEvaListShow[index]);
 		},
@@ -790,72 +803,72 @@ export default {
 			// 	});
 		
 			this.dataErr = false;
-			
-				let resData = this.htEvaListShow[this.curEvaIndex];
-				resData.score = 0;
-				for (let index in resData.papers) {
-					let blob = resData.papers[index].blob;
-					let sheetNo = resData.papers[index].sheetNo;
-					let totlaSocre = 0;
-					if (resData.papers[index].point) {
-						totlaSocre = resData.papers[index].point.reduce((prev, next, index, array) => {
-							return prev + next;
-						});
-					}
+			let resData = this.htEvaListShow[this.curEvaIndex];
+			resData.score = 0;
+			for (let index in resData.papers) {
+				let blob = resData.papers[index].blob;
+				let sheetNo = resData.papers[index].sheetNo;
+				let totlaSocre = 0;
+				if (resData.papers[index].point) {
+					totlaSocre = resData.papers[index].point.reduce((prev, next, index, array) => {
+						return prev + next;
+					});
+				}
 					
 					
-					resData.examPaperErr = false;
-					resData.papers[index].examScope = resData.scope;
-					resData.papers[index].examCode = resData.code;
-					resData.papers[index].examId = resData.id;
-					resData.papers[index].owner = resData.owner;
-					resData.papers[index].sheetNo = sheetNo;
-					try {
-						let tmdid = this.htEvaListShow[this.curEvaIndex].creatorId;
-						if (tmdid != this.$store.state.userInfo.TEAMModelId && this.htEvaListShow[this.curEvaIndex].scope != 'school') {
-							resData.papers[index] = await this.$evTools.getFullPaperByTmdId(tmdid, resData.papers[index].blob)
-						} else {
-							resData.papers[index] = await this.$evTools.getFullPaper(resData.papers[index])
-						}
-					} catch (e) {
-						resData.papers[index].item = [];
-						resData.examPaperErr = true;
-						this.$Message.error(this.$t("learnActivity.simple.paperErr"));
-					}
-					resData.papers[index].examScope = resData.scope;
-					resData.papers[index].examCode = resData.code;
-					resData.papers[index].examId = resData.id;
-					resData.papers[index].examSchool = resData.school;
-					resData.papers[index].examSubject = resData.subjects;
-					resData.papers[index].creatorId = resData.creatorId;
-					resData.papers[index].owner = resData.owner;
-					resData.papers[index].sheetNo = sheetNo;
-					resData.papers[index].totlaSocre = totlaSocre;
-					if (!resData.papers[index].subjectId) {
-						resData.papers[index].subjectId = blob.substring(blob.lastIndexOf("/") + 1);
-						// resData.papers[index].subjectName = resData.subjects[index].name;
-						resData.papers[index].subjectName = resData.name;
+				resData.examPaperErr = false;
+				resData.papers[index].examScope = resData.scope;
+				resData.papers[index].examCode = resData.code;
+				resData.papers[index].examId = resData.id;
+				resData.papers[index].owner = resData.owner;
+				resData.papers[index].sheetNo = sheetNo;
+				try {
+					let tmdid = this.htEvaListShow[this.curEvaIndex].creatorId;
+					if (tmdid != this.$store.state.userInfo.TEAMModelId && this.htEvaListShow[this.curEvaIndex].scope != 'school') {
+						resData.papers[index] = await this.$evTools.getFullPaperByTmdId(tmdid, resData.papers[index].blob)
+					} else {
+						resData.papers[index] = await this.$evTools.getFullPaper(resData.papers[index])
 					}
-					resData.papers[index].blob = blob;
-					resData.score += resData.papers[index].score;
-
-					//补充评测状态信息
-					let statusInfo = this.getEvStatusInfo(resData.progress, resData.sStatus);
-					resData.progText = statusInfo.progText;
-					resData.progColor = statusInfo.progColor;
-					resData.scoreText = statusInfo.scoreText;
-					resData.scoreColor = statusInfo.scoreColor;
-					resData.curSchool = !(resData.scope === "school" && resData.school != this.$store.state.userInfo.schoolCode);
-					//设置各科题号信息
-					this.setPaperQuInfo(resData.papers);
+				} catch (e) {
+					resData.papers[index].item = [];
+					resData.examPaperErr = true;
+					this.$Message.error(this.$t("learnActivity.simple.paperErr"));
 				}
-				this.htEvaListShow.splice(this.curEvaIndex, 1, resData);
-
-				this.examDetaiInfo = JSON.parse(JSON.stringify(resData));
-				// if (this.examDetaiInfo.progress === 'finish' && !this.dataErr) {
-				if (this.examDetaiInfo.progress === "finish") {
-					this.findSimpleAna();
+				resData.papers[index].examScope = resData.scope;
+				resData.papers[index].examCode = resData.code;
+				resData.papers[index].examId = resData.id;
+				resData.papers[index].examSchool = resData.school;
+				resData.papers[index].examSubject = resData.subjects;
+				resData.papers[index].creatorId = resData.creatorId;
+				resData.papers[index].owner = resData.owner;
+				resData.papers[index].sheetNo = sheetNo;
+				resData.papers[index].totlaSocre = totlaSocre;
+				if (!resData.papers[index].subjectId) {
+					resData.papers[index].subjectId = blob.substring(blob.lastIndexOf("/") + 1);
+					// resData.papers[index].subjectName = resData.subjects[index].name;
+					resData.papers[index].subjectName = resData.name;
 				}
+				resData.papers[index].blob = blob;
+				resData.score += resData.papers[index].score;
+
+				//补充评测状态信息
+				let statusInfo = this.getEvStatusInfo(resData.progress, resData.sStatus);
+				resData.progText = statusInfo.progText;
+				resData.progColor = statusInfo.progColor;
+				resData.scoreText = statusInfo.scoreText;
+				resData.scoreColor = statusInfo.scoreColor;
+				resData.curSchool = !(resData.scope === "school" && resData.school != this.$store.state.userInfo.schoolCode);
+				//设置各科题号信息
+				this.setPaperQuInfo(resData.papers);
+			}
+			this.htEvaListShow.splice(this.curEvaIndex, 1, resData);
+
+			this.examDetaiInfo = JSON.parse(JSON.stringify(resData));
+			// if (this.examDetaiInfo.progress === 'finish' && !this.dataErr) {
+			if (this.examDetaiInfo.progress === "finish") {
+				this.findSimpleAna();
+			}
+			console.log('examDetaiInfo', this.examDetaiInfo);
 			
 		},
 		// 设置各科题号信息
@@ -1086,6 +1099,96 @@ export default {
 			this.$Message.success(activeId);
 
 		},
+		//顯示匯出成績視窗
+		showExportScoreModalFunc() {
+			this.showExportScoreModal = true;
+		},
+		//勾選匯出成績課程名單
+        handleSelectionChange(val) {
+            this.importSelection = val;
+		},
+		//匯出所有活動成績至Excel
+		async exportExcel() {
+			console.log('勾選匯出成績課程名單', this.importSelection);
+			this.$api.htcommunity.findSummaryRecords({ data: this.importSelection })
+            .then(
+                (res) => {
+					debugger;
+					console.log('findSummaryRecords res:', res);
+                },
+                (err) => {
+                    this.$Message.error(this.$t("learnActivity.mgtScEv.exInfoErr"));
+                }
+            )
+            .finally(() => {
+                
+            });
+
+
+            let title = ['學校', '老師', '班級', '評量名01', '評量名02', '總平均', '排名'];
+			let key = ['school', 'creator', 'no', 'groupId', 'groupName'];
+			let data = [
+				['AAA', 123456789, 18, 'xx省oo縣', 'xx省oo縣', 100, 1],
+				['BBB', 789456123, 20, 'cc省rr縣', 'cc省rr縣', 90, 2],
+				['CCC', 741852963, 25, 'hh省zz縣', 'hh省zz縣', 80, 3]
+			];
+			let autoWidth = true;
+			let filename = '檔案名';
+			let wb = XLSX.utils.book_new();
+
+            let arr = excel.export_json_to_array(key, data);
+			arr.unshift(title);
+            arr.unshift(['合併欄內容', '', '', '', '', '', '']);
+            let ws = XLSX.utils.aoa_to_sheet(arr);
+            ws = excel.export_auto_width(ws, arr);
+			ws["!merges"] = [{
+				s: {//s为开始
+					c: 0,//开始列
+					r: 0//开始取值范围
+				},
+				e: {//e结束
+					c: 6,//结束列
+					r: 0//结束范围
+				}
+			}];
+            XLSX.utils.book_append_sheet(wb, ws);
+            let wbout = XLSX.write(wb, {
+                bookType: 'xlsx',
+                bookSST: true,
+                type: 'array'
+			});
+
+			let ws2 = XLSX.utils.aoa_to_sheet(arr);
+            ws2 = excel.export_auto_width(ws2, arr);
+            XLSX.utils.book_append_sheet(wb, ws2);
+
+            XLSX.writeFile(wb, filename + '.xlsx');
+
+
+
+   //         const params = {
+   //             title: this.columns.map(i => i.title),
+   //             key: this.columns.map(i => i.key),
+   //             data: this.originData,
+   //             autoWidth: true,
+			//	filename: this.tableName + '(' + subjectName + '、' + className + ')',
+   //             setTitle: {
+   //                 title: className,
+   //                 rules: [{//合并第一行数据
+   //                     s: {//s为开始
+   //                         c: 0,//开始列
+   //                         r: 0//开始取值范围
+   //                     },
+   //                     e: {//e结束
+   //                         c: 4,//结束列
+   //                         r: 0//结束范围
+   //                     }
+   //                 }]
+   //             }
+   //         }
+			//excel.export_array_to_excel(params);
+
+		},
 	},
 	watch: {
 		curBarIndex(n, o) {
@@ -1097,6 +1200,31 @@ export default {
 				}
 			}
 		},
+        examDetaiInfo: {
+			handler(n) {
+				this.groupListForExport = [];
+				n.stuLists.forEach((s) => {
+					s.courseLists.forEach((c) => {
+						c.groupLists.forEach((g) => {
+							let res = {
+                                schoolId: s.schoolId,
+								schoolName: s.schoolName,
+                                creatorId: s.creatorId,
+								creatorName: s.creatorName,
+                                subjectId: c.courseId,
+								courseId: c.courseId,
+								courseName: c.courseName,
+                                examId: (c.examId != null) ? c.examId : '',
+                                groupListId: g.id,
+								groupListName: g.name,
+                                teacherName: s.creatorName + ' (' + s.schoolName + ')'
+							};
+                            this.groupListForExport.push(res);
+                        });
+                    });
+                });
+			}
+		}
 		// "$store.state.user.curSemester": {
 		// 	deep: true,
 		// 	immediate: true,
@@ -1124,4 +1252,7 @@ export default {
 		height: 100% !important;
 	}
 }
+.export-group-tb {
+    line-height: 10px !important;
+}
 </style>

+ 206 - 0
TEAMModelOS/Controllers/Common/ExamController.cs

@@ -2276,6 +2276,204 @@ namespace TEAMModelOS.Controllers
                 return BadRequest();
             }
 
+        }
+        /// <summary>
+        /// 查询複數评测详细信息(教师用)
+        /// </summary>
+        /// <param name="request"></param>
+        /// <returns></returns>
+        [ProducesDefaultResponseType]
+        [Authorize(Roles = "IES")]
+        [AuthToken(Roles = "teacher,admin")]
+        [HttpPost("find-summary-records")]
+        public async Task<IActionResult> findSummaryRecords(JsonElement request)
+        {
+            try
+            {
+                if (!request.TryGetProperty("data", out JsonElement data)) return BadRequest();
+                List<FindExamClassResultsReqParam> requestParams = data.ToObject<List<FindExamClassResultsReqParam>>();
+                StringBuilder sqlClassResult = new StringBuilder($"SELECT c.id,c.subjectId,c.code,c.scIds,c.info,c.studentIds,c.studentAnswers,c.studentScores,c.mark,c.status FROM c WHERE ");
+                StringBuilder sqlClassResultWhere = new StringBuilder();
+                foreach (FindExamClassResultsReqParam param in requestParams)
+                {
+                    if(!sqlClassResultWhere.Length.Equals(0))
+                    {
+                        sqlClassResultWhere.Append(" OR ");
+                    }
+                    sqlClassResultWhere.Append($"( c.code = 'ExamClassResult-{param.creatorId}' ");
+                    sqlClassResultWhere.Append($" AND c.examId = '{param.examId}' ");
+                    sqlClassResultWhere.Append($" AND c.subjectId = '{param.subjectId}' ");
+                    sqlClassResultWhere.Append($" AND c.info.id = '{param.groupListId}' )");
+                }
+                sqlClassResult.Append(sqlClassResultWhere);
+                var client = _azureCosmos.GetCosmosClient();
+                List<string> ids = new(); //學生ID
+                Dictionary<string, List<string>> examClassResultSchoolGroupDic = new Dictionary<string, List<string>>(); //examClassResult 學校ID=>班級ID對應表 ※成員訊息用
+                Dictionary<string, List<string>> examClassResultCodeIdDic = new Dictionary<string, List<string>>(); //examClassResult code對應ID表 ※取得閱卷變更結果用
+                List<ExamClassResult> examClassResults = new List<ExamClassResult>();
+                await foreach (var item in client.GetContainer(Constant.TEAMModelOS, "Common").GetItemQueryIteratorSql<ExamClassResult>(queryText: sqlClassResult.ToString(), requestOptions: null ))
+                {
+                    examClassResults.Add(item);
+                    ids.AddRange(item.studentIds);
+                    //code =>ID 對應表
+                    if (!examClassResultCodeIdDic.ContainsKey(item.code)) examClassResultCodeIdDic.Add(item.code, new List<string>() { item.id });
+                    else if (!examClassResultCodeIdDic[item.code].Contains(item.id)) examClassResultCodeIdDic[item.code].Add(item.id);
+                    //學校ID=>班級ID對應表
+                    string sid = (!string.IsNullOrWhiteSpace(item.school)) ? item.school : string.Empty;
+                    if (!examClassResultSchoolGroupDic.ContainsKey(sid)) examClassResultSchoolGroupDic.Add(sid, new List<string>() { item.info.id });
+                    else if (!examClassResultSchoolGroupDic[sid].Contains(item.info.id)) examClassResultSchoolGroupDic[sid].Add(item.info.id);
+                }
+                //取得成員名單
+                List<RMember> members = new List<RMember>();
+                List<RGroupList> groups = new List<RGroupList>();
+                foreach (KeyValuePair<string, List<string>> item in examClassResultSchoolGroupDic)
+                {
+                    string schId = item.Key;
+                    List<string> cids = item.Value;
+                    (List<RMember> member, List<RGroupList> group) = await GroupListService.GetMemberByListids(_coreAPIHttpService, client, _dingDing, cids, $"{schId}", null);
+                    members.AddRange(member);
+                    groups.AddRange(group);
+                }
+                members = members.Distinct().ToList();
+                groups = groups.Distinct().ToList();
+                ///获取真实的名称 
+                List<ufo> ufos = new();
+                List<string> delIds = new();
+                if (groups.Count > 0)
+                {
+                    if (ids.Count > 0)
+                    {
+                        var sSchIds = members.Where(c => c.type == 2).Select(z => new { z.id, z.schoolId }).ToList();
+                        Dictionary<string, List<string>> SchIdDic = new Dictionary<string, List<string>>(); //學校ID => TMID 字典
+                        foreach(var schId in sSchIds)
+                        {
+                            string sch = schId.schoolId;
+                            string tmid = schId.id;
+                            if(SchIdDic.ContainsKey(sch))
+                            {
+                                if (!SchIdDic[sch].Contains(tmid))
+                                {
+                                    SchIdDic[sch].Add(tmid);
+                                }
+                            }
+                            else
+                            {
+                                SchIdDic.Add(sch, new List<string>() { tmid });
+                            }
+                        }
+                        List<string> sIds = sSchIds.Select(z => z.id).ToList();
+                        List<string> tIds = members.Where(c => c.type == 1).Select(z => z.id).ToList();
+                        List<string> pIds = members.Where(c => c.type == 3).Select(z => z.id).ToList(); //簡易課程名單成員
+                        List<ufo> students = new List<ufo>();
+                        if (tIds.Any())
+                        {
+                            var tmds = ids.Intersect(tIds).ToList();
+                            if (tmds.Any())
+                            {
+                                var content = new StringContent(tmds.ToJsonString(), Encoding.UTF8, "application/json");
+                                string json = await _coreAPIHttpService.GetUserInfos(content);
+                                if (!string.IsNullOrWhiteSpace(json))
+                                {
+                                    try
+                                    {
+                                        List<ufo> tmdInfos = json.ToObject<List<ufo>>();
+                                        if (tmdInfos.IsNotEmpty())
+                                        {
+                                            foreach (ufo fo in tmdInfos)
+                                            {
+                                                fo.type = 1;
+                                            }
+                                            ufos.AddRange(tmdInfos);
+                                        }
+                                    }
+                                    catch (Exception ex)
+                                    {
+                                        //await _dingDing.SendBotMsg($"{_coreAPIHttpService.options.location}用户转换失败:{_coreAPIHttpService.options.coreUrl}{json}\n ", GroupNames.醍摩豆服務運維群組);
+                                    }
+                                }
+                            }
+
+                        }
+                        if (sIds.Any())
+                        {
+                            var stus = ids.Intersect(sIds).ToList();
+                            if (stus.Any())
+                            {
+                                foreach(KeyValuePair<string, List<string>> ipare in SchIdDic)
+                                {
+                                    string schId = ipare.Key;
+                                    List<string> stuIds = ipare.Value;
+                                    await foreach (var item in client.GetContainer(Constant.TEAMModelOS, "Student").GetItemQueryStreamIteratorSql(queryText: $"select distinct c.id,c.name from c where c.id in ({string.Join(",", stuIds.Select(o => $"'{o}'"))}) and c.pk = 'Base'", requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Base-{schId}") }))
+                                    {
+                                        using var stuJson = await JsonDocument.ParseAsync(item.Content);
+                                        if (stuJson.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
+                                        {
+                                            var accounts = stuJson.RootElement.GetProperty("Documents").EnumerateArray();
+                                            while (accounts.MoveNext())
+                                            {
+                                                JsonElement account = accounts.Current;
+                                                ufo fo = new()
+                                                {
+                                                    id = account.GetProperty("id").GetString(),
+                                                    name = account.GetProperty("name").GetString(),
+                                                    //schoolId = account.GetProperty("schoolId").GetString(),
+                                                    type = 2
+                                                };
+                                                students.Add(fo);
+                                            }
+                                        }
+                                    }
+                                }
+                                ufos.AddRange(students);
+                            }
+                        }
+                        if (pIds.Any()) //簡易課程名單處理
+                        {
+                            var ptus = ids.Intersect(pIds).ToList();
+                            if (ptus.Any())
+                            {
+                                List<RMember> simples = members.Where(c => ptus.Contains(c.id)).ToList();
+                                foreach (RMember simple in simples)
+                                {
+                                    ufos.Add(new ufo()
+                                    {
+                                        id = simple.id,
+                                        name = (!string.IsNullOrWhiteSpace(simple.name)) ? simple.name : (!string.IsNullOrWhiteSpace(simple.nickname)) ? simple.nickname : "",
+                                        type = simple.type
+                                    });
+                                }
+                            }
+                        }
+                    }
+                }
+                //取得教師批改紀錄 ※目前只有統測評量有紀錄
+                List<ExamClassResultMark> markResult = new List<ExamClassResultMark>();
+                foreach (KeyValuePair<string, List<string>> seed in examClassResultCodeIdDic)
+                {
+                    string code = seed.Key.Replace("ExamClassResult", "ExamClassResultMark");
+                    List<string> idList = seed.Value;
+                    await foreach (var item in client.GetContainer(Constant.TEAMModelOS, Constant.Common).GetItemQueryStreamIteratorSql(queryText: $"SELECT * FROM c where c.id in ({string.Join(",", idList.Select(o => $"'{o}'"))}) and c.pk = 'ExamClassResultMark'",
+                                    requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey(code) }))
+                    {
+                        using var json = await JsonDocument.ParseAsync(item.Content);
+                        if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
+                        {
+                            foreach (var obj in json.RootElement.GetProperty("Documents").EnumerateArray())
+                            {
+                                markResult.Add(obj.ToObject<ExamClassResultMark>());
+                            }
+                        }
+                    }
+                }
+
+                return Ok(new { examClassResults, ufos, delIds, markResult });
+            }
+            catch (Exception ex)
+            {
+                //await _dingDing.SendBotMsg($"OS,{_option.Location},exam/findSummaryRecord()\n{ex.Message}\n{ex.StackTrace}\n\n{id.GetString()}", GroupNames.醍摩豆服務運維群組);
+                return BadRequest();
+            }
+
         }
         //学生端查询评测列表
         [ProducesDefaultResponseType]
@@ -4929,4 +5127,12 @@ namespace TEAMModelOS.Controllers
     {
         public string jointExamType { get; set; }
     }
+    //取得複數學生作答成績 Request參數 
+    public class FindExamClassResultsReqParam
+    {
+        public string creatorId { get; set; }
+        public string examId { get; set; }
+        public string subjectId { get; set; }
+        public string groupListId { get; set; }
+    }
 }

+ 1 - 0
TEAMModelOS/Controllers/Teacher/JointEventController.cs

@@ -23,6 +23,7 @@ using TEAMModelOS.SDK.Models.Service;
 using Azure.Messaging.ServiceBus;
 using Azure.Storage.Sas;
 using System.IdentityModel.Tokens.Jwt;
+using TEAMModelOS.Controllers.Core;
 
 namespace TEAMModelOS.Controllers.Common
 {