Explorar o código

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

CrazyIter_Bin hai 10 meses
pai
achega
ceccf4b9d1
Modificáronse 24 ficheiros con 4705 adicións e 40 borrados
  1. 128 1
      TEAMModelOS.Function/IESServiceBusTrigger.cs
  2. 5 0
      TEAMModelOS.FunctionV4/CosmosDB/TriggerExam.cs
  3. 34 1
      TEAMModelOS/ClientApp/public/lang/en-US.js
  4. 34 1
      TEAMModelOS/ClientApp/public/lang/zh-CN.js
  5. 35 1
      TEAMModelOS/ClientApp/public/lang/zh-TW.js
  6. 35 0
      TEAMModelOS/ClientApp/src/api/htcommunity.js
  7. 3 0
      TEAMModelOS/ClientApp/src/api/index.js
  8. 14 6
      TEAMModelOS/ClientApp/src/boot-app.js
  9. 42 0
      TEAMModelOS/ClientApp/src/common/BaseLayout.vue
  10. 45 0
      TEAMModelOS/ClientApp/src/router/routes.js
  11. 1896 0
      TEAMModelOS/ClientApp/src/static/region_gl.json
  12. 2 1
      TEAMModelOS/ClientApp/src/utils/js-fn.js
  13. 3 3
      TEAMModelOS/ClientApp/src/utils/public.js
  14. 4 4
      TEAMModelOS/ClientApp/src/view/Home.vue
  15. 85 0
      TEAMModelOS/ClientApp/src/view/htcommunity/htMgtAnnouncement.vue
  16. 136 0
      TEAMModelOS/ClientApp/src/view/htcommunity/htMgtAnnouncementDetail.vue
  17. 121 0
      TEAMModelOS/ClientApp/src/view/htcommunity/htMgtCompleteProgress.vue
  18. 179 0
      TEAMModelOS/ClientApp/src/view/htcommunity/htMgtExam.less
  19. 1098 0
      TEAMModelOS/ClientApp/src/view/htcommunity/htMgtExam.vue
  20. 745 0
      TEAMModelOS/ClientApp/src/view/htcommunity/htMgtHome.vue
  21. 2 2
      TEAMModelOS/ClientApp/src/view/knowledge-point/index/page.vue
  22. 1 1
      TEAMModelOS/ClientApp/src/view/task/marking/mark/ByStu.vue
  23. 36 16
      TEAMModelOS/Controllers/Client/HiTeachController.cs
  24. 22 3
      TEAMModelOS/Controllers/Teacher/JointEventController.cs

+ 128 - 1
TEAMModelOS.Function/IESServiceBusTrigger.cs

@@ -2081,7 +2081,6 @@ namespace TEAMModelOS.Function
             var json = JsonDocument.Parse(message.Body);
             try
             {
-
                 json.RootElement.TryGetProperty("id", out JsonElement id);
                 json.RootElement.TryGetProperty("progress", out JsonElement progress);
                 json.RootElement.TryGetProperty("code", out JsonElement code);
@@ -2104,6 +2103,134 @@ namespace TEAMModelOS.Function
             }
 
         }
+        /// <summary>
+        /// 統測活動行程進行狀況更新
+        /// </summary>
+        /// <param name="message"></param>
+        /// <param name="messageActions"></param>
+        /// <returns></returns>
+        [Function("JointEventSchedule")]
+        public async Task JointEventFunc(
+            [ServiceBusTrigger("%Azure:ServiceBus:ActiveTask%", "jointevent-schedule", Connection = "Azure:ServiceBus:ConnectionString")]
+            ServiceBusReceivedMessage message,
+            ServiceBusMessageActions messageActions)
+        {
+            _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;
+            try
+            {
+                jsonMsg.TryGetProperty("jointEventId", out JsonElement jointEventId);
+                jsonMsg.TryGetProperty("jointScheduleId", out JsonElement jointScheduleId);
+                jsonMsg.TryGetProperty("progress", out JsonElement progress);
+
+                var table = _azureStorage.GetCloudTableClient().GetTableReference("ChangeRecord");
+                string PartitionKey = string.Format("{0}{1}{2}{3}{4}{5}{6}", "JointEvent", "-", $"{jointEventId}", "-", "schedule", "-", $"{progress}"); //主key: JointEvent-{jointEventId}-schedule-{progress}  RowKey: {jointScheduleId}
+                List<ChangeRecord> records = await table.FindListByDict<ChangeRecord>(new Dictionary<string, object>() { { "RowKey", $"{jointScheduleId}" }, { "PartitionKey", PartitionKey } });
+
+                bool updFlg = false;
+                string code = "JointEvent";
+                var client = _azureCosmos.GetCosmosClient();
+                
+                JointEvent jointEvent = await client.GetContainer(Constant.TEAMModelOS, "Teacher").ReadItemAsync<JointEvent>(jointEventId.ToString(), new PartitionKey($"{code}"));
+                if (jointEvent != null)
+                {
+                    JointEvent.JointEventSchedule jointEventSchedule = jointEvent.schedule.Where(s => s.id.Equals($"{jointScheduleId}")).FirstOrDefault();
+                    if (jointEventSchedule != null)
+                    {
+                        //資料處理
+                        if (!jointEventSchedule.progress.Equals($"{progress}"))
+                        {
+                            jointEventSchedule.progress = $"{progress}";
+                            updFlg = true;
+                        }
+                        if (updFlg)
+                        {
+                            await client.GetContainer(Constant.TEAMModelOS, "Teacher").ReplaceItemAsync(jointEvent, jointEvent.id);
+                        }
+                        //訊息處理
+                        switch (jointEventSchedule.progress)
+                        {
+                            case "pending":
+                                var msg = new ServiceBusMessage(new { jointEventId = $"{jointEventId}", jointScheduleId = $"{jointScheduleId}", progress = "going" }.ToJsonString());
+                                msg.ApplicationProperties.Add("name", "JointEventSchedule");
+                                if (records.Count > 0)
+                                {
+                                    try
+                                    {
+                                        await _serviceBus.GetServiceBusClient().CancelMessageAsync(Environment.GetEnvironmentVariable("Azure:ServiceBus:ActiveTask"), records[0].sequenceNumber);
+                                    }
+                                    catch (Exception)
+                                    {
+                                    }
+                                    long start = await _serviceBus.GetServiceBusClient().SendScheduleMessageAsync(Environment.GetEnvironmentVariable("Azure:ServiceBus:ActiveTask"), msg, DateTimeOffset.FromUnixTimeMilliseconds(jointEventSchedule.startTime));
+                                    records[0].sequenceNumber = start;
+                                    await table.SaveOrUpdate<ChangeRecord>(records[0]);
+                                }
+                                else
+                                {
+                                    long start = await _serviceBus.GetServiceBusClient().SendScheduleMessageAsync(Environment.GetEnvironmentVariable("Azure:ServiceBus:ActiveTask"), msg, DateTimeOffset.FromUnixTimeMilliseconds(jointEventSchedule.startTime));
+                                    ChangeRecord changeRecord = new ChangeRecord
+                                    {
+                                        RowKey = jointEventSchedule.id,
+                                        PartitionKey = PartitionKey,
+                                        sequenceNumber = start,
+                                        msgId = message.MessageId
+                                    };
+                                    await table.Save<ChangeRecord>(changeRecord);
+                                }
+                                break;
+                            case "going":
+                                //刪除pending訊息
+                                string pkey = string.Format("{0}{1}{2}{3}{4}{5}{6}", "JointEvent", "-", $"{jointEventId}", "-", "schedule", "-", "pending");
+                                await table.DeleteSingle<ChangeRecord>(pkey, jointEventSchedule.id);
+                                //發送finish訊息
+                                var messageEnd = new ServiceBusMessage(new { jointEventId = $"{jointEventId}", jointScheduleId = $"{jointScheduleId}", progress = "finish" }.ToJsonString());
+                                messageEnd.ApplicationProperties.Add("name", "JointEventSchedule");
+                                if (records.Count > 0)
+                                {
+                                    long end = await _serviceBus.GetServiceBusClient().SendScheduleMessageAsync(Environment.GetEnvironmentVariable("Azure:ServiceBus:ActiveTask"), messageEnd, DateTimeOffset.FromUnixTimeMilliseconds(jointEventSchedule.endTime));
+                                    await _serviceBus.GetServiceBusClient().CancelMessageAsync(Environment.GetEnvironmentVariable("Azure:ServiceBus:ActiveTask"), records[0].sequenceNumber);
+                                    records[0].sequenceNumber = end;
+                                    await table.SaveOrUpdate<ChangeRecord>(records[0]);
+                                }
+                                else
+                                {
+                                    long end = await _serviceBus.GetServiceBusClient().SendScheduleMessageAsync(Environment.GetEnvironmentVariable("Azure:ServiceBus:ActiveTask"), messageEnd, DateTimeOffset.FromUnixTimeMilliseconds(jointEventSchedule.endTime));
+                                    ChangeRecord changeRecord = new ChangeRecord
+                                    {
+                                        RowKey = jointEventSchedule.id,
+                                        PartitionKey = PartitionKey,
+                                        sequenceNumber = end,
+                                        msgId = messageEnd.MessageId
+                                    };
+                                    await table.Save<ChangeRecord>(changeRecord);
+                                }
+                                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);
+                                break;
+                        }
+                    }
+                }
+            }
+            catch (CosmosException e)
+            {
+                await _dingDing.SendBotMsg($"{Environment.GetEnvironmentVariable("Option:Location")}-ServiceBus,JointEventScheduleBus()-CosmosDB异常{e.Message}\n{e.StackTrace}\n{e.StatusCode}\n{jsonMsg.ToJsonString()}", GroupNames.醍摩豆服務運維群組);
+            }
+            catch (Exception ex)
+            {
+                await _dingDing.SendBotMsg($"{Environment.GetEnvironmentVariable("Option:Location")}-ServiceBus,JointEventScheduleBus()\n{ex.Message}\n{ex.StackTrace}\n\n{jsonMsg.ToJsonString()}", GroupNames.醍摩豆服務運維群組);
+            }
+            finally
+            {  // Complete the message
+                await messageActions.CompleteMessageAsync(message);
+            }
+
+        }
+
         private async Task RefreshBlob(string name, string u)
         {
             long statr = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

+ 5 - 0
TEAMModelOS.FunctionV4/CosmosDB/TriggerExam.cs

@@ -813,6 +813,11 @@ namespace TEAMModelOS.FunctionV4
                 return stuid;
             }
         }
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="questionData"></param>
+        /// <param name="learnRecordItem"></param>
         private static void setCorrectChoices(QuestionData questionData, LearnRecordItem learnRecordItem)
         {
             #region === Correct Choices ===

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

@@ -6090,7 +6090,11 @@ const LANG_EN_US = {
             artExam: 'Assessment Activity',
             artExam1: 'Assessment Evaluation',
             iotBoard: 'Smart Classroom IoT Dashboard',
-            areaIotboard: 'Smart Classroom IoT Dashboard'
+            areaIotboard: 'Smart Classroom IoT Dashboard',
+            unifiedPurchasingPlatform: 'Unified Purchasing Platform',
+            eventManagement: 'Event Management',
+            completionProgressRevealed: 'Completion progress revealed',
+            announcementResults: 'Announcement results',
         },
         compt: {
             cusWare: 'Teaching Material',
@@ -7973,6 +7977,35 @@ const LANG_EN_US = {
         addTableNotice: 'Note: The scores of the newly added score sheet are based on the current scores in the system, and the scores are not linked to other functions.',
         finishWarning: 'Score statistics will only count the completed evaluation scores.',
     },
+    htcommunity: {
+        addEvent:'New Event',
+        serialNumber:'Serial Number',
+        activityName:'Activity Name',
+        schedule:'Time Schedule',
+        administrator:'Administrator',
+        details:'Details',
+        view:'View',
+        eventExam:'Event Exam',
+        eventArea:'Event Area',
+        group:'Group',
+        groupName:'Group Name',
+        importantTimetable:'Important Timeline',
+        activitiesMustBeCompletedInOrder:'Events must be completed in order',
+        stage:'Stage',
+        description:'Description',
+        Eventlayout:'Event layout',
+        examCanBeChecked:'Exam are repeatable',
+        operation:'Operation',        
+        confirm:'Save',
+        cancel:'Cancel',
+        designatedReviewer:'Designated reviewer',
+        delete:'Delete',
+        registrationGroup:'Registration group',
+        export:'Export',
+        signup:'Sign up',
+        activityPlace:'Place',        
+        fileUrl:'FileUrl',
+    },
     activity: {
         scoreWord: {
             only: 'By Assessment Score:',

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

@@ -6087,7 +6087,11 @@ const LANG_ZH_CN = {
             artExam: '评测活动',
             artExam1: '评测考核',
             iotBoard: 'IoT看板',
-            areaIotboard: '数据看板'
+            areaIotboard: '数据看板',
+            unifiedPurchasingPlatform: '统购平台',
+            eventManagement: '活动后台',
+            completionProgressRevealed: '完成进度揭露',
+            announcementResults: '公告结果',
         },
         compt: {
             cusWare: '课件',
@@ -7974,6 +7978,35 @@ const LANG_ZH_CN = {
         addTableNotice:'注意:新增的成绩表成绩以当下系统有的成绩为准,成绩表成绩没有跟其他功能连动',          
         finishWarning: '成绩统计只会统计已结束的评量成绩',
     },
+    htcommunity: {
+        addEvent:'新增活动',
+        serialNumber:'序号',
+        activityName:'活动名称',
+        schedule:'时程',
+        administrator:'管理员',
+        details:'详细内容',
+        view:'查看',
+        eventExam:'活動評量',
+        eventArea:'活动区域',
+        group:'组别',
+        groupName:'组别名称',
+        importantTimetable:'重要时程',
+        activitiesMustBeCompletedInOrder:'必须按顺序完成活动',
+        stage:'阶段',
+        description:'说明',
+        Eventlayout:'活动布置',
+        examCanBeChecked:'评量可重复',
+        operation:'操作',        
+        confirm:'保存',
+        cancel:'取消',
+        designatedReviewer:'指定評審',
+        delete:'删除',
+        registrationGroup:'报名组别',
+        export:'汇出',
+        signup:'报名',
+        activityPlace:'地点',        
+        fileUrl:'档案连结',
+    },
     activity: {
         scoreWord: {
             only: '按评审分数',

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

@@ -6090,7 +6090,11 @@ const LANG_ZH_TW = {
             artExam: '評量活動',
             artExam1: '評量考核',
             iotBoard: '智慧教室IoT儀表',
-            areaIotboard: '智慧教室IoT儀表'
+            areaIotboard: '智慧教室IoT儀表',
+            unifiedPurchasingPlatform: '統購平台',
+            eventManagement: '活動後台',
+            completionProgressRevealed: '完成進度揭露',
+            announcementResults: '公告結果',
         },
         compt: {
             cusWare: '教材',
@@ -7973,6 +7977,36 @@ const LANG_ZH_TW = {
         addTableNotice: '注意:新增的成績表成績以當下系統有的成績為準,成績表成績沒有跟其他功能連動',
         finishWarning: '成績統計只會統計已結束的評量成績',        
     },
+    htcommunity: {
+        addEvent:'新增活動',
+        serialNumber:'序號',
+        activityName:'活動名稱',
+        schedule:'時程',
+        administrator:'管理員',
+        details:'詳細內容',
+        view:'查看',        
+        eventExam:'活動評量',
+        eventArea:'活動區域',
+        group:'組別',
+        groupName:'組別名稱',
+        importantTimetable:'重要時程',
+        activitiesMustBeCompletedInOrder:'必須按順序完成活動',
+        stage:'階段',
+        description:'說明',
+        Eventlayout:'活動布置',
+        examCanBeChecked:'評量可重複',
+        operation:'操作',        
+        confirm:'保存',
+        cancel:'取消',
+        pleaseChoose:'請選擇',
+        designatedReviewer:'指定評審',
+        delete:'刪除',
+        registrationGroup:'報名組別',
+        export:'匯出',
+        signup:'報名',
+        activityPlace:'地點',        
+        fileUrl:'檔案連結',
+    },
     activity: {
         scoreWord: {
             only: '按評審分數',

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

@@ -0,0 +1,35 @@
+import { post } from '@/api/http'
+export default {
+    // 新增修改統測活動
+    jointEventUpsert: function(data) {
+        return post('/joint/event/upsert', data)
+    },
+    // 新增修改統測活動分組
+    jointEventGroupUpsert: function(data) {
+        return post('/joint/event-group/upsert', data)
+    },
+    // 新增修改統測時程
+    jointScheduleUpsert: function(data) {
+        return post('/joint/schedule/upsert', data)
+    },
+    // 取得統測活動
+    jointEventFind: function(data) {
+        return post('/joint/event/find', data)
+    },
+    // 刪除統測活動分組
+    jointEventGroupDelete: function(data) {
+        return post('/joint/event-group/delete', data)
+    },
+    // 刪除活動行程
+    jointScheduleDelete: function(data) {
+        return post('/joint/schedule/delete', data)
+    },
+    // 取得統測評量
+    jointExamFind: function(data) {
+        return post('/joint/exam/find', data)
+    },
+   
+   
+   
+	
+}

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

@@ -46,6 +46,8 @@ import iot from './iot'
 import areaActivity from './areaActivity'
 import schoolDashborad from './schoolDashborad'
 import appraise from './appraise'
+import htcommunity from './htcommunity'
+
 export default {
     accessToken,
     learnActivity,
@@ -92,6 +94,7 @@ export default {
     areaActivity,
     schoolDashborad,
     appraise,
+    htcommunity,
     // 获取登录跳转链接
     getLoginLink: function (data) {
         return post('api/login/login', data)

+ 14 - 6
TEAMModelOS/ClientApp/src/boot-app.js

@@ -21,9 +21,11 @@ import jwtDecode from 'jwt-decode'
 import GLOBAL from '@/static/Global.js'
 import echarts from 'echarts'
 import vuescroll from 'vuescroll/dist/vuescroll-native'
-import { Tree, Cascader, Switch, Carousel, CarouselItem } from 'element-ui' // 按需引入element Tree组件
+//import { Tree, Cascader, Switch, Carousel, CarouselItem } from 'element-ui' // 按需引入element Tree组件
 import 'element-ui/lib/theme-chalk/tree.css'
 import 'element-ui/lib/theme-chalk/icon.css'
+import ElementUI from 'element-ui';
+import 'element-ui/lib/theme-chalk/index.css';
 import animated from 'animate.css'
 import VueLogger from 'vuejs-logger'
 import access from './access/index'
@@ -84,11 +86,17 @@ Vue.use(konva)
 Vue.use(animated)
 Vue.use(commonComponents)
 Vue.use(evaluationComponents)
-Vue.use(Tree)
-Vue.use(Cascader)
-Vue.use(Switch)
-Vue.use(Carousel)
-Vue.use(CarouselItem)
+
+// Vue.use(Tree)
+// Vue.use(Cascader)
+// Vue.use(Switch)
+// Vue.use(Carousel)
+// Vue.use(CarouselItem)
+Vue.use(ElementUI, {
+    i18n: (key, value) => i18n.t(key, value)
+})
+
+
 // 定义全局日志组件
 Vue.use(VueLogger, {
     isEnabled: true,

+ 42 - 0
TEAMModelOS/ClientApp/src/common/BaseLayout.vue

@@ -1605,6 +1605,48 @@
                 isShow: true
             }, */
 								]
+							},
+							//統購平台
+							{
+								icon: "iconfont icon-yishu",
+								name: this.$t("system.menu.unifiedPurchasingPlatform"),
+								router: "",
+								role: "teacher|admin",
+								permission: "",
+								subName: "unifiedPurchasingPlatform",
+								isShow: this.IES5Menu,
+								child: [
+									{
+										icon: "iconfont icon-basic-setting",
+										name: this.$t("system.menu.eventManagement"),
+										router: "/home/htMgtHome",
+										tag: "",
+										role: "teacher|admin",
+										permission: "",
+										menuName: "eventManagement",
+										isShow: true
+									},
+									// {
+									// 	icon: "iconfont icon-data1",
+									// 	name: this.$t("system.menu.completionProgressRevealed"),
+									// 	router: "/home/htMgtCompleteProgress",
+									// 	tag: "",
+									// 	role: "teacher|admin",
+									// 	permission: "",
+									// 	menuName: "completionProgressRevealed",
+									// 	isShow: true
+									// },
+									// {
+									// 	icon: "iconfont icon-notify",
+									// 	name: this.$t("system.menu.announcementResults"),
+									// 	router: "/home/htMgtAnnouncement",
+									// 	tag: "",
+									// 	role: "teacher|admin",
+									// 	permission: "",
+									// 	menuName: "announcementResults",
+									// 	isShow: true
+									// },
+									]
 							}
 					  ];
 				return data;

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

@@ -1537,6 +1537,51 @@ export const routes = [{
             activeName: 'infoProcess',
         }
     },
+    // 統購平台:活動後台-首頁
+    {
+        path: 'htMgtHome',
+        name: 'htMgtHome',
+        component: () => import('@/view/htcommunity/htMgtHome.vue'),
+        meta: {
+            activeName: 'htMgtHome',
+        }
+    },
+    // 統購平台:活動後台-活動評量
+    {
+        path: 'htMgtExam',
+        name: 'htMgtExam',
+        component: () => import('@/view/htcommunity/htMgtExam.vue'),
+        meta: {
+            activeName: 'htMgtExam',
+        }
+    },
+    // 統購平台:活動後台-完成進度揭露
+    {
+        path: 'htMgtCompleteProgress',
+        name: 'htMgtCompleteProgress',
+        component: () => import('@/view/htcommunity/htMgtCompleteProgress.vue'),
+        meta: {
+            activeName: 'htMgtCompleteProgress',
+        }
+    },
+    // 統購平台:活動後台-公告結果
+    {
+        path: 'htMgtAnnouncementDetail',
+        name: 'htMgtAnnouncementDetail',
+        component: () => import('@/view/htcommunity/htMgtAnnouncementDetail.vue'),
+        meta: {
+            activeName: 'htMgtAnnouncementDetail',
+        }
+    },
+    // 統購平台:活動後台-公告詳細結果
+    {
+        path: 'htMgtAnnouncement',
+        name: 'htMgtAnnouncement',
+        component: () => import('@/view/htcommunity/htMgtAnnouncement.vue'),
+        meta: {
+            activeName: 'htMgtAnnouncement',
+        }
+    },
     ]
 },
 //学生端

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1896 - 0
TEAMModelOS/ClientApp/src/static/region_gl.json


+ 2 - 1
TEAMModelOS/ClientApp/src/utils/js-fn.js

@@ -216,7 +216,7 @@ function secondTimeFormat(timestamp) {
     let H = date.getHours()
     let MIN = date.getMinutes()
     let SEC = date.getSeconds()
-    return `${Y}/${M < 9 ? '0' + (M + 1) : M + 1}/${D < 9 ? '0' + D : D} ${H < 10 ? '0' + H : H}:${MIN < 10 ? '0' + MIN : MIN}:${SEC < 10 ? '0' + SEC : SEC}`
+    return `${Y}/${M < 9 ? '0' + (M + 1) : M + 1}/${D < 10 ? '0' + D : D} ${H < 10 ? '0' + H : H}:${MIN < 10 ? '0' + MIN : MIN}:${SEC < 10 ? '0' + SEC : SEC}`
 }
 
 function timeFormat(timestamp) {
@@ -229,6 +229,7 @@ function timeFormat(timestamp) {
     let MIN = date.getMinutes()
     return `${Y}/${M < 9 ? '0' + (M + 1) : M + 1}/${D < 9 ? '0' + D : D} ${H < 10 ? '0' + H : H}:${MIN < 10 ? '0' + MIN : MIN}`
 }
+//時間戳轉換
 function dateFormat(timestamp) {
     if (timestamp <= 0) return ''
     timestamp = timestamp < 10000000000 ? timestamp * 1000 : timestamp

+ 3 - 3
TEAMModelOS/ClientApp/src/utils/public.js

@@ -552,9 +552,9 @@ export default {
 				.day
 			let startTime = ''
 			let endTime = ''
-			console.error(startMonth)
-			console.error(curYear)
-			console.error(settingSemesters[curSemeterIndex])
+			//console.error(startMonth)
+			//console.error(curYear)
+			//console.error(settingSemesters[curSemeterIndex])
 			// 如果当前学校只设置了一个学期
 			if (settingSemesters.length === 1) {
 				startMonth = settingSemesters[0].month

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 4 - 4
TEAMModelOS/ClientApp/src/view/Home.vue


+ 85 - 0
TEAMModelOS/ClientApp/src/view/htcommunity/htMgtAnnouncement.vue

@@ -0,0 +1,85 @@
+<template>
+    <div class="app-container">
+        <el-row>
+            <el-col :span="5" style="text-align: center">
+                <div style="margin-bottom: 80px;text-align: left;font-size: x-large;">
+                    {{this.$t("htcommunity.eventArea")}}:
+                    <el-select v-model="region" :placeholder="this.$t('htcommunity.pleaseChoose')">
+                        <el-option v-for="item in activeRegion" :key="item.id" :label="item.name" :value="item.id">
+                        </el-option>
+                    </el-select>
+                </div>
+            </el-col>
+        </el-row>
+        <el-card shadow="always" style="width: 800px; margin-left: 120px">
+            <el-row>
+                <el-col :span="24" style="text-align: center">
+                    <el-table :data="tableData" style="width: 100%;">
+                        <el-table-column :label="this.$t('htcommunity.serialNumber')" width="100"  prop="id">                           
+                        </el-table-column>
+                        <el-table-column :label="this.$t('htcommunity.activityName')" width="470" prop="name">                          
+                        </el-table-column>
+                        <el-table-column :label="this.$t('htcommunity.operation')" width="180">
+                            <template slot-scope="scope">
+                                <el-button size="mini" @click="viewDetail(scope.$index, scope.row)" type="primary">{{$t("htcommunity.view")}}</el-button>                                                                
+                            </template>
+                        </el-table-column>
+                    </el-table>
+                </el-col>
+            </el-row>
+        </el-card>
+
+    </div>
+</template>
+<script>
+export default {
+    data() {
+        return {
+            region: "",
+            activeRegion: [
+                {
+                    id: "1",
+                    name: "全部"
+                },
+                {
+                    id: "2",
+                    name: "臺北市"
+                },
+                {
+                    id: "3",
+                    name: "新北市"
+                },
+            ],
+            tableData: [{
+                id: '1',
+                name: '台灣文化藏寶趣'
+            }, {
+                id: '2',
+                name: '閩南語大冒險'
+            }, {
+                id: '3',
+                name: '阿美族的文化傳承'
+            }]
+        }
+    },
+    methods: {
+        viewDetail(index, row) {        
+            this.$router.push({
+					name: "htMgtAnnouncementDetail",
+					params: {
+						index:index
+					}
+				});
+        },
+        handleDelete(index, row) {
+            console.log(index, row);
+        }
+
+    }
+};
+</script>
+<style scoped>
+.app-container {
+  padding: 20px;
+}
+</style>

+ 136 - 0
TEAMModelOS/ClientApp/src/view/htcommunity/htMgtAnnouncementDetail.vue

@@ -0,0 +1,136 @@
+<template>
+    <div class="app-container">
+        <el-row>
+            <el-col :span="5" style="text-align: center">
+                <div style="margin-bottom: 70px;text-align: left;font-size: x-large;">
+                    {{this.$t("htcommunity.eventArea")}}:
+                    <el-select v-model="region" :placeholder="this.$t('htcommunity.pleaseChoose')">
+                        <el-option v-for="item in activeRegion" :key="item.id" :label="item.name" :value="item.id">
+                        </el-option>
+                    </el-select>
+                </div>
+            </el-col>
+        </el-row>
+        <el-card shadow="always" style="width: 65%; margin-left: 120px">
+            <el-row>
+                <el-col :span="24" style="text-align: center">
+                    <div style="margin: 10px">
+                        <el-tabs v-model="activeName" type="card" @tab-click="handleClick">
+                            <el-tab-pane label="賽前模擬" name="first" style="margin-right:10px; "></el-tab-pane>
+                            <el-tab-pane label="活動1" name="second"></el-tab-pane>
+                            <el-tab-pane label="活動2" name="third"></el-tab-pane>
+                            <el-tab-pane label="挑戰賽" name="fourth"></el-tab-pane>
+                        </el-tabs>
+                    </div>
+                    <div style="margin: 10px;text-align:left; margin-bottom: 20px;">
+                        {{this.$t("htcommunity.group")}}:
+                        <el-select v-model="filterGroup" :placeholder="this.$t('htcommunity.pleaseChoose')">
+                            <el-option v-for="item in activeGroups" :key="item.id" :label="item.name" :value="item.id">
+                            </el-option>
+                        </el-select>
+                        <el-button type="success" style="float: right;">{{this.$t("htcommunity.export")}} </el-button>
+                    </div>
+                    <el-table :data="tableData" style="width: 100%;">
+                        <el-table-column label="名次" width="150" prop="rank">
+                        </el-table-column>
+                        <el-table-column label="班級" width="150" prop="class">
+                        </el-table-column>   
+                        <el-table-column label="總人數" width="150" prop="totalNumber">
+                        </el-table-column>   
+                        <el-table-column label="缺考率" width="150" prop="absenteeismRate">
+                        </el-table-column>   
+                        <el-table-column label="及格率" width="150" prop="passingRate">
+                        </el-table-column>   
+                        <el-table-column label="總均分" width="150" prop="overallAverageScore">
+                        </el-table-column>   
+                        <el-table-column label="獎品" width="150" prop="prizes">
+                        </el-table-column>                         
+                    </el-table>
+                </el-col>
+            </el-row>
+        </el-card>
+
+    </div>
+</template>
+<script>
+export default {
+    data() {
+        return {
+            region: "",
+            activeName:"",
+            filterGroup:"",
+            activeRegion: [
+            {
+                    id: "1",
+                    name: "全部"
+                },
+                {
+                    id: "2",
+                    name: "臺北市"
+                },
+                {
+                    id: "3",
+                    name: "新北市"
+                },
+            ],
+            tableData: [
+            {                   
+                    rank: "第一名",
+                    class: "A班",
+                    totalNumber: "25",
+                    absenteeismRate: "0%",
+                    passingRate: "100%",
+                    overallAverageScore: "92.56",
+                    prizes: "3000元",                    
+                },
+                {
+                    rank: "第二名",
+                    class: "A班",
+                    totalNumber: "25",
+                    absenteeismRate: "0%",
+                    passingRate: "100%",
+                    overallAverageScore: "92.56",
+                    prizes: "3000元",   
+                },
+                {
+                    rank: "第三名",
+                    class: "A班",
+                    totalNumber: "25",
+                    absenteeismRate: "0%",
+                    passingRate: "100%",
+                    overallAverageScore: "92.56",
+                    prizes: "3000元",   
+                },
+            ],
+            activeGroups:[
+					{
+						id:"1",
+						name:"國小組"
+					},
+					{
+						id:"2",
+						name:"國中組"
+					},
+					{
+						id:"3",
+						name:"高中組"
+					},
+				],
+        }
+    },
+    methods: {
+        viewDetail(index, row) {        
+                
+        },
+        handleDelete(index, row) {
+            console.log(index, row);
+        }
+
+    }
+};
+</script>
+<style scoped>
+.app-container {
+  padding: 20px;
+}
+</style>

+ 121 - 0
TEAMModelOS/ClientApp/src/view/htcommunity/htMgtCompleteProgress.vue

@@ -0,0 +1,121 @@
+<template>
+    <div class="app-container">
+        <el-row >
+            <el-col :span="5" style="text-align: center">
+                <div style="margin-bottom: 80px;text-align: left;font-size: x-large;">
+                    {{this.$t("htcommunity.eventArea")}}:
+                    <el-select v-model="region" :placeholder="this.$t('htcommunity.pleaseChoose')">
+                        <el-option v-for="item in activeRegion" :key="item.id" :label="item.name" :value="item.id">
+                        </el-option>
+                    </el-select>
+                </div>
+            </el-col>
+        </el-row>
+        <el-card shadow="always">
+        <el-row>
+            <el-col :span="5" style="text-align: center">
+                <div style="margin-bottom: 30px;text-align: left;font-size: x-large;">
+                    {{this.$t("htcommunity.eventArea")}}: <el-link type="primary" :underline="false" style="font-size: x-large;" @click="viewExams" >台灣文化藏寶趣</el-link>
+                </div>
+                <div style="margin-bottom: 10px;">
+                    {{this.$t("htcommunity.registrationGroup")}}:
+                    <el-select v-model="filter.group" 	:placeholder="this.$t('htcommunity.pleaseChoose')">
+                        <el-option v-for="item in activeGroups" :key="item.id" :label="item.name" :value="item.id">
+                        </el-option>
+                    </el-select>
+                </div>
+            </el-col>
+            <el-col :span="3" style="text-align: center" v-for="item in activeItems" :key="item.id" >
+                <div style="margin-bottom: 10px;font-size: large;">{{item.name}}</div>
+                <el-progress type="circle" :percentage=item.percentage></el-progress>
+            </el-col>
+        </el-row>
+        </el-card>
+
+    </div>
+</template>
+  <script>
+  export default {
+    data() {
+        return {
+            region:"",
+            activeRegion: [
+                {
+                    id: "1",
+                    name: "全部"
+                },
+                {
+                    id: "2",
+                    name: "臺北市"
+                },
+                {
+                    id: "3",
+                    name: "新北市"
+                },
+            ],
+            filter: {
+                progress: "",
+                source: "",
+                name: "",
+                group: ""
+            },
+            activeGroups: [
+                {
+                    id: "1",
+                    name: "國小組"
+                },
+                {
+                    id: "2",
+                    name: "國中組"
+                },
+                {
+                    id: "3",
+                    name: "高中組"
+                },
+            ],
+            activeItems: [
+                {
+                    id: "1",
+                    name: "賽前模擬",
+                    percentage:"73"
+                },
+                {
+                    id: "2",
+                    name: "活動1",
+                    percentage:"33"
+                },
+                {
+                    id: "3",
+                    name: "活動2",
+                    percentage:"69"
+                },
+                {
+                    id: "4",
+                    name: "挑戰賽",
+                    percentage:"80"
+                },
+            ],
+            formInline: {
+          user: '',
+          region: ''
+        }
+        }
+    },
+    methods:{
+        viewExams(activity) {
+            this.$router.push({
+                name: "htMgtExam",
+                params: {
+                    activity: activity
+                }
+            });
+        }, 
+
+    }
+};
+</script>
+<style scoped>
+.app-container {
+  padding: 20px;
+}
+</style>

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

@@ -0,0 +1,179 @@
+@main-bgColor: rgb(40,40,40); //主背景颜色
+@borderColor: var(--border-color);
+@primary-textColor: #fff; //文本主颜色
+@second-textColor: var(--second-text-color); //文本副级颜色
+@primary-fontSize: 14px;
+@second-fontSize: 16px;
+@borderColor: #424242;
+
+.manage-evaluation-container {
+    width: 100%;
+    height: 100%;
+    //display: flex;
+    flex-direction: row;
+
+    .evaluation-list-wrap {
+        width:100%;
+        height: 100%;
+        padding-left: 0px;
+        color: var(--second-text-color);
+    }
+
+    .evaluation-detail-wrap {
+        width:100%;
+        height: 100%;
+    }
+}
+.evaluation-list-wrap {
+    .evaluation-list-title {
+        width: 100%;
+        height: 45px;
+        line-height: 45px;
+        padding-left: 15px;
+    }
+
+    .evaluation-list-main {
+        width: 100%;
+        height: ~"calc(100% - 50px)";
+
+        .evaluation-item {
+            padding: 15px 10px 15px 15px;
+            cursor: pointer;
+            background: white;
+            &:hover{
+                background: var(--hover-text-color);
+            }
+
+            .evaluation-name {
+                font-size: 16px;
+                color: var(--primary-text-color);
+            }
+
+            .evaluation-type {
+                color: @second-textColor;
+                margin-top: 4px;
+                font-size: 12px;
+
+                .ivu-icon{
+                    color: var(--assist-color-light);
+                }
+            }
+        }
+    }
+}
+.evaluation-item:hover{
+    .edit-end-time{
+        display: inline-block;
+    }
+}
+.edit-end-time{
+    display: none;
+    font-size: 15px;
+    color: #2d8cf0 !important;
+}
+.all-tag-box{
+    display: flex;
+    align-items: center;
+    margin-top: 6px;
+}
+.tags-wrap{
+    display: flex;
+    flex-wrap: wrap;
+    border: 1px solid #d2d2d2;
+    width: fit-content;
+    border-radius: 3px;
+    overflow: hidden;
+}
+.ev-info-tag{
+    color: #2d8cf0;
+    
+}
+.ev-tag-common{
+    padding: 1px 6px;
+    white-space: nowrap;
+    font-size:12px;
+    border-right: 1px solid #d2d2d2;
+    &:last-child{
+        border: none;
+    }
+}
+.evaluation-status-tag {
+    white-space: nowrap;
+}
+.handle-end-tag{
+    background:#ed4014;
+    color: white;
+    height: fit-content;
+    margin-left: 5px;
+    padding: 2px 6px;
+    border-radius: 2px;
+}
+.ev-qr-tag{
+    text-align: center;
+    padding: 1px 2px 1px 1px;
+    position: absolute;
+    right: 15px;
+    top: 50%;
+    margin-top: -10px;
+    &:hover{
+        color: #2d8cf0;
+    }
+}
+.to-create-icon {
+    float: right;
+    margin-right: 20px;
+    margin-top: 12px;
+    cursor: pointer;
+    color: var(--normal-icon-color);
+    font-size: 18px;
+}
+.filter-item{
+    margin: 10px 5px;
+}
+.evaluation-detail-bar {
+    width: 100%;
+    height: 45px;
+    line-height: 45px;
+    box-shadow: 0 2px 5px #e9e9e9;
+    padding-left: 10px;
+    color: @second-textColor;
+    position: relative;
+
+    .evalustion-bar-item {
+        display: inline-block;
+        cursor: pointer;
+        user-select: none;
+        line-height: 38px;
+    }
+}
+.evaluation-base-info {
+    width: 100%;
+    height: ~"calc(100% - 45px)";
+    // padding-left: 10px;
+    background: #f6f6f6;
+}
+.exam-target{
+    color: #808695;
+    margin: 0px 5px;
+    font-size: 14px;
+    &:hover{
+        color: #2db7f5;
+    }
+}
+.ev-list-scroll{
+    width: 100%;
+    height: 100%;
+}
+.action-icon-wrap{
+    float: right;
+    position: relative;
+    z-index: 99;
+}
+.copy-text-info{
+    padding: 5px 10px;
+    cursor: pointer;
+    user-select: none;
+    &:hover{
+        color: #2d8cf0;
+    }
+}

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1098 - 0
TEAMModelOS/ClientApp/src/view/htcommunity/htMgtExam.vue


+ 745 - 0
TEAMModelOS/ClientApp/src/view/htcommunity/htMgtHome.vue

@@ -0,0 +1,745 @@
+<template>
+  <div id="app" class="app-container">
+    <el-button type="primary" class="btn-new" @click="newEvent">{{this.$t("htcommunity.addEvent")}}</el-button>
+    <el-table :data="activities" class="table-main" border v-loading="mainLoading">
+      <el-table-column prop="index" :label="this.$t('htcommunity.serialNumber')" min-width="50"></el-table-column>
+      <el-table-column prop="name" :label="this.$t('htcommunity.activityName')"></el-table-column>
+      <el-table-column prop="time" :label="this.$t('htcommunity.schedule')" min-width="150"></el-table-column>
+      <el-table-column prop="admin" :label="this.$t('htcommunity.administrator')"></el-table-column>
+      <el-table-column :label="this.$t('htcommunity.details')">
+        <template slot-scope="scope">
+          <el-button type="text" @click="viewDetails(scope.row)">{{$t("htcommunity.view")}}</el-button>
+        </template>
+      </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>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 詳細內容彈出視窗 -->
+    <el-dialog :title="this.$t('htcommunity.details')" :visible.sync="showDetailsModal" width="50%">
+      <p>{{this.$t("htcommunity.activityName")}}: {{ selectedActivity.name }}</p>
+      <p>{{this.$t("htcommunity.schedule")}}: {{ selectedActivity.time }}</p>
+      <p>{{this.$t("htcommunity.administrator")}}: {{ selectedActivity.admin }}</p>
+      <p>原始資料: {{ selectedActivity.originalData }}</p>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="showDetailsModal = false">{{this.$t("htcommunity.cancel")}}</el-button>
+      </span>
+    </el-dialog>
+
+    <!-- 新增活動彈出視窗 -->
+    <el-dialog :title="this.$t('htcommunity.addEvent')" :visible.sync="showAddModal" width="90%"
+      :close-on-click-modal="false" :close-on-press-escape="false" :close="resetNewActivity" top="6vh">
+      <el-form :model="newActivity" label-width="100px" :rules="rules" ref="newActivity">
+        <div class="form-container">
+          <div class="form-left">
+            <el-form-item :label="this.$t('htcommunity.activityName')" prop="name">
+              <el-input v-model="newActivity.name"></el-input>
+            </el-form-item>
+            <el-form-item :label="this.$t('htcommunity.eventArea')">
+              <el-select v-model="newActivity.region" :placeholder="this.$t('htcommunity.pleaseChoose')">
+                <!-- <el-option v-for="(name,code) in twCityArr" :label="name" :value="code" :key="code"></el-option>        -->
+                <el-option v-for="item in twCityArr" :key="item.code" :label="item.name" :value="item.code" />
+              </el-select>
+            </el-form-item>
+            <el-form-item :label="this.$t('htcommunity.administrator')">
+              <el-input v-model="newActivity.admin"></el-input>
+            </el-form-item>
+            <el-form-item :label="this.$t('htcommunity.schedule')" prop="time">
+              <el-date-picker v-model="newActivity.time" type="datetimerange" start-placeholder="Start"
+                end-placeholder="End"  style="width: 400px;">
+              </el-date-picker>
+            </el-form-item>                   
+          </div>
+          <div class="form-right">
+            <!-- 組別表格 -->           
+            <el-form-item :label="this.$t('htcommunity.group')">
+              <!-- <el-tooltip content="指定評審,請檢查確認為正確醍摩豆ID" placement="bottom" effect="light"> -->
+              <el-tooltip placement="bottom" effect="light">
+                <div slot="content" style="color: red;font-size: 1.2em;">指定評審,請輸入醍摩豆用戶編號,檢查確認為正確醍摩豆ID</div>
+                <i class="el-icon-warning-outline" style="font-size: 2em;"></i>
+              </el-tooltip>
+              <!-- <span>指定評審請輸入<span class="point">手機號碼</span>、<span class="point">醍摩豆用戶編號</span>或<span class="point">電子信箱</span>等資訊進行搜尋</span> -->
+              <el-table :data="newActivity.groups" style="width: 100%;" border :cell-style="{ textAlign: 'center' }"
+                :header-cell-style="{ textAlign: 'center' }" v-loading="groupsLoading">
+                <el-table-column prop="groupName" :label="this.$t('htcommunity.groupName')" min-width="180">
+                  <template slot-scope="scope">
+                    <el-input v-model="scope.row.groupName"></el-input>
+                  </template>
+                </el-table-column>
+                <el-table-column prop="groupJudge" :label="this.$t('htcommunity.designatedReviewer')" min-width="245">
+                  <template slot-scope="scope">                   
+                    <el-input v-model="scope.row.groupJudge" style="width: 200px;margin-right: 10px"></el-input>
+                    <el-button type="primary" icon="el-icon-search" size="mini" @click="checkTmdid(scope)"
+                    style="margin-top: 5px;">檢查</el-button>
+                    <span v-if="scope.row.valid">✔️</span>                  
+
+                  </template>
+                </el-table-column>
+                <el-table-column :label="this.$t('htcommunity.operation')" min-width="60">
+                  <template slot-scope="scope">
+                    <el-button type="text" @click="removeGroup(scope)">{{$t("htcommunity.delete")}}</el-button>
+                  </template>
+                </el-table-column>
+              </el-table>
+              <el-button type="primary" class="btn-add" icon="el-icon-plus" @click="addGroup"></el-button>
+            </el-form-item>
+            
+          </div>
+          
+        </div>
+        <div>
+          <p style="font-size: medium; margin-bottom: 10px;">{{this.$t("htcommunity.importantTimetable")}}:</p>
+            <el-checkbox v-model="newActivity.requireOrderCompletion">{{this.$t("htcommunity.activitiesMustBeCompletedInOrder")}}</el-checkbox>
+            <!-- 計畫內容表格 -->
+            <el-table :data="newActivity.planContent" style="width: 100%;" border :cell-style="{ textAlign: 'center' }"
+              :header-cell-style="{ textAlign: 'center' }" v-loading="scheduleLoading">
+              <el-table-column prop="step" label="step" min-width="30px">
+                <template slot-scope="scope">
+                  {{ scope.$index + 1 }}
+                </template>
+              </el-table-column>
+              <el-table-column prop="schedule" :label="this.$t('htcommunity.schedule')" min-width="235px">
+                <template slot-scope="scope">                  
+                  <el-date-picker v-model="scope.row.schedule" type="datetimerange" start-placeholder="Start"
+                    end-placeholder="End"  style="width: 100%;" :disabled="scheduleDisabled(scope, '')">
+                  </el-date-picker>
+                  <span style="color:red;font-size: small;" v-show="scope.row.progress==='going'">進行中時程不可編輯</span>
+                </template>
+              </el-table-column>
+              <el-table-column prop="name" :label="this.$t('htcommunity.stage')" min-width="100px">
+                <template slot-scope="scope">                  
+                  <el-input v-model="scope.row.name" :disabled="scheduleDisabled(scope, '')"></el-input>
+                </template>
+              </el-table-column>              
+              <el-table-column prop="location" :label="this.$t('htcommunity.activityPlace')" min-width="100px">
+                <template slot-scope="scope">                  
+                  <el-input v-model="scope.row.location" :disabled="scheduleDisabled(scope, '')"></el-input>
+                </template>
+              </el-table-column>
+              <el-table-column prop="description" :label="this.$t('htcommunity.description')" min-width="150px">
+                <template slot-scope="scope">                  
+                  <el-input  type="textarea" autosize v-model="scope.row.description" :disabled="scheduleDisabled(scope, '')"></el-input>
+                </template>
+              </el-table-column>
+              <el-table-column prop="blobs" :label="this.$t('htcommunity.fileUrl')" min-width="150px">
+                <template slot-scope="scope">                  
+                  <el-input type="textarea" autosize v-model="scope.row.blobs" :disabled="scheduleDisabled(scope, '')"></el-input>
+                </template>
+              </el-table-column>
+              <el-table-column prop="type" :label="this.$t('htcommunity.signup')" min-width="33px">
+                <template slot-scope="scope">
+                  <el-checkbox v-model="scope.row.type" :disabled="scheduleDisabled(scope, '')"></el-checkbox>                 
+                </template>
+              </el-table-column>
+              <el-table-column prop="examType" :label="this.$t('htcommunity.Eventlayout')" min-width="50px">
+                <template slot-scope="scope">
+                  <el-checkbox v-model="scope.row.examType" :disabled="scheduleDisabled(scope, 'examType')"></el-checkbox>
+                </template>
+              </el-table-column>
+              <el-table-column prop="evaluation" :label="this.$t('htcommunity.examCanBeChecked')" min-width="60px">
+                <template slot-scope="scope">
+                  <el-checkbox v-model="scope.row.evaluation" :disabled="scheduleDisabled(scope, 'evaluation')"></el-checkbox>
+                </template>
+              </el-table-column>
+              <el-table-column :label="this.$t('htcommunity.operation')" min-width="33px">
+                <template slot-scope="scope">
+                  <el-button type="text" @click="removePlanContent(scope)">{{$t("htcommunity.delete")}}</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+            <el-button type="primary" class="btn-add" icon="el-icon-plus" @click="addPlanContent"></el-button>
+          </div>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="cancelUpsert">{{this.$t("htcommunity.cancel")}}</el-button>
+        <el-button type="primary" @click="saveNewEvent">{{this.$t("htcommunity.confirm")}}</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+//import twTCitys from '@/static/twJsonT.js'
+import option_gl from '@/static/region_gl.json'
+import { forEach } from 'jszip';
+export default {
+  data() {
+    return {
+      // 是否為新增模式
+      isInsert:true,
+      mainLoading:false,
+      groupsLoading: false,
+      scheduleLoading: false,
+      //curTwCity: null,
+      twCityArr: [],
+      activities: [
+        // {
+        //   index: "1",
+        //   name: "活動1",
+        //   time: "2024-07-01~2024-07-31",
+        //   admin: "管理員A"
+        // }
+      ],
+      showDetailsModal: false,
+      showAddModal: false,
+      selectedActivity: {},
+      newActivity: {
+        id:'',
+        name: '',
+        region: '',
+        admin: '',
+        time: [],
+        groups: [
+          {id:'', groupName: '', groupJudge: '', valid:false }
+        ],
+        requireOrderCompletion: false,
+        planContent: [
+          {id:'', step: 1, schedule: [], name: '報名時間', location: '', description: '', blobs: '', type: true, examType: false, evaluation: false }
+        ],
+       
+      },
+      // 表單驗證
+      rules: {
+        name: [
+          { required: true, message: '請輸入活動名稱' }
+        ],
+        time: [
+          {
+            required: true, message: '請輸入時程'
+          }
+        ],
+      }
+    };
+  },
+  methods: {
+    // 時程編輯的啟用及關閉  以及  動態切換活動布置及評量可重複啟用及關閉
+    scheduleDisabled(scope, column) {
+      if (scope.row.progress === "going") {
+        return true;
+      } else {
+        if (column === 'examType' || column === 'evaluation') {
+          if (scope.row.type) {
+            scope.row.examType = false;
+            scope.row.evaluation = false;
+            return true;
+          } else {
+            return false;
+          }
+        }
+      }
+    },
+    // 取得列表資料
+    getList(){
+      //先清除列表
+      this.activities.splice(0, this.activities.length);
+      try {
+        this.mainLoading = true;
+        let param = {};
+      //  取得列表資料API
+      this.$api.htcommunity.jointEventFind(param).then(
+          res => {
+            if (res) {
+              let index = 1;
+
+              res.data.forEach(item=>{
+                let activity = {
+                  id: item.id,
+                  index: index,
+                  name: item.name,
+                  time: this.$jsFn.secondTimeFormat(item.startTime) + "~" + this.$jsFn.secondTimeFormat(item.endTime),
+                  admin: item.admin[0],
+                  originalData:item,                             
+                }
+                this.activities.push(activity);
+                index++;
+              });
+                   
+            } else {    
+
+            }
+          },
+          err => {console.log("API error : "+err);  }
+        )
+      } catch (error) {
+        console.log("API error : "+error);  
+      } finally {
+        this.mainLoading = false;
+      }
+      
+
+
+    },
+    //確認tmdID
+    checkTmdid(scope){
+      this.groupsLoading = true;      
+      console.log(scope);
+      // 先檢查有輸入
+      if (scope.row.groupJudge) {
+        //if (scope.row.groupJudge.indexOf(0) == 0) scope.row.groupJudge = scope.row.groupJudge.substr(1) //開頭有0要去0
+       // 先檢查有沒有重複的tmdID
+        // const judgeCount = {};
+        // this.groups.forEach(group => {
+        //   if (group.groupJudge) {
+        //     if (!judgeCount[group.groupJudge]) {
+        //       judgeCount[group.groupJudge] = 0;
+        //     }
+        //     judgeCount[group.groupJudge]++;
+        //   }
+        // });
+
+        // this.groups.forEach(group => {
+        //   if (group.groupJudge) {
+        //     group.valid = judgeCount[group.groupJudge] > 1;
+        //   } else {
+        //     group.valid = false;
+        //   }
+        // });
+
+
+        
+        // 向CoreID取得使用者
+        this.$store.dispatch('user/getUserFromCoreId', [scope.row.groupJudge]).then(
+          (res) => {
+            if (res.code == 1 && res.data.length > 0) {
+              scope.row.valid = true;
+            }
+            else {
+              this.$message({
+                showClose: true,
+                message: '帳號不存在',
+                type: 'error'
+              });
+              scope.row.valid = false;
+            }
+            this.groupsLoading = false;
+          },
+          (err) => {
+            //this.$Message.error('user/getUserFromCoreId API error!')
+          }
+        );
+      } else {
+        this.$message({
+          showClose: true,
+          message: '請輸入帳號',
+          type: 'error'
+        });
+        scope.row.valid = false;
+        this.groupsLoading = false;
+      }        
+
+    },
+    // 新增活動
+    newEvent(){
+      this.isInsert = true;
+      this.showAddModal = true;
+      this.resetNewActivity();
+    },
+    //取消
+    cancelUpsert(){
+      this.showAddModal = false
+      this.resetNewActivity();
+    },
+    //查看詳細內容
+    viewDetails(activity) {
+      this.isInsert = false;
+      // 設定編輯視窗
+      this.showAddModal = true;
+      this.resetNewActivity();
+      let selectedOption = {};
+      if (activity.originalData.geo.cityId) {
+        //optionsData.find(od => od.code == element.cityId)
+        selectedOption = this.twCityArr.find(od => od.code == activity.originalData.geo.cityId);
+        //selectedOption = this.twCityArr[parseInt(activity.originalData.geo.cityId)];
+      }
+
+      // #region 設定活動
+      this.newActivity = {
+        id: activity.id,
+        name: activity.name,
+        region: selectedOption.code === undefined ? '' : selectedOption.code,
+        admin: activity.admin,
+        time: [this.$jsFn.secondTimeFormat(activity.originalData.startTime), this.$jsFn.secondTimeFormat(activity.originalData.endTime)],
+        groups: [
+          //{ groupName: '', groupJudge: '', valid:false }
+        ],
+        requireOrderCompletion: false,
+        planContent: [
+        ],
+      };      
+      // #endregion
+      // #region 設定活動分組      
+      activity.originalData.groups.forEach(item => {
+        let group = {
+          id: item.id,
+          groupName: item.name,
+          groupJudge: item.assistants[0],
+          valid: false
+        }
+        this.newActivity.groups.push(group);
+      });
+      // #endregion
+      // #region 設定活動時程
+      activity.originalData.schedule.forEach(item => {
+       
+        let schedule = {
+          id: item.id,
+          step: item.orderby,
+          schedule: [this.$jsFn.secondTimeFormat(item.startTime), this.$jsFn.secondTimeFormat(item.endTime)],
+          name: item.name,
+          type: item.type === "join" ? true : false,
+          examType: item.examType === "regular" ? true : false,
+          evaluation: item.examOverwrite,
+          progress: item.progress,
+          description: item.description,
+          location: item.location,
+          blobs: item.blobs === null ? "" : item.blobs[0].blob,
+        }        
+        this.newActivity.planContent.push(schedule);
+      });
+
+      // #endregion
+    },
+    //查看活動評量
+    viewExams(activity) {
+      // let params = {
+      //   jointEventId: "37db8a66-660c-43a5-b8ce-6686c070973e", 
+      //   creatorId: "1595321354",
+      //   name:"活動",
+      //   progress:"going" 
+      //   }
+      //   //  保存试卷到cosmos
+      //   this.$api.htcommunity.jointEventFind(params).then(
+      //     res => {
+      //       if (res.errCode === "") {
+      //         console.log("API 資料 : ");
+      //         console.log(res.data);
+      //       } else {
+              
+      //       }
+      //     },
+      //     err => {console.log("API error : "+err);
+            
+      //     }
+      //   )
+      this.$router.push({
+					name: "htMgtExam",
+					params: {
+						data: activity.originalData
+					}
+				});
+    },
+    //儲存新增/更新活動    
+    saveNewEvent() {
+      //呼叫API儲存
+      //const promises = ApplicationData.map(async (item) => {
+      //console.log(item);
+      const startTime = new Date(this.newActivity.time[0]);
+      const endTime = new Date(this.newActivity.time[1]);
+      let tmdid = JSON.parse(decodeURIComponent(localStorage.getItem('t_userInfo'), "utf-8")).id;         
+      // 先新增/更新活動
+      let dataEvent = {
+        jointEventId: this.newActivity.id,
+        name: this.newActivity.name,
+        startTime: startTime.getTime(),
+        endTime: endTime.getTime(),
+        creatorId: tmdid,
+        geo: {
+          countryId: "TW",
+          cityId: this.newActivity.region.toString()
+        },
+        admin: [tmdid],      
+      };
+
+      try {
+        // 新增/更新活動API
+        this.$api.htcommunity.jointEventUpsert(dataEvent).then(
+          async res => {
+            if (res.errCode === "") {              
+              if (res.jointEvent.id) {//如果成功新增/更新活動 新增/更新組別                    
+
+                //#region 新增/更新組別
+                let dataGroup = {
+                  jointEventId: res.jointEvent.id,
+                  groups: []
+                };
+
+                this.newActivity.groups.forEach(item => {
+                  let upItem = {
+                    id: item.id,
+                    name: item.groupName,
+                    assistants: [item.groupJudge]
+                  }
+                  dataGroup.groups.push(upItem);
+                });
+
+                //新增/更新組別API
+                const apiCall1 = this.$api.htcommunity.jointEventGroupUpsert(dataGroup).then(
+                  resGroup => {
+                    if (resGroup.error == null) {
+                      // this.resetNewActivity();
+                      // this.showAddModal = false;
+                      //#region 新增/更新時程
+                      let dataSchedule = {
+                        jointEventId: res.jointEvent.id,
+                        schedules: [
+                        ]
+                      };
+                      this.newActivity.planContent.forEach(item => {                        
+                        let pType = item.type === true ? "join" : "exam";
+                        //let pExamType = item.type === true ? "" : (item.examType === true ? "regular" : "custom");
+                        let pExamType = item.examType === true ? "regular" : "custom";
+                        let pStartTime = new Date(item.schedule[0]);
+                        let pEndTime = new Date(item.schedule[1]);
+                        let upItem = {
+                          id: item.id,
+                          type: pType,
+                          examType: pExamType,
+                          examOverwrite: item.evaluation, //評量可否重複作答 true:可                           
+                          name: item.name, //行程階段
+                          startTime: pStartTime.getTime(),  //起始時間
+                          endTime: pEndTime.getTime(), //結束時間
+                          orderby: item.step, //排序
+                          location: item.location,//地點
+                          description: item.description,//說明
+                          blobs: [
+                            { name: "fileName", blob: item.blobs }
+                          ]//檔案連結
+                        }
+                        dataSchedule.schedules.push(upItem);
+                      });
+
+                      //新增/更新時程API
+                      const apiCall2 = this.$api.htcommunity.jointScheduleUpsert(dataSchedule).then(
+                        resSchedule => {
+                          if (resSchedule.error == null) {
+                            this.resetNewActivity();
+                            this.showAddModal = false;
+                            this.getList();
+
+                          } else {
+                          }
+                        },
+                        err => {console.log("API error : "+err);
+                        }
+                      )
+                      // #endregion
+
+                    } else {
+                    }
+                  },
+                  err => {console.log("API error : "+err);
+                  }
+                )
+                // #endregion
+             
+              }
+            } else {
+              
+            }
+          },
+          err => {console.log("API error : "+err);
+            
+          }
+        )
+      } catch (e) {
+        console.error(e);
+      }
+      finally {
+        this.loading = false;
+       
+      }
+      
+
+    },
+    //重設表單狀態
+    resetNewActivity() {
+      // 重置表單驗證狀態
+      if (this.$refs["newActivity"] !== undefined) {
+        this.$refs["newActivity"].resetFields();
+      }
+
+      this.newActivity = {
+        id:'',
+        name: '',
+        region: '',
+        admin: JSON.parse(decodeURIComponent(localStorage.getItem('t_userInfo'), "utf-8")).id,
+        time: [],
+        groups: [
+          { groupName: '', groupJudge: '', valid:false }
+        ],
+        requireOrderCompletion: false,
+        planContent: [
+          {id:'', step: 1, schedule: [], name: '報名時間', location: '', description: '', blobs: '',  type: true, examType: false, evaluation: false }
+        ]
+      };      
+    },
+    //增加分組
+    addGroup() {
+      this.newActivity.groups.push({ groupName: '', groupJudge: '', valid:false });
+    },
+    //移除分組
+    removeGroup(scope) {
+      this.$confirm('確定要刪除組別?', '提示', {
+        confirmButtonText: '確定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        try {
+          this.groupsLoading = true;
+          let param = {
+            jointEventId: this.newActivity.id,
+            groupId: scope.row.id
+          };
+          //  刪除統測活動分組API
+          this.$api.htcommunity.jointEventGroupDelete(param).then(
+            res => {
+              if (res.errCode === "") {
+                this.newActivity.groups.splice(scope.$index, 1);
+                this.groupsLoading = false;
+                // 重新取列表資料以防頁面顯示錯誤
+                this.getList();
+              } else {
+              }
+            },
+            err => { console.log("API error : " + err); }
+          )
+        } catch (error) {
+          console.log("API error : " + error);
+        } finally {
+        }       
+      }).catch(() => {       
+      }); 
+    },
+    //增加時程
+    addPlanContent() {
+      this.newActivity.planContent.push({id:'', step: this.newActivity.planContent.length + 1, schedule: [], name: '', location: '', description: '', blobs: '',  type: false, examType: false, evaluation: false });
+    },
+    //移除時程
+    removePlanContent(scope) {      
+      this.$confirm('確定要刪除時程?', '提示', {
+        confirmButtonText: '確定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        try {
+          this.scheduleLoading = true;
+          let param = {
+            jointEventId: this.newActivity.id,
+            scheduleId: scope.row.id
+          };
+          //  刪除統測活動分組API
+          this.$api.htcommunity.jointScheduleDelete(param).then(
+            res => {
+              if (res.errCode === "") {
+                this.newActivity.planContent.splice(scope.$index, 1);
+                this.scheduleLoading = false;
+                // 重新取列表資料以防頁面顯示錯誤
+                this.getList();
+              } else {
+              }
+            },
+            err => { console.log("API error : " + err); }
+          )
+        } catch (error) {
+          console.log("API error : " + error);
+        } finally {
+        }       
+      }).catch(() => {       
+      });       
+    }
+  },
+  created () {
+    this.twCityArr = option_gl[0].children;      
+    //this.curTwCity = this.twTCitys[0]
+  },
+  mounted(){
+    this.getList();
+  }
+};
+</script>
+
+<style scoped>
+.app-container {
+  padding: 20px;
+}
+.form-container {
+  display: flex;
+  justify-content: space-between;
+}
+.form-left {
+  width: 40%;
+}
+.form-right {
+  width: 60%;
+}
+.dialog-footer {
+  text-align: right;
+}
+.table-main{
+  width: 80%; margin-top: 20px; margin-left: 20px; margin-right: 20px;
+}
+.btn-new{
+  margin-top: 20px; margin-left: 20px;
+}
+.btn-add{
+  margin-top: 20px; 
+}
+.el-table th, .el-table td {
+  text-align: center;
+  border: 2px solid #000; /* 深色边框 */
+  color: #606266;
+}
+.el-table--border{
+  border-color: #BEBEBE;
+}
+/*表格字体 标签字体*/
+/* .el-table,
+.el-form-item__label {
+  color: #BEBEBE;
+}
+
+.el-table thead {
+  color: #BEBEBE;
+} */
+
+
+.el-table--border:after,
+.el-table--group:after,
+.el-table:before {
+  background-color: #BEBEBE;
+}
+
+/* // 外边框 */
+.el-table--border,
+.el-table--group {
+  border-color: #BEBEBE;
+}
+
+
+/* // 横线 */
+.el-table td,
+.el-table th.is-leaf,
+.el-table--border,
+.el-table--group {
+  border-bottom: 1px solid #BEBEBE;
+}
+
+/* // 竖线 */
+.el-table--border td,
+.el-table--border th {
+  border-right: 1px solid #BEBEBE;
+}
+.el-table td.el-table__cell{
+  border-bottom: 1px solid #BEBEBE;
+  background-color: red;
+}
+.point{
+  color: #40a8f0;
+    font-size: 16px;
+    margin: 0 3px;
+}
+
+</style>

+ 2 - 2
TEAMModelOS/ClientApp/src/view/knowledge-point/index/page.vue

@@ -479,7 +479,7 @@ export default {
                     this.subTree.push({
                         allcids: [],
                         children: [],
-                        id: this.subTree.length ? (Number(this.subTree[this.subTree.length - 1].id) + 1) : '1',
+                        id: this.subTree.length ? (Number(this.subTree[this.subTree.length - 1].id) + 1).toString() : '1',
                         level: 1,
                         link: 0,
                         name: val.name,
@@ -692,7 +692,7 @@ export default {
                                     pointTree.push({
                                         allcids: [],
                                         children: [],
-                                        id: `${pointTree.length ? Number(pointTree[pointTree.length - 1].id) + 1 : (this.subTree.length ? Number(this.subTree[this.subTree.length - 1].id) + 1 : '1')}`,
+                                        id: `${pointTree.length ? (Number(pointTree[pointTree.length - 1].id) + 1).toString() : (this.subTree.length ? (Number(this.subTree[this.subTree.length - 1].id) + 1).toString() : '1')}`,
                                         level: 1,
                                         link: 0,
                                         name: data.results[i][key],

+ 1 - 1
TEAMModelOS/ClientApp/src/view/task/marking/mark/ByStu.vue

@@ -645,7 +645,7 @@ export default {
       if (this.stuAnswer.length) {
         let index = this.quIndex
         this.score = this.stuScore[index] == -1 ? null : this.stuScore[index]
-        let answerOrMark = this.markArr[index] ? this.getMarkHtml(this.markArr[index]) : (this.quNoList[index].type === 'subjective' && this.quNoList[index].answerType != 'text' && this.stuAnswer[index].length ? this.getSubjectiveHtml(this.stuAnswer[index].toString(), this.quNoList[index].answerType) : this.stuAnswer[index])
+        let answerOrMark = this.markArr[index] ? this.getMarkHtml(this.markArr[index]) : (this.quNoList[index].type === 'subjective' && this.quNoList[index].answerType && this.quNoList[index].answerType != 'text' && this.stuAnswer[index].length ? this.getSubjectiveHtml(this.stuAnswer[index].toString(), this.quNoList[index].answerType) : this.stuAnswer[index])
         return answerOrMark
       } else {
         return this.stuId + this.$t('learnActivity.mark.noAnswer')

+ 36 - 16
TEAMModelOS/Controllers/Client/HiTeachController.cs

@@ -207,7 +207,7 @@ namespace TEAMModelOS.Controllers.Client
                 #endregion
 
                 #region 評量
-                string sql3 = $"SELECT *  FROM c WHERE CONTAINS(c.code,'Exam-') and c.endTime > {_startdate} and c.endTime < {_enddate}";
+                string sql3 = $"SELECT *  FROM c WHERE CONTAINS(c.code,'Exam-') and c.startTime > {_startdate} and c.endTime < {_enddate}";
 
                 await foreach (var item in client.GetContainer(Constant.TEAMModelOS, Constant.Common).GetItemQueryIteratorSql<ExamInfo>(queryText: sql3))
                 {
@@ -2335,8 +2335,9 @@ namespace TEAMModelOS.Controllers.Client
                     }
                 }
                 //取得有作答的評測班級
+                string sqltest = $"SELECT c.examId, c.info.id as classId, c.studentAnswers FROM c WHERE ARRAY_CONTAINS({JsonSerializer.Serialize(examIdList)}, c.examId) AND c.progress=true AND CONTAINS(c.code, 'ExamClassResult')";
                 Dictionary<string, List<string>> examClassFinDic = new Dictionary<string, List<string>>();
-                await foreach (var exam in client.GetContainer(Constant.TEAMModelOS, "Common").GetItemQueryStreamIteratorSql(queryText: $"SELECT  c.examId, c.info.id as classId FROM c where (c.status<>404 or IS_DEFINED(c.status) = false ) and  ARRAY_CONTAINS({JsonSerializer.Serialize(examIdList)}, c.examId) AND c.progress=true", requestOptions: new QueryRequestOptions() { }))
+                await foreach (var exam in client.GetContainer(Constant.TEAMModelOS, "Common").GetItemQueryStreamIteratorSql(queryText: $"SELECT c.examId, c.info.id as classId, c.studentAnswers FROM c WHERE ARRAY_CONTAINS({JsonSerializer.Serialize(examIdList)}, c.examId) AND c.progress=true AND CONTAINS(c.code, 'ExamClassResult')", requestOptions: new QueryRequestOptions() ))
                 {
                     var jsonecr = await JsonDocument.ParseAsync(exam.Content);
                     if (jsonecr.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
@@ -2345,15 +2346,24 @@ namespace TEAMModelOS.Controllers.Client
                         {
                             string examId = obj.GetProperty("examId").ToString();
                             string classId = obj.GetProperty("classId").ToString();
-                            if (examClassFinDic.ContainsKey(examId) && !examClassFinDic[examId].Contains(classId))
+                            List<List<string>> studentAnswers = obj.GetProperty("studentAnswers").ToObject<List<List<string>>>();
+                            bool isFinish = false; //評量是否已完成 ※有任一學生有作答則視為已完成
+                            foreach (List<string> studentAnswer in studentAnswers)
                             {
-                                examClassFinDic[examId].Add(classId);
+                                if (studentAnswer.Count > 0) { isFinish = true; break; }
                             }
-                            else
+                            if (isFinish)
                             {
-                                List<string> classIdList = new List<string>();
-                                classIdList.Add(classId);
-                                examClassFinDic.Add(examId, classIdList);
+                                if (examClassFinDic.ContainsKey(examId) && !examClassFinDic[examId].Contains(classId))
+                                {
+                                    examClassFinDic[examId].Add(classId);
+                                }
+                                else
+                                {
+                                    List<string> classIdList = new List<string>();
+                                    classIdList.Add(classId);
+                                    examClassFinDic.Add(examId, classIdList);
+                                }
                             }
                         }
                     }
@@ -2875,24 +2885,34 @@ namespace TEAMModelOS.Controllers.Client
                 }
                 //取得有作答的評測班級、已完成的 評測ID、班級ID、科目ID 列表製作
                 List<ExamFinishClassesSubList> examFinClassSubList = new List<ExamFinishClassesSubList>(); //已完成的 評測ID、班級ID、科目ID 列表
-                await foreach (var exam in client.GetContainer(Constant.TEAMModelOS, "Common").GetItemQueryStreamIteratorSql(queryText: $"SELECT c.examId, c.info.id as classId, c.subjectId FROM c WHERE ARRAY_CONTAINS({JsonSerializer.Serialize(examIdList)}, c.examId) AND c.progress=true", requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"ExamClassResult-{school_code}") }))
+                await foreach (var exam in client.GetContainer(Constant.TEAMModelOS, "Common").GetItemQueryStreamIteratorSql(queryText: $"SELECT c.examId, c.info.id as classId, c.subjectId, c.studentAnswers FROM c WHERE ARRAY_CONTAINS({JsonSerializer.Serialize(examIdList)}, c.examId) AND c.progress=true", requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"ExamClassResult-{school_code}") }))
                 {
                     var jsonecr = await JsonDocument.ParseAsync(exam.Content);
                     if (jsonecr.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
                     {
                         foreach (var obj in jsonecr.RootElement.GetProperty("Documents").EnumerateArray())
                         {
+                            //追加判斷:是否有作答要看 studentAnswers,若其下的array全部都為空array則視為未完成
                             string examId = obj.GetProperty("examId").ToString();
                             string classId = obj.GetProperty("classId").ToString();
                             string subjectId = obj.GetProperty("subjectId").ToString();
-                            ExamFinishClassesSubList existExamFinishClassesSubRow = examFinClassSubList.Where(e => e.examId == examId && e.classId == classId && e.subjectId == subjectId).FirstOrDefault();
-                            if (existExamFinishClassesSubRow == null)
+                            List<List<string>> studentAnswers = obj.GetProperty("studentAnswers").ToObject<List<List<string>>>();
+                            bool isFinish = false; //評量是否已完成 ※有任一學生有作答則視為已完成
+                            foreach(List<string> studentAnswer in studentAnswers)
+                            {
+                                if(studentAnswer.Count > 0) { isFinish = true; break; }
+                            }
+                            if (isFinish)
                             {
-                                ExamFinishClassesSubList ExamFinishClassesSubRow = new ExamFinishClassesSubList();
-                                ExamFinishClassesSubRow.examId = examId;
-                                ExamFinishClassesSubRow.classId = classId;
-                                ExamFinishClassesSubRow.subjectId = subjectId;
-                                examFinClassSubList.Add(ExamFinishClassesSubRow);
+                                ExamFinishClassesSubList existExamFinishClassesSubRow = examFinClassSubList.Where(e => e.examId == examId && e.classId == classId && e.subjectId == subjectId).FirstOrDefault();
+                                if (existExamFinishClassesSubRow == null)
+                                {
+                                    ExamFinishClassesSubList ExamFinishClassesSubRow = new ExamFinishClassesSubList();
+                                    ExamFinishClassesSubRow.examId = examId;
+                                    ExamFinishClassesSubRow.classId = classId;
+                                    ExamFinishClassesSubRow.subjectId = subjectId;
+                                    examFinClassSubList.Add(ExamFinishClassesSubRow);
+                                }
                             }
                         }
                     }

+ 22 - 3
TEAMModelOS/Controllers/Teacher/JointEventController.cs

@@ -20,6 +20,7 @@ using System;
 using static TEAMModelOS.SDK.Models.JointEvent;
 using System.Linq;
 using TEAMModelOS.SDK.Models.Service;
+using Azure.Messaging.ServiceBus;
 
 namespace TEAMModelOS.Controllers.Common
 {
@@ -280,6 +281,7 @@ namespace TEAMModelOS.Controllers.Common
                 if (string.IsNullOrWhiteSpace(jointEventId)) return BadRequest();
                 List<JointEventSchedule> schedules = (request.TryGetProperty("schedules", out JsonElement _schedules)) ? _schedules.ToObject<List<JointEventSchedule>>() : new List<JointEventSchedule>();
                 if (schedules.Count.Equals(0)) return BadRequest();
+                var sbm = new List<ServiceBusMessage>(); //ServiceBus訊息列表
 
                 //取得活動本體
                 JointEvent jointEvent = await client.GetContainer(Constant.TEAMModelOS, "Teacher").ReadItemAsync<JointEvent>(jointEventId, new PartitionKey("JointEvent"));
@@ -317,17 +319,27 @@ namespace TEAMModelOS.Controllers.Common
                     jointEventSchedule.type = schedule.type;
                     jointEventSchedule.examType = (schedule.type.Equals("join")) ? null : schedule.examType;
                     jointEventSchedule.examOverwrite = schedule.examOverwrite;
+                    long startTimeOld = jointEventSchedule.startTime;
+                    long endTimeOld = jointEventSchedule.endTime;
                     jointEventSchedule.startTime = schedule.startTime;
                     jointEventSchedule.endTime = schedule.endTime;
                     jointEventSchedule.progress = (schedule.startTime <= now && now <= schedule.endTime) ? "going" : (now < schedule.startTime) ? "pending" : "finish";
                     jointEventSchedule.orderby = schedule.orderby;
-                    if (!string.IsNullOrWhiteSpace(schedule.location)) jointEventSchedule.location = schedule.location;
-                    if (!string.IsNullOrWhiteSpace(schedule.description)) jointEventSchedule.description = schedule.description;
-                    if(schedule.blobs != null) jointEventSchedule.blobs = schedule.blobs;
+                    if (schedule.location != null) jointEventSchedule.location = schedule.location;
+                    if (schedule.description != null) jointEventSchedule.description = schedule.description;
+                    if (schedule.blobs != null) jointEventSchedule.blobs = schedule.blobs;
                     if (mode.Equals("add"))
                     {
                         jointEvent.schedule.Add(jointEventSchedule);
                     }
+                    //製作Schedule progress更新訊息(active-task)
+                    bool sendScheduleMsg = (!startTimeOld.Equals(jointEventSchedule.startTime) || endTimeOld.Equals(jointEventSchedule.endTime) ) ? true : false; //是否發送變更Schedule.progress訊息
+                    if(sendScheduleMsg)
+                    {
+                        var msg = new ServiceBusMessage(new { jointEventId = $"{jointEvent.id}", jointScheduleId = $"{jointEventSchedule.id}", progress = jointEventSchedule.progress }.ToJsonString());
+                        msg.ApplicationProperties.Add("name", "JointEventSchedule");
+                        sbm.Add(msg);
+                    }
                 }
                 //排序
                 jointEvent.schedule = jointEvent.schedule.OrderBy(s => s.orderby).ToList();
@@ -340,6 +352,13 @@ namespace TEAMModelOS.Controllers.Common
 
                 //DB更新:JointEvent
                 await client.GetContainer(Constant.TEAMModelOS, "Teacher").ReplaceItemAsync<JointEvent>(jointEvent, jointEvent.id, new PartitionKey("JointEvent"));
+
+                //批量發送Schedule progress消息
+                if(sbm.Count > 0)
+                {
+                    var s = await _serviceBus.GetServiceBusClient().SendBatchMessageAsync(_configuration.GetValue<string>("Azure:ServiceBus:ActiveTask"), sbm);
+                }
+
                 return Ok(new { errCode, err, jointEvent });
             }
             catch (Exception e)